mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-20 16:23:39 +00:00
Compare commits
No commits in common. "master" and "v1.0.0-beta.3" have entirely different histories.
master
...
v1.0.0-bet
119 changed files with 9324 additions and 5692 deletions
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
|
@ -1,6 +1 @@
|
|||
github: soywod
|
||||
ko_fi: soywod
|
||||
buy_me_a_coffee: soywod
|
||||
liberapay: soywod
|
||||
thanks_dev: soywod
|
||||
custom: https://www.paypal.com/paypalme/soywod
|
||||
|
|
42
.github/workflows/release-on-demand.yml
vendored
42
.github/workflows/release-on-demand.yml
vendored
|
@ -1,42 +0,0 @@
|
|||
name: Release on demand
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
os:
|
||||
description: Operating system
|
||||
type: choice
|
||||
required: true
|
||||
default: ubuntu-latest
|
||||
options:
|
||||
- ubuntu-24.04
|
||||
- macos-13
|
||||
- macos-14
|
||||
target:
|
||||
description: Architecture
|
||||
type: choice
|
||||
required: true
|
||||
options:
|
||||
- aarch64-apple-darwin
|
||||
- aarch64-unknown-linux-musl
|
||||
- aarch64-unknown-linux-musl
|
||||
- armv6l-unknown-linux-musleabihf
|
||||
- armv7l-unknown-linux-musleabihf
|
||||
- i686-unknown-linux-musl
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-unknown-linux-musl
|
||||
- x86_64-w64-mingw32
|
||||
features:
|
||||
description: Cargo features
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
release-on-demand:
|
||||
uses: pimalaya/nix/.github/workflows/release-on-demand.yml@master
|
||||
secrets: inherit
|
||||
with:
|
||||
project: himalaya
|
||||
os: ${{ inputs.os }}
|
||||
target: ${{ inputs.target }}
|
||||
features: ${{ inputs.features }}
|
160
.github/workflows/release.yml
vendored
Normal file
160
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,160 @@
|
|||
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-unix-releases:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: create-release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: linux
|
||||
os: ubuntu-latest
|
||||
- target: linux-musl
|
||||
os: ubuntu-latest
|
||||
- target: macos
|
||||
os: macos-latest
|
||||
# TODO: uncomment once nix build .#windows works
|
||||
# - target: windows
|
||||
# os: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v24
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-23.11
|
||||
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 -L .#${{ matrix.target }}
|
||||
- name: Copy binary
|
||||
run: |
|
||||
cp result/bin/himalaya* .
|
||||
- name: Patch binary interpreter
|
||||
if: ${{ matrix.target == 'linux' }}
|
||||
run: |
|
||||
nix-shell -p patchelf --command "sudo patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 himalaya"
|
||||
- name: Prepare release archives
|
||||
run: |
|
||||
mkdir -p {man,completions}
|
||||
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.tgz himalaya* man completions
|
||||
zip -r himalaya.zip himalaya* man completions
|
||||
- name: Upload tarball release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya-${{ matrix.target }}.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya-${{ matrix.target }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
# TODO: remove me once nix build .#windows works
|
||||
deploy-windows-release:
|
||||
runs-on: windows-latest
|
||||
needs: create-release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
- name: Build release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release
|
||||
- name: Copy binary
|
||||
run: |
|
||||
copy target/release/himalaya.exe .
|
||||
- name: Prepare release archives
|
||||
run: |
|
||||
mkdir man
|
||||
mkdir completions
|
||||
./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.tgz himalaya.exe man completions
|
||||
Compress-Archive -Path himalaya.exe,man,completions -DestinationPath himalaya.zip
|
||||
- name: Upload tarball release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya-windows.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya-windows.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
publish-crates-io:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v24
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-23.11
|
||||
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}
|
15
.github/workflows/releases.yml
vendored
15
.github/workflows/releases.yml
vendored
|
@ -1,15 +0,0 @@
|
|||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: pimalaya/nix/.github/workflows/releases.yml@master
|
||||
secrets: inherit
|
||||
with:
|
||||
project: himalaya
|
24
.github/workflows/tests.yml
vendored
Normal file
24
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: 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-23.11
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: soywod
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Build then test
|
||||
run: nix build
|
205
CHANGELOG.md
205
CHANGELOG.md
|
@ -7,189 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.0] - 2025-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added `-y|--yes` flag for `folder purge` and `folder delete` commands. [#469]
|
||||
|
||||
### Changed
|
||||
|
||||
- Put back `warn` the default log level. [#522]
|
||||
|
||||
Since logs are sent to `stderr`, warnings can be easily discarded by prepending commands with `RUST_LOG=off` or by appending commands with `2>/dev/null`.
|
||||
|
||||
- Changed `message.send.save-copy` default to `true`. [#536]
|
||||
|
||||
- Changed default downloads directory. [core#1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed permissions issues when using `install.sh`. [#515]
|
||||
- Fixed de/serialization issues of backends' `none` variant. [#523]
|
||||
- Fixed list envelopes out of bound error when empty result. [#535]
|
||||
- Fixed macOS x86_64 builds. [#538]
|
||||
|
||||
## [1.0.0] - 2024-12-09
|
||||
|
||||
The Himalaya CLI scope has changed. It does not include anymore the synchronization, nor the envelope watching. These scopes have moved to dedicated projects:
|
||||
|
||||
- [Neverest CLI](https://github.com/pimalaya/neverest), CLI to synchronize, backup and restore emails
|
||||
- [Mirador CLI](https://github.com/pimalaya/mirador), CLI to watch mailbox changes
|
||||
|
||||
Due to the long time difference with the previous `v1.0.0-beta.4` release, this changelog may be incomplete. The simplest way to upgrade is to reconfigure Himalaya CLI from scratch, using the wizard or the [`config.sample.toml`](./config.sample.toml).
|
||||
|
||||
Himalaya CLI will now try to adopt the [conventional commits specification](https://github.com/conventional-commits/conventionalcommits.org). Tools like [`git-cliff`](https://git-cliff.org/) may help us generating more accurate changelogs in the future.
|
||||
|
||||
### Added
|
||||
|
||||
- Added `message edit` command to edit a message. To edit on place (replace a message), use `--on-place`.
|
||||
- Added `account.list.table.preset` global config option, `accounts.<name>.folder.list.table.preset` and `accounts.<name>.envelope.list.table.preset` account config options.
|
||||
|
||||
These options customize the shape of tables, see examples at [`comfy_table::presets`](https://docs.rs/comfy-table/latest/comfy_table/presets/index.html). Defaults to `"|| |-||| "`, which corresponds to [`comfy_table::presets::ASCII_MARKDOWN`](https://docs.rs/comfy-table/latest/comfy_table/presets/constant.ASCII_MARKDOWN.html).
|
||||
|
||||
- Added `account.list.table.name-color` config option to customize the color used for the accounts' `NAME` column (defaults to `green`).
|
||||
- Added `account.list.table.backends-color` config option to customize the color used for the folders' `BACKENDS` column (defaults to `blue`).
|
||||
- Added `account.list.table.default-color` config option to customize the color used for the folders' `DEFAULT` column (defaults to `reset`).
|
||||
- Added `accounts.<name>.folder.list.table.name-color` account config option to customize the color used for the folders' `NAME` column (defaults to `blue`).
|
||||
- Added `accounts.<name>.folder.list.table.desc-color` account config option to customize the color used for the folders' `DESC` column (defaults to `green`).
|
||||
- Added `accounts.<name>.envelope.list.table.id-color` account config option to customize the color used for the envelopes' `ID` column (defaults to `red`).
|
||||
- Added `accounts.<name>.envelope.list.table.flags-color` account config option to customize the color used for the envelopes' `FLAGS` column (defaults to `reset`).
|
||||
- Added `accounts.<name>.envelope.list.table.subject-color` account config option to customize the color used for the envelopes' `SUBJECT` column (defaults to `green`).
|
||||
- Added `accounts.<name>.envelope.list.table.sender-color` account config option to customize the color used for the envelopes' `FROM` column (defaults to `blue`).
|
||||
- Added `accounts.<name>.envelope.list.table.date-color` account config option to customize the color used for the envelopes' `DATE` column (defaults to `dark_yellow`).
|
||||
- Added `accounts.<name>.envelope.list.table.unseen-char` account config option to customize the char used for unseen envelopes (defaults to `*`).
|
||||
- Added `accounts.<name>.envelope.list.table.replied-char` account config option to customize the char used for replied envelopes (defaults to `R`).
|
||||
- Added `accounts.<name>.envelope.list.table.flagged-char` account config option to customize the char used for flagged envelopes (defaults to `!`).
|
||||
- Added `accounts.<name>.envelope.list.table.attachment-char` account config option to customize the char used for envelopes with at least one attachment (defaults to `@`).
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored the `account configure` command: this command stands now for creating or editing account configurations from the wizard. The command requires the `wizard` cargo feature.
|
||||
- Improved the `account doctor` command: it now checks the state of the config, and the new `--fix` argument allows you to configure keyring, OAuth 2.0 etc.
|
||||
- Improved long version `--version`. [#496]
|
||||
- Improved error messages when missing cargo features. For example, if a TOML configuration uses the IMAP backend without the `imap` cargo features, the error `missing "imap" feature` is displayed. [#20](https://github.com/pimalaya/core/issues/20)
|
||||
- Normalized enum-based configurations, using the [internally tagged representation](https://serde.rs/enum-representations.html#internally-tagged) `type =`. It should reduce issues due to misconfiguration, and improve othe error messages. Yet it is not perfect, see [#802](https://github.com/toml-rs/toml/issues/802):
|
||||
|
||||
- `imap.*`, `maildir.*` and `notmuch.*` moved to `backend.*`:
|
||||
|
||||
```toml
|
||||
# before
|
||||
imap.host = "localhost"
|
||||
imap.port = 143
|
||||
|
||||
# after
|
||||
backend.type = "imap"
|
||||
backend.host = "localhost"
|
||||
backend.port = 143
|
||||
```
|
||||
|
||||
- `smtp.*` and `sendmail.*` moved to `message.send.backend.*`:
|
||||
|
||||
```toml
|
||||
# before
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 25
|
||||
|
||||
# after
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "localhost"
|
||||
message.send.backend.port = 25
|
||||
```
|
||||
|
||||
- `pgp.backend` renamed `pgp.type`:
|
||||
|
||||
```toml
|
||||
# before
|
||||
pgp.backend = "commands"
|
||||
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
|
||||
|
||||
# after
|
||||
pgp.type = "commands"
|
||||
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
|
||||
```
|
||||
|
||||
- `{imap,smtp}.auth` moved as well:
|
||||
|
||||
```toml
|
||||
# before
|
||||
imap.password.cmd = "pass show example"
|
||||
smtp.oauth2.method = "xoauth2"
|
||||
|
||||
# after
|
||||
backend.auth.type = "password"
|
||||
backend.auth.cmd = "pass show example"
|
||||
message.send.backend.auth.type = "oauth2"
|
||||
message.send.backend.auth.method = "xoauth2"
|
||||
```
|
||||
|
||||
- Moved IMAP and SMTP `encryption` to `encryption.type`.
|
||||
|
||||
This change prepares the config to accept different TLS providers with their options. The `true` and `false` variant have been removed as well:
|
||||
|
||||
```toml
|
||||
# before
|
||||
backend.encryption = "none" # or false
|
||||
backend.encryption = "start-tls"
|
||||
message.send.backend.encryption = "tls" # or true
|
||||
|
||||
# after
|
||||
backend.encryption.type = "none"
|
||||
backend.encryption.type = "start-tls"
|
||||
message.send.backend.encryption.type = "tls"
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed pre-release archives issue. [#492]
|
||||
- Fixed mailto parsing issue. [core#10]
|
||||
- Fixed `Answered` flag not set when replying to a message. [#508]
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed systemd service from `assets/` folder, as Himalaya CLI scope does not include synchronization nor watching anymore.
|
||||
|
||||
## [1.0.0-beta.4] - 2024-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added systemd service in `assets/` folder.
|
||||
- Added configuration option `message.delete.style` that can be either `folder` (deleted messages are moved to the Trash folder, default style) or `flag` (deleted messages receive the Deleted flag).
|
||||
- Added `--debug` as an alias for `RUST_LOG=debug`.
|
||||
- Added `--trace` as an alias for `RUST_LOG=trace` and `RUST_BACKTRACE=1`.
|
||||
- Added notes about `--debug` and `--trace` when error occurs.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Added back the search feature**: you can now give an optional filter and sort query at the end of the `envelope list` command. See `envelope list --help` or [pimalaya.org](https://pimalaya.org/himalaya/cli/master/usage/advanced/envelope/list.html#query) for more detail on the search API.
|
||||
- Changed the `envelope list` folder argument due to the search query: it became a flag `--folder|-f`.
|
||||
- Made the global `--config|-c` option repeatable: the first option is considered the path to the main config, and successive options are considered partial overrides [#184].
|
||||
- Refactored error management: error should be more clear, colored and can now contain spantrace and backtrace.
|
||||
- Made `--help` content wrapping properly thanks to the `clap` cargo feature `wrap_help`.
|
||||
- Improved `template {new,reply,forward}` command JSON output: they return now a JSON object with 3 properties:
|
||||
- `content`: the content of the template
|
||||
- `cursor.row`: the row at which the cursor should be placed by the interface using the template
|
||||
- `cursor.col`: the column at which the cursor should be placed by the interface using the template
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed watch IMAP envelopes when folder was empty [#179].
|
||||
- Prevented parsing of undefined config options [#188].
|
||||
- Fixed `In-Reply-To` header being skipped from mailto URLs [#194].
|
||||
- Fixed error page out of bounds when filtering envelopes returned an empty result [#195].
|
||||
|
||||
## [1.0.0-beta.3] - 2024-02-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added `account check-up` command.
|
||||
- Added account check-up command.
|
||||
- Added wizard warning about google passwords [#41].
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed account configurations flatten level in order to improve diagnostic errors, due to a [bug](https://github.com/toml-rs/toml/issues/589#issuecomment-1872345017) in clap. **This means that accounts need to be prefixed by `accounts`: `[my-account]` becomes `[accounts.my-account]`**. It also opens doors for interface-specific configurations.
|
||||
- Rolled back cargo feature additions from the previous release. It was a mistake: the amount of features was too big, the code (both CLI and lib) was too hard to maintain. Cargo features kept: `imap`, `maildir`, `notmuch`, `smtp`, `sendmail`, `account-sync`, `account-discovery`, `pgp-gpg`, `pgp-commands` and `pgp-native`.
|
||||
- Moved `sync.strategy` to `folder.sync.filter`.
|
||||
- Changed location of the synchronization data from `$XDG_DATA_HOME/himalaya/<account-name>` to `$XDG_DATA_HOME/pimalaya/email/sync/<account-name>-cache`.
|
||||
- Changed location of the synchronization cache from `sync.dir` to `$XDG_CACHE_HOME/pimalaya/email/sync/<hash>/`.
|
||||
- Replaced id mapping database `SQLite` by `sled`, a pure key-val store written in Rust to improve portability of the tool. **Therefore, id aliases are reset**.
|
||||
|
@ -913,10 +739,7 @@ Few major concepts changed:
|
|||
- Password from command
|
||||
- Set up README
|
||||
|
||||
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0...HEAD
|
||||
[1.0.0]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.4...v1.0.0
|
||||
[1.0.0-beta.4]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.3...v1.0.0-beta.4
|
||||
[1.0.0-beta.3]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...v1.0.0-beta.3
|
||||
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...HEAD
|
||||
[1.0.0-beta.2]: https://github.com/soywod/himalaya/compare/v1.0.0-beta...v1.0.0-beta.2
|
||||
[1.0.0-beta]: https://github.com/soywod/himalaya/compare/v0.9.0...v1.0.0-beta
|
||||
[0.9.0]: https://github.com/soywod/himalaya/compare/v0.8.4...v0.9.0
|
||||
|
@ -956,17 +779,13 @@ Few major concepts changed:
|
|||
[0.2.0]: https://github.com/soywod/himalaya/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/soywod/himalaya/releases/tag/v0.1.0
|
||||
|
||||
[#469]: https://github.com/pimalaya/himalaya/issues/469
|
||||
[#492]: https://github.com/pimalaya/himalaya/issues/492
|
||||
[#496]: https://github.com/pimalaya/himalaya/issues/496
|
||||
[#508]: https://github.com/pimalaya/himalaya/issues/508
|
||||
[#515]: https://github.com/pimalaya/himalaya/issues/515
|
||||
[#518]: https://github.com/pimalaya/himalaya/issues/518
|
||||
[#522]: https://github.com/pimalaya/himalaya/issues/522
|
||||
[#523]: https://github.com/pimalaya/himalaya/issues/523
|
||||
[#535]: https://github.com/pimalaya/himalaya/issues/535
|
||||
[#536]: https://github.com/pimalaya/himalaya/issues/536
|
||||
[#538]: https://github.com/pimalaya/himalaya/issues/538
|
||||
|
||||
[core#1]: https://github.com/pimalaya/core/issues/1
|
||||
[core#10]: https://github.com/pimalaya/core/issues/10
|
||||
[#39]: https://todo.sr.ht/~soywod/pimalaya/39
|
||||
[#41]: https://todo.sr.ht/~soywod/pimalaya/41
|
||||
[#43]: https://todo.sr.ht/~soywod/pimalaya/43
|
||||
[#54]: https://todo.sr.ht/~soywod/pimalaya/54
|
||||
[#58]: https://todo.sr.ht/~soywod/pimalaya/58
|
||||
[#59]: https://todo.sr.ht/~soywod/pimalaya/59
|
||||
[#60]: https://todo.sr.ht/~soywod/pimalaya/60
|
||||
[#95]: https://todo.sr.ht/~soywod/pimalaya/95
|
||||
[#172]: https://todo.sr.ht/~soywod/pimalaya/172
|
||||
[#173]: https://todo.sr.ht/~soywod/pimalaya/173
|
||||
|
|
|
@ -4,66 +4,28 @@ Thank you for investing your time in contributing to Himalaya CLI!
|
|||
|
||||
## Development
|
||||
|
||||
The development environment is managed by [Nix](https://nixos.org/download.html).
|
||||
Running `nix-shell` will spawn a shell with everything you need to get started with the lib.
|
||||
The development environment is managed by [Nix](https://nixos.org/download.html). Running `nix-shell` will spawn a shell with everything you need to get started with the lib: `cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`, `notmuch`…
|
||||
|
||||
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
|
||||
```sh
|
||||
# Start a Nix shell
|
||||
$ nix-shell
|
||||
|
||||
```text
|
||||
rustup update
|
||||
# then build the CLI
|
||||
$ cargo build
|
||||
|
||||
# run the CLI
|
||||
$ cargo run --feature pgp-gpg -- envelope list
|
||||
```
|
||||
|
||||
or install manually the following dependencies:
|
||||
## Contributing
|
||||
|
||||
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
|
||||
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
|
||||
Himalaya CLI supports open-source, hence the choice of using [SourceHut](https://sourcehut.org/) for managing the project. The only reason why the source code is hosted on GitHub is to build releases for all major platforms (using GitHub Actions). Don't worry, contributing on SourceHut is not a big deal: you just need to send emails! You don't need to create any account. Here a small comparison guide with GitHub:
|
||||
|
||||
## Build
|
||||
The equivalent of **GitHub Discussions** are:
|
||||
|
||||
```text
|
||||
cargo build
|
||||
```
|
||||
- The [Matrix](https://matrix.org/) chat room [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org)
|
||||
- The SourceHut mailing list. You can consult existing messages [here](https://lists.sr.ht/~soywod/pimalaya). You can "open a new discussion" by sending an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). You can also [subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht) and [unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht) to the mailing list, so you can receive a copy of all discussions.
|
||||
|
||||
You can disable default [features](https://doc.rust-lang.org/cargo/reference/features.html) with `--no-default-features` and enable features with `--features feat1,feat2,feat3`.
|
||||
The equivalent of **GitHub Issues** is the SourceHut bug tracker. You can consult existing bugs [here](https://todo.sr.ht/~soywod/pimalaya), and you can "open a new issue" by sending an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
|
||||
Finally, you can build a release with `--release`:
|
||||
|
||||
```text
|
||||
cargo build --no-default-features --features imap,smtp,keyring --release
|
||||
```
|
||||
|
||||
## Override dependencies
|
||||
|
||||
If you want to build Himalaya CLI with a dependency installed locally (for example `email-lib`), then you can [override it](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html) by adding the following lines at the end of `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[patch.crates-io]
|
||||
email-lib = { path = "/path/to/email-lib" }
|
||||
```
|
||||
|
||||
If you get the following error:
|
||||
|
||||
```text
|
||||
note: perhaps two different versions of crate email are being used?
|
||||
```
|
||||
|
||||
then you may need to override more Pimalaya's sub-dependencies:
|
||||
|
||||
```toml
|
||||
[patch.crates-io]
|
||||
email-lib.path = "/path/to/core/email"
|
||||
imap-client.path = "/path/to/imap-client"
|
||||
keyring-lib.path = "/path/to/core/keyring"
|
||||
mml-lib.path = "/path/to/core/mml"
|
||||
oauth-lib.path = "/path/to/core/oauth"
|
||||
pgp-lib.path = "/path/to/core/pgp"
|
||||
pimalaya-tui.path = "/path/to/tui"
|
||||
process-lib.path = "/path/to/core/process"
|
||||
secret-lib.path = "/path/to/core/secret"
|
||||
```
|
||||
|
||||
*See [pimalaya/core#32](https://github.com/pimalaya/core/issues/32) for more information.*
|
||||
|
||||
## Commit style
|
||||
|
||||
Starting from the `v1.0.0`, Himalaya CLI tries to adopt the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary).
|
||||
The equivalent of **GitHub Pull requests** is the SourceHut mailing list. You can "open a new pull request" by sending an email containing a git patch at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). The simplest way to send a patch is to use [git send-email](https://git-scm.com/docs/git-send-email), follow [this guide](https://git-send-email.io/) to configure git properly.
|
||||
|
|
3503
Cargo.lock
generated
3503
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
110
Cargo.toml
110
Cargo.toml
|
@ -1,70 +1,90 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "CLI to manage emails"
|
||||
version = "1.1.0"
|
||||
version = "1.0.0-beta.3"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
categories = ["command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "maildir", "smtp"]
|
||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "smtp", "sync"]
|
||||
homepage = "https://pimalaya.org/"
|
||||
documentation = "https://github.com/pimalaya/himalaya/"
|
||||
repository = "https://github.com/pimalaya/himalaya/"
|
||||
documentation = "https://pimalaya.org/himalaya/cli/latest/"
|
||||
repository = "https://github.com/soywod/himalaya/"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["imap", "maildir", "smtp", "sendmail", "wizard", "pgp-commands"]
|
||||
imap = ["email-lib/imap", "pimalaya-tui/imap"]
|
||||
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
|
||||
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
|
||||
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
|
||||
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
|
||||
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
|
||||
oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
|
||||
wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
|
||||
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"]
|
||||
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"]
|
||||
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"]
|
||||
default = [
|
||||
"imap",
|
||||
"maildir",
|
||||
# "notmuch",
|
||||
"smtp",
|
||||
"sendmail",
|
||||
|
||||
[build-dependencies]
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
|
||||
"account-discovery",
|
||||
"account-sync",
|
||||
|
||||
# "pgp-commands",
|
||||
# "pgp-gpg",
|
||||
# "pgp-native",
|
||||
]
|
||||
|
||||
imap = ["email-lib/imap"]
|
||||
maildir = ["email-lib/maildir"]
|
||||
notmuch = ["email-lib/notmuch"]
|
||||
smtp = ["email-lib/smtp"]
|
||||
sendmail = ["email-lib/sendmail"]
|
||||
|
||||
account-discovery = ["email-lib/account-discovery"]
|
||||
account-sync = ["email-lib/account-sync"]
|
||||
|
||||
pgp = []
|
||||
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"]
|
||||
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pgp"]
|
||||
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pgp"]
|
||||
|
||||
[dev-dependencies]
|
||||
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
|
||||
async-trait = "0.1"
|
||||
tempfile = "3.3"
|
||||
|
||||
[dependencies]
|
||||
ariadne = "0.2"
|
||||
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
chrono = "0.4.24"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clap_complete = "4.4"
|
||||
clap_mangen = "0.2"
|
||||
color-eyre = "0.6"
|
||||
email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
|
||||
mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
|
||||
console = "0.15.2"
|
||||
dialoguer = "0.10.2"
|
||||
dirs = "4"
|
||||
email-lib = { version = "=0.22.3", default-features = false }
|
||||
email_address = "0.2.4"
|
||||
env_logger = "0.8"
|
||||
erased-serde = "0.3"
|
||||
indicatif = "0.17"
|
||||
keyring-lib = "=0.3.2"
|
||||
log = "0.4"
|
||||
mail-builder = "0.3"
|
||||
md5 = "0.7"
|
||||
mml-lib = { version = "=1.0.7", default-features = false }
|
||||
oauth-lib = "=0.1.0"
|
||||
once_cell = "1.16"
|
||||
open = "5.3"
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["rustls", "email", "path", "cli", "himalaya", "tracing", "sled"] }
|
||||
secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
process-lib = "=0.3.1"
|
||||
secret-lib = "=0.3.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
shellexpand-utils = "=0.2.1"
|
||||
shellexpand-utils = "=0.2.0"
|
||||
sled = "=0.34.7"
|
||||
termcolor = "1"
|
||||
terminal_size = "0.1"
|
||||
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.8"
|
||||
tracing = "0.1"
|
||||
toml = "0.7.4"
|
||||
toml_edit = "0.19.8"
|
||||
unicode-width = "0.1"
|
||||
url = "2.2"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
[patch.crates-io]
|
||||
imap-codec.git = "https://github.com/duesee/imap-codec"
|
||||
|
||||
email-lib.git = "https://github.com/pimalaya/core"
|
||||
imap-client.git = "https://github.com/pimalaya/imap-client"
|
||||
keyring-lib.git = "https://github.com/pimalaya/core"
|
||||
mml-lib.git = "https://github.com/pimalaya/core"
|
||||
oauth-lib.git = "https://github.com/pimalaya/core"
|
||||
pgp-lib.git = "https://github.com/pimalaya/core"
|
||||
pimalaya-tui.git = "https://github.com/pimalaya/tui"
|
||||
process-lib.git = "https://github.com/pimalaya/core"
|
||||
secret-lib.git = "https://github.com/pimalaya/core"
|
||||
[target.'cfg(not(windows))'.dependencies.coredump]
|
||||
version = "0.1"
|
||||
|
|
714
README.md
714
README.md
|
@ -1,694 +1,98 @@
|
|||
<div align="center">
|
||||
<img src="./logo.svg" alt="Logo" width="128" height="128" />
|
||||
<h1>📫 Himalaya</h1>
|
||||
<p>CLI to manage emails, based on <a href="https://crates.io/crates/email-lib"><code>email-lib</code></a></p>
|
||||
<p>
|
||||
<a href="https://github.com/pimalaya/himalaya/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/pimalaya/himalaya?color=success"/></a>
|
||||
<a href="https://repology.org/project/himalaya/versions"><img alt="Repology" src="https://img.shields.io/repology/repositories/himalaya?color=success"></a>
|
||||
<a href="https://matrix.to/#/#pimalaya:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/pimalaya:matrix.org?color=success&label=chat"/></a>
|
||||
</p>
|
||||
</div>
|
||||
# 📫 Himalaya [](https://github.com/soywod/himalaya/releases/latest) [](https://matrix.to/#/#pimalaya.himalaya:matrix.org)
|
||||
|
||||
```
|
||||
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
|
||||
```
|
||||
Welcome to [**Himalaya CLI**](https://pimalaya.org/himalaya/cli/latest/), the Command-Line Interface to manage emails based on [email-lib](https://crates.io/crates/email-lib).
|
||||
|
||||

|
||||

|
||||
|
||||
*Disclaimer: the project is under active development, do not use in production before the final `v1.0.0`.*
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-accounting configuration:
|
||||
- interactive via **wizard** (requires `wizard` feature)
|
||||
- manual via **TOML**-based configuration file (see [`./config.sample.toml`](./config.sample.toml))
|
||||
- Message composition based on `$EDITOR`
|
||||
- **IMAP** backend (requires `imap` feature)
|
||||
- **Maildir** backend (requires `maildir` feature)
|
||||
- **Notmuch** backend (requires `notmuch` feature)
|
||||
- **SMTP** backend (requires `smtp` feature)
|
||||
- **Sendmail** backend (requires `sendmail` feature)
|
||||
- Global system **keyring** for secret management (requires `keyring` feature)
|
||||
- **OAuth 2.0** authorization flow (requires `oauth2` feature)
|
||||
- **JSON** output via `--output json`
|
||||
- **PGP** encryption:
|
||||
- via shell commands (requires `pgp-commands` feature)
|
||||
- via [GPG](https://www.gnupg.org/) bindings (requires `pgp-gpg` feature)
|
||||
- via native implementation (requires `pgp-native` feature)
|
||||
|
||||
*Himalaya CLI is written in [Rust](https://www.rust-lang.org/), and relies on [cargo features](https://doc.rust-lang.org/cargo/reference/features.html) to enable or disable functionalities. Default features can be found in the `features` section of the [`Cargo.toml`](./Cargo.toml#L18), or on [docs.rs](https://docs.rs/crate/himalaya/latest/features).*
|
||||
- [Folder (aka mailbox) management](https://pimalaya.org/himalaya/cli/latest/usage/advanced/folder/)
|
||||
- [Envelopes listing](https://pimalaya.org/himalaya/cli/latest/usage/basic/envelope/list.html)
|
||||
- [Message composition](https://pimalaya.org/himalaya/cli/latest/usage/basic/message/send.html) based on `$EDITOR`
|
||||
- Message manipulation ([copy](https://pimalaya.org/himalaya/cli/latest/usage/advanced/message/copy.html)/[move](https://pimalaya.org/himalaya/cli/latest/usage/advanced/message/move.html)/[delete](https://pimalaya.org/himalaya/cli/latest/usage/advanced/message/delete.html))
|
||||
- [Multi-accounting](https://pimalaya.org/himalaya/cli/latest/configuration/)
|
||||
- [Account synchronization](https://pimalaya.org/himalaya/cli/latest/usage/basic/account/sync.html) for offline usage
|
||||
- Support multiple backends: [IMAP](https://pimalaya.org/himalaya/cli/latest/usage/advanced/imap.html), [Maildir](https://pimalaya.org/himalaya/cli/latest/usage/advanced/maildir.html), [Notmuch](https://pimalaya.org/himalaya/cli/latest/usage/advanced/notmuch.html), [SMTP](https://pimalaya.org/himalaya/cli/latest/usage/advanced/smtp.html), [Sendmail](https://pimalaya.org/himalaya/cli/latest/usage/advanced/sendmail.html).
|
||||
- [PGP](https://pimalaya.org/himalaya/cli/latest/usage/advanced/pgp/) end-to-end encryption
|
||||
- Generate [man pages](https://pimalaya.org/himalaya/cli/latest/usage/advanced/man.html)
|
||||
- Generate [completion scripts](https://pimalaya.org/himalaya/cli/latest/usage/advanced/completion.html) for various shells
|
||||
- [JSON output](https://pimalaya.org/himalaya/cli/latest/usage/advanced/#-o--output)
|
||||
- …and more! [Get started now](https://pimalaya.org/himalaya/cli/latest/quickstart.html)
|
||||
|
||||
## Installation
|
||||
|
||||
<details>
|
||||
<summary>Pre-built binary</summary>
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<a href="https://repology.org/project/himalaya/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/himalaya.svg" alt="Packaging status" />
|
||||
</a>
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
Himalaya CLI can be installed with the installer:
|
||||
```bash
|
||||
# Arch Linux (official)
|
||||
$ pacman -S himalaya
|
||||
|
||||
*As root:*
|
||||
# Arch Linux (from sources)
|
||||
$ yay -S himalaya-git
|
||||
|
||||
```
|
||||
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | sudo sh
|
||||
```
|
||||
# Homebrew
|
||||
$ brew install himalaya
|
||||
|
||||
*As a regular user:*
|
||||
# Scoop
|
||||
$ scoop install himalaya
|
||||
|
||||
```
|
||||
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh
|
||||
```
|
||||
# Cargo
|
||||
$ cargo install himalaya
|
||||
|
||||
These commands install the latest binary from the GitHub [releases](https://github.com/pimalaya/himalaya/releases) section.
|
||||
# Nix
|
||||
$ nix-env -i himalaya
|
||||
|
||||
If you want a more up-to-date version than the latest release, check out the [`releases`](https://github.com/pimalaya/himalaya/actions/workflows/releases.yml) GitHub workflow and look for the *Artifacts* section. You should find a pre-built binary matching your OS. These pre-built binaries are built from the `master` branch.
|
||||
# Fedora/CentOS
|
||||
$ dnf copr enable atim/himalaya
|
||||
$ dnf install himalaya
|
||||
```
|
||||
|
||||
*Such binaries are built with the default cargo features. If you want to enable or disable a feature, please use another installation method.*
|
||||
</details>
|
||||
*See the [documentation](https://pimalaya.org/himalaya/cli/latest/installation.html) for other installation methods.*
|
||||
|
||||
<details>
|
||||
<summary>Cargo</summary>
|
||||
|
||||
Himalaya CLI can be installed with [cargo](https://doc.rust-lang.org/cargo/):
|
||||
|
||||
```
|
||||
cargo install himalaya
|
||||
```
|
||||
|
||||
*With only IMAP support:*
|
||||
|
||||
```
|
||||
cargo install himalaya --no-default-features --features imap
|
||||
```
|
||||
|
||||
You can also use the git repository for a more up-to-date (but less stable) version:
|
||||
|
||||
```
|
||||
cargo install --frozen --force --git https://github.com/pimalaya/himalaya.git
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Arch Linux</summary>
|
||||
|
||||
Himalaya CLI can be installed on [Arch Linux](https://archlinux.org/) with either the community repository:
|
||||
|
||||
```
|
||||
pacman -S himalaya
|
||||
```
|
||||
|
||||
or the [user repository](https://aur.archlinux.org/):
|
||||
|
||||
```
|
||||
git clone https://aur.archlinux.org/himalaya-git.git
|
||||
cd himalaya-git
|
||||
makepkg -isc
|
||||
```
|
||||
|
||||
If you use [yay](https://github.com/Jguer/yay), it is even simplier:
|
||||
|
||||
```
|
||||
yay -S himalaya-git
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Homebrew</summary>
|
||||
|
||||
Himalaya CLI can be installed with [Homebrew](https://brew.sh/):
|
||||
|
||||
```
|
||||
brew install himalaya
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Scoop</summary>
|
||||
|
||||
Himalaya CLI can be installed with [Scoop](https://scoop.sh/):
|
||||
|
||||
```
|
||||
scoop install himalaya
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fedora Linux/CentOS/RHEL</summary>
|
||||
|
||||
Himalaya CLI can be installed on [Fedora Linux](https://fedoraproject.org/)/CentOS/RHEL via [COPR](https://copr.fedorainfracloud.org/coprs/atim/himalaya/) repo:
|
||||
|
||||
```
|
||||
dnf copr enable atim/himalaya
|
||||
dnf install himalaya
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Nix</summary>
|
||||
|
||||
Himalaya CLI can be installed with [Nix](https://serokell.io/blog/what-is-nix):
|
||||
|
||||
```
|
||||
nix-env -i himalaya
|
||||
```
|
||||
|
||||
You can also use the git repository for a more up-to-date (but less stable) version:
|
||||
|
||||
```
|
||||
nix-env -if https://github.com/pimalaya/himalaya/archive/master.tar.gz
|
||||
```
|
||||
|
||||
*Or, from within the source tree checkout:*
|
||||
|
||||
```
|
||||
nix-env -if .
|
||||
```
|
||||
|
||||
If you have the [Flakes](https://nixos.wiki/wiki/Flakes) feature enabled:
|
||||
|
||||
```
|
||||
nix profile install himalaya
|
||||
```
|
||||
|
||||
*Or, from within the source tree checkout:*
|
||||
|
||||
```
|
||||
nix profile install
|
||||
```
|
||||
|
||||
*You can also run Himalaya directly without installing it:*
|
||||
|
||||
```
|
||||
nix run himalaya
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Sources</summary>
|
||||
|
||||
Himalaya CLI can be installed from sources.
|
||||
|
||||
First you need to install the Rust development environment (see the [rust installation documentation](https://doc.rust-lang.org/cargo/getting-started/installation.html)):
|
||||
|
||||
```
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
```
|
||||
|
||||
Then, you need to clone the repository and install dependencies:
|
||||
|
||||
```
|
||||
git clone https://github.com/pimalaya/himalaya.git
|
||||
cd himalaya
|
||||
cargo check
|
||||
```
|
||||
|
||||
Now, you can build Himalaya:
|
||||
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
*Binaries are available under the `target/release` folder.*
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Configuration
|
||||
|
||||
Just run `himalaya`, the wizard will help you to configure your default account.
|
||||
*Please read the [documentation](https://pimalaya.org/himalaya/cli/latest/configuration/).*
|
||||
|
||||
Accounts can be (re)configured via the wizard using the command `himalaya account configure <name>`.
|
||||
## Contributing
|
||||
|
||||
You can also manually edit your own configuration, from scratch:
|
||||
*Please read the [contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) for more detailed information.*
|
||||
|
||||
- Copy the content of the documented [`./config.sample.toml`](./config.sample.toml)
|
||||
- Paste it in a new file `~/.config/himalaya/config.toml`
|
||||
- Edit, then comment or uncomment the options you want
|
||||
A **bug tracker** is available on [SourceHut](https://todo.sr.ht/~soywod/pimalaya). <sup>[[send an email](mailto:~soywod/pimalaya@todo.sr.ht)]</sup>
|
||||
|
||||
<details>
|
||||
<summary>Proton Mail (Bridge)</summary>
|
||||
A **mailing list** is available on [SourceHut](https://lists.sr.ht/~soywod/pimalaya). <sup>[[send an email](mailto:~soywod/pimalaya@lists.sr.ht)] [[subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht)] [[unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht)]</sup>
|
||||
|
||||
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
|
||||
If you want to **report a bug**, please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
|
||||
- Id order may be reversed or shuffled, but envelopes will still be sorted by date.
|
||||
- SSL/TLS needs to be deactivated manually.
|
||||
- The password to use is the one generated by Proton Bridge, not the one from your Proton Mail account.
|
||||
If you want to **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). The simplest way to send a patch is to use [git send-email](https://git-scm.com/docs/git-send-email), follow [this guide](https://git-send-email.io/) to configure git properly.
|
||||
|
||||
```toml
|
||||
[accounts.proton]
|
||||
email = "example@proton.me"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "127.0.0.1"
|
||||
backend.port = 1143
|
||||
backend.encryption.type = "none"
|
||||
backend.login = "example@proton.me"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "127.0.0.1"
|
||||
message.send.backend.port = 1025
|
||||
message.send.backend.encryption.type = "none"
|
||||
message.send.backend.login = "example@proton.me"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show proton"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "proton-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a proton` will ask for your IMAP password, just paste the one generated previously.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Gmail</summary>
|
||||
|
||||
Google passwords cannot be used directly. There is two ways to authenticate yourself:
|
||||
|
||||
### Using [App Passwords](https://support.google.com/mail/answer/185833)
|
||||
|
||||
This option is the simplest and the fastest. First, be sure that:
|
||||
|
||||
- IMAP is enabled
|
||||
- Two-step authentication is enabled
|
||||
- Less secure app access is enabled
|
||||
|
||||
First create a [dedicated password](https://myaccount.google.com/apppasswords) for Himalaya.
|
||||
|
||||
```toml
|
||||
[accounts.gmail]
|
||||
email = "example@gmail.com"
|
||||
|
||||
folder.aliases.inbox = "INBOX"
|
||||
folder.aliases.sent = "[Gmail]/Sent Mail"
|
||||
folder.aliases.drafts = "[Gmail]/Drafts"
|
||||
folder.aliases.trash = "[Gmail]/Trash"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.gmail.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@gmail.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.gmail.com"
|
||||
message.send.backend.port = 465
|
||||
message.send.backend.login = "example@gmail.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.cmd = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show gmail"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "gmail-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a gmail` will ask for your IMAP password, just paste the one generated previously.
|
||||
|
||||
### Using OAuth 2.0
|
||||
|
||||
This option is the most secure but the hardest to configure. It requires the `oauth2` and `keyring` cargo features.
|
||||
|
||||
First, you need to get your OAuth 2.0 credentials by following [this guide](https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name-.). Once you get your client id and your client secret, you can configure your Himalaya account this way:
|
||||
|
||||
```toml
|
||||
[accounts.gmail]
|
||||
email = "example@gmail.com"
|
||||
|
||||
folder.aliases.inbox = "INBOX"
|
||||
folder.aliases.sent = "[Gmail]/Sent Mail"
|
||||
folder.aliases.drafts = "[Gmail]/Drafts"
|
||||
folder.aliases.trash = "[Gmail]/Trash"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.gmail.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@gmail.com"
|
||||
backend.auth.type = "oauth2"
|
||||
backend.auth.client-id = "*****"
|
||||
backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
|
||||
backend.auth.access-token.keyring = "gmail-oauth2-access-token"
|
||||
backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
|
||||
backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
backend.auth.pkce = true
|
||||
backend.auth.scope = "https://mail.google.com/"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.gmail.com"
|
||||
message.send.backend.port = 465
|
||||
message.send.backend.login = "example@gmail.com"
|
||||
message.send.backend.auth.type = "oauth2"
|
||||
message.send.backend.auth.client-id = "*****"
|
||||
message.send.backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
|
||||
message.send.backend.auth.access-token.keyring = "gmail-oauth2-access-token"
|
||||
message.send.backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
|
||||
message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
message.send.backend.auth.pkce = true
|
||||
message.send.backend.auth.scope = "https://mail.google.com/"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a gmail` will complete your OAuth 2.0 setup and ask for your client secret.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Outlook</summary>
|
||||
|
||||
```toml
|
||||
[accounts.outlook]
|
||||
email = "example@outlook.com"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "outlook.office365.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@outlook.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp-mail.outlook.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "example@outlook.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show outlook"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "outlook-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a outlook` will ask for your IMAP password, just paste the one generated previously.
|
||||
|
||||
### Using OAuth 2.0
|
||||
|
||||
This option is the most secure but the hardest to configure. First, you need to get your OAuth 2.0 credentials by following [this guide](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth). Once you get your client id and your client secret, you can configure your Himalaya account this way:
|
||||
|
||||
```toml
|
||||
[accounts.outlook]
|
||||
email = "example@outlook.com"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "outlook.office365.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@outlook.com"
|
||||
backend.auth.type = "oauth2"
|
||||
backend.auth.client-id = "*****"
|
||||
backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
|
||||
backend.auth.access-token.keyring = "outlook-oauth2-access-token"
|
||||
backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
|
||||
backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||
backend.auth.pkce = true
|
||||
backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.mail.outlook.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.starttls = true
|
||||
message.send.backend.login = "example@outlook.com"
|
||||
message.send.backend.auth.type = "oauth2"
|
||||
message.send.backend.auth.client-id = "*****"
|
||||
message.send.backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
|
||||
message.send.backend.auth.access-token.keyring = "outlook-oauth2-access-token"
|
||||
message.send.backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
|
||||
message.send.backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
message.send.backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||
message.send.backend.auth.pkce = true
|
||||
message.send.backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
|
||||
```
|
||||
|
||||
Running `himalaya configure -a outlook` will complete your OAuth 2.0 setup and ask for your client secret.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>iCloud Mail</summary>
|
||||
|
||||
From the [iCloud Mail](https://support.apple.com/en-us/HT202304) support page:
|
||||
|
||||
- IMAP port = `993`.
|
||||
- IMAP login = name of your iCloud Mail email address (for example, `johnappleseed`, not `johnappleseed@icloud.com`)
|
||||
- SMTP port = `587` with `STARTTLS`
|
||||
- SMTP login = full iCloud Mail email address (for example, `johnappleseed@icloud.com`, not `johnappleseed`)
|
||||
|
||||
```toml
|
||||
[accounts.icloud]
|
||||
email = "johnappleseed@icloud.com"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.mail.me.com"
|
||||
backend.port = 993
|
||||
backend.login = "johnappleseed"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.mail.me.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "johnappleseed@icloud.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show icloud"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "icloud-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a icloud` will ask for your IMAP password, just paste the one generated previously.
|
||||
</details>
|
||||
|
||||
## Other interfaces
|
||||
|
||||
- [pimalaya/himalaya-vim](https://github.com/pimalaya/himalaya-vim), a Vim plugin sitting at the top of Himalaya CLI
|
||||
- [dantecatalfamo/himalaya-emacs](https://github.com/dantecatalfamo/himalaya-emacs), an Emacs plugin sitting at the top of Himalaya CLI
|
||||
- [jns/himalaya-raycast](https://www.raycast.com/jns/himalaya), a Raycast extension for Himalaya CLI
|
||||
- [pimalaya/himalaya-repl](https://github.com/pimalaya/himalaya-repl), an experimental Read-Eval-Print-Loop variant of Himalaya CLI
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary>How different is it from aerc, mutt or alpine?</summary>
|
||||
|
||||
Aerc, mutt and alpine can be categorized as Terminal User Interfaces (TUI). When the program is executed, your terminal is locked into an event loop and you interact with your emails using keybinds.
|
||||
|
||||
Himalaya is also a TUI, but more specifically a Command-Line Interface (CLI). There is no event loop: you interact with your emails using shell commands, in a stateless way.
|
||||
|
||||
Additionaly, Himalaya CLI is based on `email-lib`, which is also part of the Pimalaya project. The aim is not just to propose a new terminal interface, but also to expose Rust tools to deal with emails. Anyone who knows Rust language can build his own email interface, without re-inventing the wheel.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to compose a message?</summary>
|
||||
|
||||
An email message is a list of **headers** (`key: val`) followed by a **body**. They form together a template:
|
||||
|
||||
```eml
|
||||
Header: value
|
||||
Header: value
|
||||
Header: value
|
||||
|
||||
Body
|
||||
```
|
||||
|
||||
***Headers and body must be separated by an empty line.***
|
||||
|
||||
### Headers
|
||||
|
||||
Here a non-exhaustive list of valid email message template headers:
|
||||
|
||||
- `Message-ID`: represents the message identifier (you usually do not need to set up it manually)
|
||||
- `In-Reply-To`: represents the identifier of the replied message
|
||||
- `Date`: represents the date of the message
|
||||
- `Subject`: represents the subject of the message
|
||||
- `From`: represents the address of the sender
|
||||
- `To`: represents the addresses of the receivers
|
||||
- `Reply-To`: represents the address the receiver should reply to instead of the `From` header
|
||||
- `Cc`: represents the addresses of the other receivers (carbon copy)
|
||||
- `Bcc`: represents the addresses of the other hidden receivers (blind carbon copy)
|
||||
|
||||
An address can be:
|
||||
|
||||
- a single email address `user@domain`
|
||||
- a named address `Name <user@domain>`
|
||||
- a quoted named address `"Name" <user@domain>`
|
||||
|
||||
Multiple address are separated by a coma `,`: `user@domain, Name <user@domain>, "Name" <user@domain>`.
|
||||
|
||||
### Plain text body
|
||||
|
||||
Email message template body can be written in plain text. The result will be compiled into a single `text/plain` MIME part:
|
||||
|
||||
```eml
|
||||
From: alice@localhost
|
||||
To: Bob <bob@localhost>
|
||||
Subject: Hello from Himalaya
|
||||
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
### MML body
|
||||
|
||||
Email message template body can also be written in MML. The MIME Meta Language was introduced by the Emacs [`mml`](https://www.gnu.org/software/emacs/manual/html_node/emacs-mime/Composing.html) ELisp module. Pimalaya [ported it](https://github.com/pimalaya/core/tree/master/mml) in Rust.
|
||||
|
||||
A raw email message is structured according to the [MIME](https://www.rfc-editor.org/rfc/rfc2045) standard. This standard produces verbose, non-friendly messages. Here comes MML: it simplifies the way email message body are structured. Thanks to its simple XML-based syntax, it allows you to easily add multiple parts, attach a binary file, or attach inline image to your body without dealing with the MIME standard.
|
||||
|
||||
For instance, this MML template:
|
||||
|
||||
```eml
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: MML simple
|
||||
|
||||
<#multipart type=alternative>
|
||||
This is a plain text part.
|
||||
<#part type=text/enriched>
|
||||
<center>This is a centered enriched part</center>
|
||||
<#/multipart>
|
||||
```
|
||||
|
||||
compiles into the following MIME Message:
|
||||
|
||||
```eml
|
||||
Subject: MML simple
|
||||
To: bob@localhost
|
||||
From: alice@localhost
|
||||
MIME-Version: 1.0
|
||||
Date: Tue, 29 Nov 2022 13:07:01 +0000
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe"
|
||||
|
||||
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is a plain text part.
|
||||
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
|
||||
Content-Type: text/enriched
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<center>This is a centered enriched part</center>
|
||||
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe--
|
||||
```
|
||||
|
||||
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to add attachments to a message?</summary>
|
||||
|
||||
*Read first about the FAQ: How to compose a message?*.
|
||||
|
||||
```eml
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: How to attach stuff
|
||||
|
||||
Regular binary attachment:
|
||||
<#part filename=/path/to/file.pdf><#/part>
|
||||
|
||||
Custom file name:
|
||||
<#part filename=/path/to/file.pdf name=custom.pdf><#/part>
|
||||
|
||||
Inline image:
|
||||
<#part disposition=inline filename=/path/to/image.png><#/part>
|
||||
```
|
||||
|
||||
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to debug Himalaya CLI?</summary>
|
||||
|
||||
The simplest way is to use `--debug` and `--trace` arguments.
|
||||
|
||||
The advanced way is based on environment variables:
|
||||
|
||||
- `RUST_LOG=<level>`: determines the log level filter, can be one of `off`, `error`, `warn`, `info`, `debug` and `trace`.
|
||||
- `RUST_SPANTRACE=1`: enables the spantrace (a span represent periods of time in which a program was executing in a particular context).
|
||||
- `RUST_BACKTRACE=1`: enables the error backtrace.
|
||||
- `RUST_BACKTRACE=full`: enables the full error backtrace, which include source lines where the error originated from.
|
||||
|
||||
Logs are written to the `stderr`, which means that you can redirect them easily to a file:
|
||||
|
||||
```
|
||||
RUST_LOG=debug himalaya 2>/tmp/himalaya.log
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How the wizard discovers IMAP/SMTP configs?</summary>
|
||||
|
||||
All the lookup mechanisms use the email address domain as base for the lookup. It is heavily inspired from the Thunderbird [Autoconfiguration](https://udn.realityripple.com/docs/Mozilla/Thunderbird/Autoconfiguration) protocol. For example, for the email address `test@example.com`, the lookup is performed as (in this order):
|
||||
|
||||
1. check for `autoconfig.example.com`
|
||||
2. look up of `example.com` in the ISPDB (the Thunderbird central database)
|
||||
3. look up `MX example.com` in DNS, and for `mx1.mail.hoster.com`, look up `hoster.com` in the ISPDB
|
||||
4. look up `SRV example.com` in DNS
|
||||
5. try to guess (`imap.example.com`, `smtp.example.com`…)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to disable color output?</summary>
|
||||
|
||||
Simply set the environment variable NO_COLOR=1
|
||||
</details>
|
||||
If you just want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org). You can also use the mailing list.
|
||||
|
||||
## Sponsoring
|
||||
|
||||
[](https://nlnet.nl/)
|
||||
[](https://nlnet.nl/project/Himalaya/index.html)
|
||||
|
||||
Special thanks to the [NLnet foundation](https://nlnet.nl/) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from various programs:
|
||||
Special thanks to the [NLnet foundation](https://nlnet.nl/project/Himalaya/index.html) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from:
|
||||
|
||||
- [NGI Assure](https://nlnet.nl/project/Himalaya/) in 2022
|
||||
- [NGI Zero Entrust](https://nlnet.nl/project/Pimalaya/) in 2023
|
||||
- [NGI Zero Core](https://nlnet.nl/project/Pimalaya-PIM/) in 2024 *(still ongoing)*
|
||||
- [NGI Assure](https://nlnet.nl/assure/) in 2022
|
||||
- [NGI Zero Entrust](https://nlnet.nl/entrust/) in 2023
|
||||
|
||||
If you appreciate the project, feel free to donate using one of the following providers:
|
||||
|
||||
[](https://github.com/sponsors/soywod)
|
||||
[](https://www.paypal.com/paypalme/soywod)
|
||||
[](https://ko-fi.com/soywod)
|
||||
[](https://www.buymeacoffee.com/soywod)
|
||||
[](https://liberapay.com/soywod)
|
||||
[](https://thanks.dev/soywod)
|
||||
[](https://www.paypal.com/paypalme/soywod)
|
||||
|
|
|
@ -3,7 +3,7 @@ Type=Application
|
|||
Name=himalaya
|
||||
DesktopName=Himalaya
|
||||
GenericName=Mail Reader
|
||||
Comment=CLI to manage emails
|
||||
Comment=Command-line interface for email management
|
||||
Terminal=true
|
||||
Exec=himalaya %U
|
||||
Categories=Application;Network
|
||||
|
@ -13,4 +13,4 @@ Actions=Compose
|
|||
|
||||
[Desktop Action Compose]
|
||||
Name=Compose
|
||||
Exec=himalaya message write %U
|
||||
Exec=himalaya write %U
|
||||
|
|
7
build.rs
7
build.rs
|
@ -1,7 +0,0 @@
|
|||
use pimalaya_tui::build::{features_env, git_envs, target_envs};
|
||||
|
||||
fn main() {
|
||||
features_env(include_str!("./Cargo.toml"));
|
||||
target_envs();
|
||||
git_envs();
|
||||
}
|
|
@ -1,651 +1,98 @@
|
|||
################################################################################
|
||||
###[ Global configuration ]#####################################################
|
||||
################################################################################
|
||||
|
||||
# Default display name for all accounts. It is used to build the full
|
||||
# email address of an account: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# Default signature for all accounts. The signature is put at the
|
||||
# bottom of all messages. It can be a path or a string. Supports TOML
|
||||
# multilines.
|
||||
#
|
||||
#signature = "/path/to/signature/file"
|
||||
#signature = """
|
||||
# Thanks you,
|
||||
# Regards
|
||||
#"""
|
||||
signature = "Regards,\n"
|
||||
|
||||
# Default signature delimiter for all accounts. It delimits the end of
|
||||
# the message body from the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# Default downloads directory path for all accounts. It is mostly used
|
||||
# for downloading attachments. Defaults to the system temporary
|
||||
# directory.
|
||||
#
|
||||
downloads-dir = "~/Downloads"
|
||||
|
||||
# Customizes the charset used to build the accounts listing
|
||||
# table. Defaults to markdown table style.
|
||||
#
|
||||
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
|
||||
#
|
||||
account.list.table.preset = "|| |-||| "
|
||||
|
||||
# Customizes the color of the NAME column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.name-color = "green"
|
||||
|
||||
# Customizes the color of the BACKENDS column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.backends-color = "blue"
|
||||
|
||||
# Customizes the color of the DEFAULT column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.default-color = "black"
|
||||
|
||||
################################################################################
|
||||
###[ Account configuration ]####################################################
|
||||
################################################################################
|
||||
|
||||
# The account name should be unique.
|
||||
#
|
||||
# The account name.
|
||||
[accounts.example]
|
||||
|
||||
# Defaultness of the account. The current account will be used by
|
||||
# default in all commands.
|
||||
#
|
||||
# The current account will be used by default for all other commands.
|
||||
default = true
|
||||
|
||||
# The email address associated to the current account.
|
||||
#
|
||||
# The display-name and the email are used to build the full email
|
||||
# address: "My example account" <example@localhost>
|
||||
display-name = "My example account"
|
||||
email = "example@localhost"
|
||||
|
||||
# The display name of the account. This and the email are used to
|
||||
# build the full email address: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# The signature put at the bottom of composed messages. It can be a
|
||||
# path or a string. Supports TOML multilines.
|
||||
#
|
||||
#signature = "/path/to/signature/file"
|
||||
#signature = """
|
||||
# Thanks you,
|
||||
# Regards
|
||||
#"""
|
||||
signature = "Regards,\n"
|
||||
|
||||
# Signature delimiter. It delimits the end of the message body from
|
||||
# the signature.
|
||||
#
|
||||
# The signature can be a string or a path to a file.
|
||||
signature = "Regards,"
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# Downloads directory path. It is mostly used for downloading
|
||||
# attachments. Defaults to the system temporary directory.
|
||||
#
|
||||
downloads-dir = "~/downloads"
|
||||
# Enable the synchronization for this account. Running the command
|
||||
# `account sync example` will synchronize all folders and all emails
|
||||
# to a local Maildir at `$XDG_DATA_HOME/himalaya/example`.
|
||||
sync.enable = false
|
||||
|
||||
# Override the default Maildir path for synchronization.
|
||||
sync.dir = "/tmp/himalaya-sync-example"
|
||||
|
||||
# Filter folders to sync
|
||||
folder.sync.filter.include = ["INBOX"]
|
||||
# folder.sync.filter.exclude = ["All mails"]
|
||||
# folder.sync.filter = "all"
|
||||
|
||||
# Defines aliases for your mailboxes. There are 4 special aliases used
|
||||
# by the tool: inbox, sent, drafts and trash. Other aliases can be
|
||||
# defined as well.
|
||||
#
|
||||
folder.aliases.inbox = "INBOX"
|
||||
folder.aliases.sent = "Sent"
|
||||
folder.aliases.drafts = "Drafts"
|
||||
folder.aliases.trash = "Trash"
|
||||
folder.aliases.a23 = "Archives/2023"
|
||||
# Define main folder aliases
|
||||
folder.alias.inbox = "INBOX"
|
||||
folder.alias.sent = "Sent"
|
||||
folder.alias.drafts = "Drafts"
|
||||
folder.alias.trash = "Trash"
|
||||
|
||||
# Customizes the number of folders to show by page.
|
||||
#
|
||||
folder.list.page-size = 10
|
||||
# Also define custom folder aliases
|
||||
folder.alias.prev-year = "Archives/2023"
|
||||
|
||||
# Customizes the charset used to build the table. Defaults to markdown
|
||||
# table style.
|
||||
#
|
||||
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
|
||||
#
|
||||
folder.list.table.preset = "|| |-||| "
|
||||
# Default backend used for all the features like adding folders,
|
||||
# listing envelopes or copying messages.
|
||||
backend = "imap"
|
||||
|
||||
# Customizes the color of the NAME column of the folder listing table.
|
||||
#
|
||||
folder.list.table.name-color = "blue"
|
||||
|
||||
# Customizes the color of the DESC column of the folder listing table.
|
||||
#
|
||||
folder.list.table.desc-color = "green"
|
||||
|
||||
|
||||
|
||||
# Customizes the number of envelopes to show by page.
|
||||
#
|
||||
envelope.list.page-size = 10
|
||||
|
||||
# Customizes the format of the envelope date.
|
||||
#
|
||||
# See supported formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
|
||||
#
|
||||
envelope.list.datetime-fmt = "%F %R%:z"
|
||||
|
||||
# Transforms envelopes date timezone into the user's local one. For
|
||||
# example, if the user's local timezone is UTC, the envelope date
|
||||
# `2023-06-15T09:00:00+02:00` becomes `2023-06-15T07:00:00-00:00`.
|
||||
#
|
||||
# Date are converted to the user's local timezone.
|
||||
envelope.list.datetime-local-tz = true
|
||||
|
||||
# Customizes the charset used to build the table. Defaults to markdown
|
||||
# table style.
|
||||
#
|
||||
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
|
||||
#
|
||||
envelope.list.table.preset = "|| |-||| "
|
||||
# Override the backend used for listing envelopes.
|
||||
# envelope.list.backend = "imap"
|
||||
|
||||
# Customizes the character of the unseen flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.unseen-char = "*"
|
||||
# Send notification on receiving new envelopes
|
||||
envelope.watch.received.notify.summary = "📬 New message from {sender}"
|
||||
|
||||
# Customizes the character of the replied flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.replied-char = "R"
|
||||
# Available placeholders: id, subject, sender, sender.name,
|
||||
# sender.address, recipient, recipient.name, recipient.address.
|
||||
envelope.watch.received.notify.body = "{subject}"
|
||||
|
||||
# Customizes the character of the flagged flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.flagged-char = "!"
|
||||
# Shell commands can also be executed when envelopes change
|
||||
# envelope.watch.any.cmd = "mbsync -a"
|
||||
|
||||
# Customizes the character of the attachment property of the envelope
|
||||
# listing table.
|
||||
#
|
||||
envelope.list.table.attachment-char = "@"
|
||||
# Override the backend used for sending messages.
|
||||
message.send.backend = "smtp"
|
||||
|
||||
# Customizes the color of the ID column of the envelope listing table.
|
||||
#
|
||||
envelope.list.table.id-color = "red"
|
||||
# Save a copy of sent messages to the sent folder.
|
||||
message.send.save-copy = false
|
||||
|
||||
# Customizes the color of the FLAGS column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.flags-color = "black"
|
||||
# IMAP config
|
||||
imap.host = "localhost"
|
||||
imap.port = 3143
|
||||
imap.login = "example@localhost"
|
||||
|
||||
# Customizes the color of the SUBJECT column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.subject-color = "green"
|
||||
# Encryption can be either "tls" (or true), "start-tls" or "none" (or false).
|
||||
imap.encryption = "none"
|
||||
|
||||
# Customizes the color of the SENDER column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.sender-color = "blue"
|
||||
# Get password from a raw string (not safe)
|
||||
imap.passwd.raw = "password"
|
||||
|
||||
# Customizes the color of the DATE column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.date-color = "yellow"
|
||||
# Get password from a shell command
|
||||
# imap.passwd.cmd = "echo password"
|
||||
|
||||
# Get password from your global system keyring using secret service
|
||||
# Keyring secrets can be (re)set with the command `account configure example`
|
||||
# imap.passwd.keyring = "example-imap-password"
|
||||
|
||||
# Customize at which period, in seconds, the IMAP IDLE mode should refresh.
|
||||
# Defaults to 1740 (29 min), as defined in the RFC.
|
||||
# imap.watch.timeout = 25
|
||||
|
||||
# Defines headers to show at the top of messages when reading them.
|
||||
#
|
||||
message.read.headers = ["From", "To", "Cc", "Subject"]
|
||||
# SMTP config
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 3025
|
||||
smtp.login = "example@localhost"
|
||||
smtp.encryption = false
|
||||
smtp.passwd.raw = "password"
|
||||
|
||||
# Represents the message text/plain format as defined in the
|
||||
# RFC2646.
|
||||
#
|
||||
# See <https://www.ietf.org/rfc/rfc2646.txt>.
|
||||
#
|
||||
#message.read.format.fixed = 80
|
||||
#message.read.format = "flowed"
|
||||
message.read.format = "auto"
|
||||
|
||||
# Defines headers to show at the top of messages when writing them.
|
||||
#
|
||||
message.write.headers = ["From", "To", "In-Reply-To", "Cc", "Subject"]
|
||||
|
||||
# Saves a copy of sent messages to the sent folder. The sent folder is
|
||||
# taken from folder.aliases, defaults to Sent.
|
||||
#
|
||||
message.send.save-copy = true
|
||||
|
||||
# Hook called just before sending a message. The command should take a
|
||||
# raw message as standard input (stdin) and returns the modified raw
|
||||
# message to the standard output (stdout).
|
||||
#
|
||||
message.send.pre-hook = "process-markdown.sh"
|
||||
|
||||
# Customizes the message deletion style. Message deletion can be
|
||||
# performed either by moving messages to the Trash folder or by adding
|
||||
# the Deleted flag to their respective envelopes.
|
||||
#
|
||||
#message.delete.style = "flag"
|
||||
message.delete.style = "folder"
|
||||
|
||||
|
||||
|
||||
# Defines how and where the signature should be displayed when writing
|
||||
# a new message.
|
||||
#
|
||||
#template.new.signature-style = "hidden"
|
||||
#template.new.signature-style = "attached"
|
||||
template.new.signature-style = "inlined"
|
||||
|
||||
# Defines the posting style when replying to a message.
|
||||
#
|
||||
# See <https://en.wikipedia.org/wiki/Posting_style>.
|
||||
#
|
||||
#template.reply.posting-style = "interleaved"
|
||||
#template.reply.posting-style = "bottom"
|
||||
template.reply.posting-style = "top"
|
||||
|
||||
# Defines how and where the signature should be displayed when
|
||||
# repyling to a message.
|
||||
#
|
||||
#template.reply.signature-style = "hidden"
|
||||
#template.reply.signature-style = "attached"
|
||||
#template.reply.signature-style = "above-quote"
|
||||
template.reply.signature-style = "below-quote"
|
||||
|
||||
# Defines the headline format put at the top of a quote when replying
|
||||
# to a message.
|
||||
#
|
||||
# Available placeholders: {senders}
|
||||
# See supported date formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
|
||||
#
|
||||
template.reply.quote-headline-fmt = "On %d/%m/%Y %H:%M, {senders} wrote:\n"
|
||||
|
||||
# Defines the posting style when forwarding a message.
|
||||
#
|
||||
# See <https://en.wikipedia.org/wiki/Posting_style>.
|
||||
#
|
||||
#template.forward.posting-style = "attached"
|
||||
template.forward.posting-style = "top"
|
||||
|
||||
# Defines how and where the signature should be displayed when
|
||||
# forwarding a message.
|
||||
#
|
||||
#template.forward.signature-style = "hidden"
|
||||
#template.forward.signature-style = "attached"
|
||||
template.forward.signature-style = "inlined"
|
||||
|
||||
# Defines the headline format put at the top of the quote when
|
||||
# forwarding a message.
|
||||
#
|
||||
template.forward.quote-headline = "-------- Forwarded Message --------\n"
|
||||
|
||||
|
||||
|
||||
# Enables PGP using GPG bindings. It requires the GPG lib to be
|
||||
# installed on the system, and the `pgp-gpg` cargo feature on.
|
||||
#
|
||||
#pgp.type = "gpg"
|
||||
|
||||
|
||||
|
||||
# Enables PGP using shell commands. A PGP client needs to be installed
|
||||
# on the system, like gpg. It also requires the `pgp-commands` cargo
|
||||
# feature.
|
||||
#
|
||||
#pgp.type = "commands"
|
||||
|
||||
# Defines the encrypt command. The special placeholder `<recipients>`
|
||||
# represents the list of recipients, formatted by
|
||||
# `pgp.encrypt-recipient-fmt`.
|
||||
#
|
||||
#pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
|
||||
|
||||
# Formats recipients for `pgp.encrypt-cmd`. The special placeholder
|
||||
# `<recipient>` is replaced by an actual recipient at runtime.
|
||||
#
|
||||
#pgp.encrypt-recipient-fmt = "--recipient <recipient>"
|
||||
|
||||
# Defines the separator used between formatted recipients
|
||||
# `pgp.encrypt-recipient-fmt`.
|
||||
#
|
||||
#pgp.encrypt-recipients-sep = " "
|
||||
|
||||
# Defines the decrypt command.
|
||||
#
|
||||
#pgp.decrypt-cmd = "gpg --decrypt --quiet"
|
||||
|
||||
# Defines the sign command.
|
||||
#
|
||||
#pgp.sign-cmd = "gpg --sign --quiet --armor"
|
||||
|
||||
# Defines the verify command.
|
||||
#
|
||||
#pgp.verify-cmd = "gpg --verify --quiet"
|
||||
|
||||
|
||||
|
||||
# Enables the native Rust implementation of PGP. It requires the
|
||||
# `pgp-native` cargo feature.
|
||||
#
|
||||
#pgp.type = "native"
|
||||
|
||||
# Defines where to find the PGP secret key.
|
||||
#
|
||||
#pgp.secret-key.path = "/path/to/secret.key"
|
||||
#pgp.secret-key.keyring = "my-pgp-secret-key"
|
||||
|
||||
# Defines how to retrieve the PGP secret key passphrase.
|
||||
#
|
||||
#pgp.secret-key-passphrase.raw = "p@assw0rd"
|
||||
#pgp.secret-key-passphrase.keyring = "my-pgp-passphrase"
|
||||
#pgp.secret-key-passphrase.cmd = "pass show pgp-passphrase"
|
||||
|
||||
# Enables the Web Key Discovery protocol to discover recipients'
|
||||
# public key based on their email address.
|
||||
#
|
||||
#pgp.wkd = true
|
||||
|
||||
# Enables public key servers discovery.
|
||||
#
|
||||
#pgp.key-servers = ["hkps://keys.openpgp.org", "hkps://keys.mailvelope.com"]
|
||||
|
||||
|
||||
|
||||
# Defines the IMAP backend as the default one for all features.
|
||||
#
|
||||
backend.type = "imap"
|
||||
|
||||
# IMAP server host name.
|
||||
#
|
||||
backend.host = "localhost"
|
||||
|
||||
# IMAP server port.
|
||||
#
|
||||
#backend.port = 143
|
||||
backend.port = 993
|
||||
|
||||
# IMAP server encryption.
|
||||
#
|
||||
#backend.encryption.type = "none"
|
||||
#backend.encryption.type = "start-tls"
|
||||
backend.encryption.type = "tls"
|
||||
|
||||
# IMAP server login.
|
||||
#
|
||||
backend.login = "example@localhost"
|
||||
|
||||
# IMAP server password authentication configuration.
|
||||
#
|
||||
backend.auth.type = "password"
|
||||
#
|
||||
# Password can be inlined (not recommended).
|
||||
#
|
||||
#backend.auth.raw = "p@assw0rd"
|
||||
#
|
||||
# Password can be stored inside your system global keyring (requires
|
||||
# the keyring cargo feature). You must run at least once `himalaya
|
||||
# account configure` to set up the password.
|
||||
#
|
||||
#backend.auth.keyring = "example-imap"
|
||||
#
|
||||
# Password can be retrieved from a shell command.
|
||||
#
|
||||
backend.auth.cmd = "pass show example-imap"
|
||||
|
||||
# IMAP server OAuth 2.0 authorization configuration.
|
||||
#
|
||||
#backend.auth.type = "oauth2"
|
||||
#
|
||||
# Client identifier issued to the client during the registration
|
||||
# process described in RFC6749.
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#backend.auth.client-id = "client-id"
|
||||
#
|
||||
# Client password issued to the client during the registration process
|
||||
# described in RFC6749.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-imap-client-secret".
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#backend.auth.client-secret.raw = "<raw-client-secret>"
|
||||
#backend.auth.client-secret.keyring = "example-imap-client-secret"
|
||||
#backend.auth.client-secret.cmd = "pass show example-imap-client-secret"
|
||||
#
|
||||
# Method for presenting an OAuth 2.0 bearer token to a service for
|
||||
# authentication.
|
||||
#
|
||||
#backend.auth.method = "oauthbearer"
|
||||
#backend.auth.method = "xoauth2"
|
||||
#
|
||||
# URL of the authorization server's authorization endpoint.
|
||||
#
|
||||
#backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
#
|
||||
# URL of the authorization server's token endpoint.
|
||||
#
|
||||
#backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
#
|
||||
# Access token returned by the token endpoint and used to access
|
||||
# protected resources. It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-imap-access-token".
|
||||
#
|
||||
#backend.auth.access-token.raw = "<raw-access-token>"
|
||||
#backend.auth.access-token.keyring = "example-imap-access-token"
|
||||
#backend.auth.access-token.cmd = "pass show example-imap-access-token"
|
||||
#
|
||||
# Refresh token used to obtain a new access token (if supported by the
|
||||
# authorization server). It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-imap-refresh-token".
|
||||
#
|
||||
#backend.auth.refresh-token.raw = "<raw-refresh-token>"
|
||||
#backend.auth.refresh-token.keyring = "example-imap-refresh-token"
|
||||
#backend.auth.refresh-token.cmd = "pass show example-imap-refresh-token"
|
||||
#
|
||||
# Enable the protection, as defined in RFC7636.
|
||||
#
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc7636>.
|
||||
#
|
||||
#backend.auth.pkce = true
|
||||
#
|
||||
# Access token scope(s), as defined by the authorization server.
|
||||
#
|
||||
#backend.auth.scope = "unique scope"
|
||||
#backend.auth.scopes = ["multiple", "scopes"]
|
||||
#
|
||||
# URL scheme of the redirect server.
|
||||
# Defaults to http.
|
||||
#
|
||||
#backend.auth.redirect-scheme = "http"
|
||||
#
|
||||
# Host name of the redirect server.
|
||||
# Defaults to localhost.
|
||||
#
|
||||
#backend.auth.redirect-host = "localhost"
|
||||
#
|
||||
# Port of the redirect server.
|
||||
# Defaults to the first available one.
|
||||
#
|
||||
#backend.auth.redirect-port = 9999
|
||||
|
||||
|
||||
|
||||
# Defines the Maildir backend as the default one for all features.
|
||||
#
|
||||
#backend.type = "maildir"
|
||||
|
||||
# The Maildir root directory. The path should point to the root level
|
||||
# of the Maildir directory.
|
||||
#
|
||||
#backend.root-dir = "~/.Mail/example"
|
||||
|
||||
# Does the Maildir folder follows the Maildir++ standard?
|
||||
#
|
||||
# See <https://en.wikipedia.org/wiki/Maildir#Maildir++>.
|
||||
#
|
||||
#backend.maildirpp = false
|
||||
|
||||
|
||||
|
||||
# Defines the Notmuch backend as the default one for all features.
|
||||
#
|
||||
#backend.type = "notmuch"
|
||||
|
||||
# The path to the Notmuch database. The path should point to the root
|
||||
# directory containing the Notmuch database (usually the root Maildir
|
||||
# directory).
|
||||
#
|
||||
#backend.db-path = "~/.Mail/example"
|
||||
|
||||
# Overrides the default path to the Maildir folder.
|
||||
#
|
||||
#backend.maildir-path = "~/.Mail/example"
|
||||
|
||||
# Overrides the default Notmuch configuration file path.
|
||||
#
|
||||
#backend.config-path = "~/.notmuchrc"
|
||||
|
||||
# Override the default Notmuch profile name.
|
||||
#
|
||||
#backend.profile = "example"
|
||||
|
||||
|
||||
|
||||
# Defines the SMTP backend for the message sending feature.
|
||||
#
|
||||
message.send.backend.type = "smtp"
|
||||
|
||||
# SMTP server host name.
|
||||
#
|
||||
message.send.backend.host = "localhost"
|
||||
|
||||
# SMTP server port.
|
||||
#
|
||||
#message.send.backend.port = 25
|
||||
#message.send.backend.port = 465
|
||||
message.send.backend.port = 587
|
||||
|
||||
# SMTP server encryption.
|
||||
#
|
||||
#message.send.backend.encryption.type = "none"
|
||||
#message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.encryption.type = "tls"
|
||||
|
||||
# SMTP server login.
|
||||
#
|
||||
message.send.backend.login = "example@localhost"
|
||||
|
||||
# SMTP server password authentication configuration.
|
||||
#
|
||||
message.send.backend.auth.type = "password"
|
||||
#
|
||||
# Password can be inlined (not recommended).
|
||||
#
|
||||
#message.send.backend.auth.raw = "p@assw0rd"
|
||||
#
|
||||
# Password can be stored inside your system global keyring (requires
|
||||
# the keyring cargo feature). You must run at least once `himalaya
|
||||
# account configure` to set up the password.
|
||||
#
|
||||
#message.send.backend.auth.keyring = "example-smtp"
|
||||
#
|
||||
# Password can be retrieved from a shell command.
|
||||
#
|
||||
message.send.backend.auth.cmd = "pass show example-smtp"
|
||||
|
||||
# SMTP server OAuth 2.0 authorization configuration.
|
||||
#
|
||||
#message.send.backend.auth.type = "oauth2"
|
||||
#
|
||||
# Client identifier issued to the client during the registration
|
||||
# process described in RFC6749.
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#message.send.backend.auth.client-id = "client-id"
|
||||
#
|
||||
# Client password issued to the client during the registration process
|
||||
# described in RFC6749.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-smtp-client-secret".
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#message.send.backend.auth.client-secret.raw = "<raw-client-secret>"
|
||||
#message.send.backend.auth.client-secret.keyring = "example-smtp-client-secret"
|
||||
#message.send.backend.auth.client-secret.cmd = "pass show example-smtp-client-secret"
|
||||
#
|
||||
# Method for presenting an OAuth 2.0 bearer token to a service for
|
||||
# authentication.
|
||||
#
|
||||
#message.send.backend.auth.method = "oauthbearer"
|
||||
#message.send.backend.auth.method = "xoauth2"
|
||||
#
|
||||
# URL of the authorization server's authorization endpoint.
|
||||
#
|
||||
#message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
#
|
||||
# URL of the authorization server's token endpoint.
|
||||
#
|
||||
#message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
#
|
||||
# Access token returned by the token endpoint and used to access
|
||||
# protected resources. It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-smtp-access-token".
|
||||
#
|
||||
#message.send.backend.auth.access-token.raw = "<raw-access-token>"
|
||||
#message.send.backend.auth.access-token.keyring = "example-smtp-access-token"
|
||||
#message.send.backend.auth.access-token.cmd = "pass show example-smtp-access-token"
|
||||
#
|
||||
# Refresh token used to obtain a new access token (if supported by the
|
||||
# authorization server). It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-smtp-refresh-token".
|
||||
#
|
||||
#message.send.backend.auth.refresh-token.raw = "<raw-refresh-token>"
|
||||
#message.send.backend.auth.refresh-token.keyring = "example-smtp-refresh-token"
|
||||
#message.send.backend.auth.refresh-token.cmd = "pass show example-smtp-refresh-token"
|
||||
#
|
||||
# Enable the protection, as defined in RFC7636.
|
||||
#
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc7636>.
|
||||
#
|
||||
#message.send.backend.auth.pkce = true
|
||||
#
|
||||
# Access token scope(s), as defined by the authorization server.
|
||||
#
|
||||
#message.send.backend.auth.scope = "unique scope"
|
||||
#message.send.backend.auth.scopes = ["multiple", "scopes"]
|
||||
#
|
||||
# URL scheme of the redirect server.
|
||||
# Defaults to http.
|
||||
#
|
||||
#message.send.backend.auth.redirect-scheme = "http"
|
||||
#
|
||||
# Host name of the redirect server.
|
||||
# Defaults to localhost.
|
||||
#
|
||||
#message.send.backend.auth.redirect-host = "localhost"
|
||||
#
|
||||
# Port of the redirect server.
|
||||
# Defaults to the first available one.
|
||||
#
|
||||
#message.send.backend.auth.redirect-port = 9999
|
||||
|
||||
|
||||
|
||||
# Defines the Sendmail backend for the message sending feature.
|
||||
#
|
||||
#message.send.backend.type = "sendmail"
|
||||
|
||||
# Customizes the sendmail shell command.
|
||||
#
|
||||
#message.send.backend.cmd = "/usr/bin/sendmail"
|
||||
# PGP needs to be enabled with one of those cargo feature:
|
||||
# pgp-commands, pgp-gpg or pgp-native
|
||||
# pgp.backend = "gpg"
|
||||
|
|
41
default.nix
41
default.nix
|
@ -1,29 +1,12 @@
|
|||
{
|
||||
pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"),
|
||||
...
|
||||
}@args:
|
||||
|
||||
pimalaya.mkDefault (
|
||||
{
|
||||
src = ./.;
|
||||
version = "1.0.0";
|
||||
mkPackage = (
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
rustPlatform,
|
||||
defaultFeatures,
|
||||
features,
|
||||
}:
|
||||
pkgs.callPackage ./package.nix {
|
||||
inherit lib rustPlatform;
|
||||
apple-sdk = pkgs.apple-sdk;
|
||||
installShellCompletions = false;
|
||||
installManPages = false;
|
||||
buildNoDefaultFeatures = !defaultFeatures;
|
||||
buildFeatures = lib.splitString "," features;
|
||||
}
|
||||
);
|
||||
}
|
||||
// removeAttrs args [ "pimalaya" ]
|
||||
)
|
||||
# This file exists for legacy Nix installs (nix-build & nix-env)
|
||||
# https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix
|
||||
# You generally do *not* have to modify this ever.
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).defaultNix
|
||||
|
|
136
flake.lock
generated
136
flake.lock
generated
|
@ -8,66 +8,127 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1732405626,
|
||||
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
|
||||
"owner": "soywod",
|
||||
"lastModified": 1708842026,
|
||||
"narHash": "sha256-1r+7l66CFPCjRDZEKq6r7aCpfjqhIkrF3ZTQyEFnkTs=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
|
||||
"rev": "b7fff64b5bc36b1662f8317632b11b15b3839b2a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "soywod",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703887061,
|
||||
"narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698420672,
|
||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1736437047,
|
||||
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
|
||||
"lastModified": 1708702655,
|
||||
"narHash": "sha256-qxT5jSLhelfLhQ07+AUxSTm1VnVH+hQxDkQSZ/m/Smo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f17b95775191ea44bc426831235d87affb10faba",
|
||||
"rev": "c5101e457206dd437330d283d6626944e28794b3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "staging-next",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pimalaya": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1737984647,
|
||||
"narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=",
|
||||
"owner": "pimalaya",
|
||||
"repo": "nix",
|
||||
"rev": "712a481632f4929d24a34cb5762e0ffdc901bd99",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "pimalaya",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pimalaya": "pimalaya"
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1732050317,
|
||||
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
|
||||
"lastModified": 1708768292,
|
||||
"narHash": "sha256-IBHMNEe3lspVdIzjpM2OVZiBFmFw1DKtdgVN5G41pRc=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
|
||||
"rev": "4a8d0f7f565b6df45da5522dd7366a4df3460cd7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -76,6 +137,21 @@
|
|||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
155
flake.nix
155
flake.nix
|
@ -2,25 +2,150 @@
|
|||
description = "CLI to manage emails";
|
||||
|
||||
inputs = {
|
||||
# FIXME: when #358989 lands on nixos-unstable
|
||||
# https://nixpk.gs/pr-tracker.html?pr=358989
|
||||
nixpkgs.url = "github:nixos/nixpkgs/staging-next";
|
||||
fenix = {
|
||||
# TODO: https://github.com/nix-community/fenix/pull/145
|
||||
# url = "github:nix-community/fenix";
|
||||
url = "github:soywod/fenix";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
gitignore = {
|
||||
url = "github:hercules-ci/gitignore.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
pimalaya = {
|
||||
url = "github:pimalaya/nix";
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk = {
|
||||
url = "github:nix-community/naersk";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
(import inputs.pimalaya).mkFlakeOutputs inputs {
|
||||
shell = ./shell.nix;
|
||||
default = ./default.nix;
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, gitignore, fenix, naersk, ... }:
|
||||
let
|
||||
inherit (gitignore.lib) gitignoreSource;
|
||||
|
||||
mkToolchain = import ./rust-toolchain.nix fenix;
|
||||
|
||||
mkDevShells = buildPlatform:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
rust-toolchain = mkToolchain.fromFile { system = buildPlatform; };
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
# Nix
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
|
||||
# Rust
|
||||
rust-toolchain
|
||||
cargo-watch
|
||||
|
||||
# OpenSSL
|
||||
openssl.dev
|
||||
|
||||
# Notmuch
|
||||
notmuch
|
||||
|
||||
# GPG
|
||||
gnupg
|
||||
gpgme
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
mkPackage = pkgs: buildPlatform: targetPlatform: package:
|
||||
let
|
||||
toolchain = mkToolchain.fromTarget {
|
||||
inherit pkgs 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/
|
||||
'';
|
||||
};
|
||||
doCheck = true;
|
||||
cargoTestOptions = opts: opts ++ [ "--lib" ];
|
||||
} // pkgs.lib.optionalAttrs (!isNull targetPlatform) {
|
||||
CARGO_BUILD_TARGET = targetPlatform;
|
||||
} // package;
|
||||
in
|
||||
naersk'.buildPackage package';
|
||||
|
||||
mkPackages = buildPlatform:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
mkPackage' = mkPackage pkgs buildPlatform;
|
||||
in
|
||||
rec {
|
||||
default = if pkgs.stdenv.isDarwin then macos else linux;
|
||||
linux = mkPackage' null { };
|
||||
linux-musl = mkPackage' "x86_64-unknown-linux-musl" (with pkgs.pkgsStatic; {
|
||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||
hardeningDisable = [ "all" ];
|
||||
});
|
||||
macos = mkPackage' null (with pkgs.darwin.apple_sdk.frameworks; {
|
||||
# NOTE: needed to prevent error Undefined symbols
|
||||
# "_OBJC_CLASS_$_NSImage" and
|
||||
# "_LSCopyApplicationURLsForBundleIdentifier"
|
||||
NIX_LDFLAGS = "-F${AppKit}/Library/Frameworks -framework AppKit";
|
||||
buildInputs = [ Cocoa ];
|
||||
});
|
||||
# FIXME: bzlip: fatal error: windows.h: No such file or directory
|
||||
# May be related to SQLite.
|
||||
windows = mkPackage' "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:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
in
|
||||
rec {
|
||||
default = if pkgs.stdenv.isDarwin then macos else linux;
|
||||
linux = mkApp self.packages.${buildPlatform}.linux;
|
||||
linux-musl = mkApp self.packages.${buildPlatform}.linux-musl;
|
||||
macos = mkApp self.packages.${buildPlatform}.macos;
|
||||
windows =
|
||||
let
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
40
install.sh
40
install.sh
|
@ -9,47 +9,25 @@ die() {
|
|||
|
||||
DESTDIR="${DESTDIR:-}"
|
||||
PREFIX="${PREFIX:-"$DESTDIR/usr/local"}"
|
||||
RELEASES_URL="https://github.com/pimalaya/himalaya/releases"
|
||||
RELEASES_URL="https://github.com/soywod/himalaya/releases"
|
||||
|
||||
binary=himalaya
|
||||
system=$(uname -s | tr [:upper:] [:lower:])
|
||||
machine=$(uname -m | tr [:upper:] [:lower:])
|
||||
|
||||
case $system in
|
||||
msys*|mingw*|cygwin*|win*)
|
||||
target=x86_64-windows
|
||||
binary=himalaya.exe;;
|
||||
|
||||
linux|freebsd)
|
||||
case $machine in
|
||||
x86_64) target=x86_64-linux;;
|
||||
x86|i386|i686) target=i686-linux;;
|
||||
arm64|aarch64) target=aarch64-linux;;
|
||||
armv6l) target=armv6l-linux;;
|
||||
armv7l) target=armv7l-linux;;
|
||||
*) die "Unsupported machine $machine for system $system";;
|
||||
esac;;
|
||||
|
||||
darwin)
|
||||
case $machine in
|
||||
x86_64) target=x86_64-darwin;;
|
||||
arm64|aarch64) target=aarch64-darwin;;
|
||||
*) die "Unsupported machine $machine for system $system";;
|
||||
esac;;
|
||||
|
||||
*)
|
||||
die "Unsupported system $system";;
|
||||
msys*|mingw*|cygwin*|win*) system=windows; binary=himalaya.exe;;
|
||||
linux|freebsd) system=linux; binary=himalaya;;
|
||||
darwin) system=macos; binary=himalaya;;
|
||||
*) die "Unsupported system: $system" ;;
|
||||
esac
|
||||
|
||||
tmpdir=$(mktemp -d) || die "Cannot create temporary directory"
|
||||
tmpdir=$(mktemp -d) || die "Failed to create tmpdir"
|
||||
trap "rm -rf $tmpdir" EXIT
|
||||
|
||||
echo "Downloading latest $system release…"
|
||||
curl -sLo "$tmpdir/himalaya.tgz" \
|
||||
"$RELEASES_URL/latest/download/himalaya.$target.tgz"
|
||||
curl -sLo "$tmpdir/himalaya.tar.gz" \
|
||||
"$RELEASES_URL/latest/download/himalaya-$system.tar.gz"
|
||||
|
||||
echo "Installing binary…"
|
||||
tar -xzf "$tmpdir/himalaya.tgz" -C "$tmpdir"
|
||||
tar -xzf "$tmpdir/himalaya.tar.gz" -C "$tmpdir"
|
||||
|
||||
mkdir -p "$PREFIX/bin"
|
||||
cp -f -- "$tmpdir/$binary" "$PREFIX/bin/$binary"
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 340.2 340.2" style="enable-background:new 0 0 340.2 340.2" xml:space="preserve">
|
||||
<style>
|
||||
.st1{fill:#f5e6ca}
|
||||
</style>
|
||||
<path d="m322.5 120.7-2.3-2h-.1L191 7.5c-5.6-4.8-12.6-7.3-19.7-7h-1.6c-7.2-.2-14.2 2.3-19.8 7L18.7 120.6C11.9 126.5 8 135.1 8 144.3v36.3c-.1.8-.1 1.5-.1 2.2v119.7c0 .9 0 1.9.1 2.9v15.3c0 5.8 1.7 10.4 4.9 13.6 4.3 4.2 10 4.9 15.9 4.9 1.4 0 2.8 0 4.2-.1 1.5 0 3-.1 4.6-.1h265.7c1.6 0 3.1 0 4.5.1 7.3.2 14.9.4 20.3-4.8 3.3-3.2 4.9-7.7 4.9-13.5V144.3c.1-9.1-3.8-17.7-10.5-23.6z" style="fill:#444" id="Calque_2"/>
|
||||
<g id="Calque_1">
|
||||
<path class="st1" d="M317.1 126.7 185.8 13.6c-4.2-3.6-9.3-5.3-14.4-5.1h-1.9c-5.1-.2-10.2 1.5-14.4 5.1L23.9 126.7c-5 4.3-7.9 10.8-7.9 17.6v176.4c0 12.6 9.7 10.3 21.7 10.3h265.7c12 0 21.7 2.2 21.7-10.3V144.3c0-6.8-2.9-13.2-7.9-17.6h-.1z"/>
|
||||
<radialGradient id="SVGID_1_" cx="176.718" cy="89.04" r="180.6" gradientTransform="matrix(.9999 .0157 .011 -.6999 -4.55 211.672)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#f7bd6c"/>
|
||||
<stop offset=".5" style="stop-color:#db8355"/>
|
||||
<stop offset=".8" style="stop-color:#29445d"/>
|
||||
<stop offset="1" style="stop-color:#143651"/>
|
||||
</radialGradient>
|
||||
<path d="M309.7 134.2c8.4 6.8 8.4 51.2 0 57.9l-111.5 58.2-27.4-22.1-27.4 22.1-100.1-51.5-11.4-6.7c-8.4-6.8-9.6-50 0-57.9L155.8 27.5c8.8-7.1 21.3-7.1 30.1 0l123.8 106.7z" style="fill:url(#SVGID_1_)"/>
|
||||
<path d="m197.7 250.4 27 78h72.7c12.6 0 27.6-5.4 27.6-25.9V182.8c0-14.2-16.5-22.1-27.6-13.1l-99.7 80.7zm-54.5 0-27 78H43.5c-12.6 0-27.6-5.4-27.6-25.9V182.8c0-14.2 16.5-22.1 27.6-13.1l99.7 80.7z" style="fill:#fcedd0"/>
|
||||
<path d="M116.7 328.1H23.1c-10.9 0-1.8-7.6-.2-8.7L134 243.2l8.9 7.2-26.3 77.7h.1zm107.3 0h93.5c10.9 0 1.8-7.6.2-8.7l-111.1-76.2-8.9 7.2 26.3 77.7z" style="fill:#7c6d5d"/>
|
||||
<path class="st1" d="M317.4 322.1c-6.5-4.3-140.1-89.8-140.1-89.8-2.1-1.3-4.4-2-6.7-2s-4.7.7-6.7 2c0 0-133.6 85.5-140.1 89.8-5.3 3.5-4.8 6.1 0 6.1h294.1c4.7 0 5.2-2.7 0-6.1h-.5z"/>
|
||||
<circle cx="170.9" cy="154.4" r="47.8" style="fill:#fff"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.1 KiB |
81
logo.svg
81
logo.svg
|
@ -1,81 +0,0 @@
|
|||
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 680.3 680.3">
|
||||
<defs>
|
||||
<radialGradient id="Dégradé_sans_nom_28" data-name="Dégradé sans nom 28" cx="345.9" cy="318.2" fx="345.9" fy="318.2" r="377.3" gradientTransform="rotate(.9 -9637.325 190.29) scale(1 .5)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f7bd6c"/>
|
||||
<stop offset=".5" stop-color="#db8355"/>
|
||||
<stop offset=".8" stop-color="#29445d"/>
|
||||
<stop offset="1" stop-color="#143651"/>
|
||||
</radialGradient>
|
||||
<style>
|
||||
.cls-2,.cls-4,.cls-5,.cls-6{stroke:#fff}.cls-8{fill:#1a374a}.cls-6{stroke-miterlimit:10}.cls-2{fill:#fed894}.cls-2,.cls-4,.cls-5{stroke-width:2.3px;stroke-linecap:round;stroke-linejoin:round}.cls-10,.cls-11,.cls-12,.cls-15,.cls-16,.cls-18,.cls-8{stroke-width:0}.cls-10{fill:#fffcf9}.cls-11{fill:#fcedd0}.cls-12{fill:#233a7b}.cls-4{fill:#fff7ea}.cls-5{fill:#fdcc7c}.cls-16{opacity:.2}.cls-15{fill:#fff}.cls-16{fill:#0b5272}.cls-6{fill:#ffedd2;stroke-width:2.8px}.cls-18{fill:#e7d6be}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-11" d="M646.1 249.5 371.8 13.2c-8.7-7.5-19.4-11-30-10.7h-3.9c-10.6-.3-21.3 3.2-30 10.7L33.5 249.5C23 258.5 17 272 17 286.2v368.5c0 26.2 20.3 21.6 45.3 21.6h555.2c25 0 45.3 4.6 45.3-21.6V286.2c0-14.2-6.1-27.7-16.5-36.7Z"/>
|
||||
<path d="M630.8 265.1c17.5 14.1 17.6 106.9 0 121L397.9 507.7l-57.2-46.1-57.2 46.1L74.4 400.2l-23.8-14.1C33 372 30.5 281.6 50.6 265L309.2 42.1c18.3-14.8 44.6-14.8 63 0l258.6 223Z" style="stroke-width:0;fill:url(#Dégradé_sans_nom_28)"/>
|
||||
<circle class="cls-15" cx="342.8" cy="320.6" r="54.4"/>
|
||||
<g style="opacity:.7">
|
||||
<path class="cls-15" d="M274.5 333c-13.3 4.8-34.5 7.2-48.5 9.9-8 1.4-28.8 5-36.5 6.3l-12.4.7 11.9-3.5c7.7-1.3 28.6-5 36.5-6.3 14.1-2.2 34.8-7 49-7Zm136.5.4c14.1 0 34.9 5.1 48.9 7.3 8 1.4 28.8 5.2 36.5 6.6l11.9 3.6-12.4-.8c-7.7-1.4-28.6-5.1-36.5-6.6-14-2.8-35.1-5.3-48.4-10.2Zm-206.3-12.6c2.8-1.2 6.5-1.7 9.6-1.4h19.2c3.1-.2 6.7.3 9.6 1.5-2.9 1.2-6.5 1.7-9.6 1.4h-19.2c-3 .2-6.8-.2-9.6-1.5Zm161.7-65.2c2.4-13.9 10.9-33.5 15.4-47 2.8-7.7 9.9-27.5 12.6-34.9l5.6-11.1-2.8 12.1c-2.6 7.3-9.9 27.3-12.6 34.9-5.1 13.3-11.1 33.7-18.2 46Zm-23.8-4.1c-2.5-13.9-1.2-35.2-1.6-49.5v-37.1l1.4-12.4 1.5 12.4V202c-.3 14.3 1.1 35.5-1.3 49.5Zm0-129.3c-1.3-2.7-1.7-6-1.5-9v-18c-.3-3 .1-6.2 1.3-9 1.3 2.8 1.7 5.9 1.5 9v18c.3 2.9 0 6.3-1.3 9Zm-23.7 133.5c-7.1-12.2-13.2-32.7-18.4-45.9-2.8-7.6-10.1-27.4-12.8-34.8l-2.9-12.1 5.6 11.1c2.7 7.3 10 27.2 12.8 34.8 4.6 13.5 13.1 33 15.7 46.9Zm-20.8 12.1c-10.8-9.1-23.6-26.2-33-36.9-5.2-6.2-18.8-22.3-23.9-28.4l-6.9-10.4 9.1 8.5c5 6 18.7 22.2 23.9 28.4 9 11.1 23.6 26.5 30.8 38.7Zm-15.4 18.5c-13.3-4.8-31.1-16.5-43.6-23.4-7.1-4.1-25.3-14.5-32.2-18.5l-10-7.4 11.4 4.9c6.8 3.9 25.2 14.4 32.2 18.5 12.2 7.4 31.3 16.8 42.2 25.9Zm-8.2 22.6c-14.1 0-34.9-4.9-49-7-8-1.4-28.8-5-36.5-6.3l-11.9-3.5 12.4.7c7.7 1.3 28.6 5 36.6 6.3 14 2.7 35.1 5.1 48.5 9.9Zm136.5-.4c13.3-4.9 34.5-7.4 48.4-10.2 8-1.4 28.7-5.2 36.5-6.6l12.4-.8-11.9 3.6c-7.7 1.4-28.6 5.1-36.5 6.6-14.1 2.2-34.7 7.3-48.9 7.3Zm-8.3-22.6c10.8-9.1 29.9-18.7 42-26.1 7-4.1 25.2-14.7 32.1-18.7l11.4-5-10 7.5c-6.7 3.9-25.1 14.6-32.1 18.7-12.5 6.9-30.2 18.7-43.5 23.6Zm-15.5-18.4c3.6-8.1 13-17.5 18.5-24.5 5.9-6.6 13.4-17.6 20.7-22.6-3.6 8.1-13 17.5-18.5 24.5-5.9 6.6-13.4 17.6-20.7 22.6Zm-20.7-82.8c.8 3 .5 6.7-.2 9.7l-1.6 9.4-1.6 9.4c-.3 3.1-1.3 6.5-3.1 9.2-.7-3.1-.5-6.6.2-9.7l1.6-9.4 1.6-9.4c.3-3 1.4-6.6 3.1-9.2Zm2.7-16c-.5-.7-.8-1.3-1.1-2 .2-1.1.7-4 .9-5.1.2-1.1.7-4 .9-5.1.6-.5 1-1 1.7-1.5.5.7.7 1.3 1.1 2-.2 1.1-.7 4-.9 5.1-.2 1.1-.7 4-.9 5.1-.6.5-1 1-1.7 1.5Zm-50.7 16.1c1.7 2.6 2.8 6.1 3.1 9.2l1.7 9.4 1.7 9.4c.8 3.1 1 6.6.3 9.7-1.7-2.6-2.8-6.1-3.1-9.2l-1.7-9.4-1.7-9.4c-.8-3-1-6.7-.3-9.7Zm-4.4-24.4c-1.7-2.1-2.4-4.1-2.7-6.7l-1.2-7-1.2-7c-.7-2.6-.7-4.6.2-7.2 1.7 2.1 2.4 4 2.7 6.7l1.2 7 1.2 7c.6 2.5.7 4.7-.2 7.2Zm-40.7 40.9c2.5 1.8 4.7 4.8 6.1 7.6l4.8 8.3 4.8 8.3c1.8 2.6 3.2 5.9 3.6 9-2.5-1.9-4.7-4.7-6.1-7.6l-4.8-8.3-4.8-8.3c-1.7-2.5-3.2-5.9-3.6-9ZM261 179.9c-2.3-1.4-3.7-3-4.8-5.4l-3.6-6.1-3.6-6.1c-1.5-2.2-2.2-4.1-2.3-6.8 2.3 1.4 3.6 3 4.8 5.4l3.6 6.1 3.6 6.1c1.5 2.2 2.3 4.2 2.3 6.8Zm-24.2 52.4c3 .8 6.1 2.9 8.3 5l7.4 6.1 7.4 6.1c2.6 1.8 5 4.4 6.4 7.3-3-.9-6-2.8-8.3-5l-7.4-6.1-7.4-6.1c-2.5-1.8-5.1-4.5-6.4-7.3Zm-19.1-15.9c-2.6-.5-4.5-1.6-6.4-3.4l-5.4-4.5-5.4-4.5c-2.2-1.6-3.5-3.1-4.5-5.6 2.7.6 4.4 1.5 6.4 3.4l5.4 4.5 5.4 4.5c2.1 1.5 3.6 3.2 4.5 5.6Zm-4.8 57.5c3.1-.2 6.7.6 9.5 1.9l9 3.3 9 3.3c3 .9 6.2 2.5 8.5 4.6-3.2.2-6.6-.6-9.5-1.9l-9-3.3-9-3.3c-2.9-.8-6.3-2.5-8.5-4.6Zm-23.4-8.4c-2.6.4-4.8 0-7.1-1l-6.7-2.4-6.7-2.4c-2.6-.7-4.3-1.7-6.2-3.8 2.7-.4 4.7 0 7.1 1l6.7 2.4 6.7 2.4c2.5.7 4.4 1.7 6.2 3.8Z"/>
|
||||
<path class="cls-15" d="M204.7 321.2c2.8-1.3 6.5-1.7 9.6-1.5h19.2c3.1-.3 6.7.2 9.6 1.4-2.9 1.2-6.4 1.7-9.6 1.5h-19.2c-3 .3-6.8-.2-9.6-1.4Zm267.7-48.1c-2.2 2.2-5.5 3.8-8.5 4.7l-9 3.3-9 3.3c-2.9 1.3-6.3 2.1-9.5 1.9 2.3-2.2 5.5-3.8 8.5-4.7l9-3.3 9-3.3c2.8-1.3 6.4-2.1 9.5-1.9Zm23.3-8.5c1.7-2 3.6-3.1 6.1-3.8l6.6-2.4 6.6-2.4c2.5-1.1 4.4-1.5 7.1-1.1-1.8 2-3.5 3.1-6.1 3.8l-6.6 2.4-6.6 2.4c-2.4 1.1-4.5 1.5-7.1 1.1Zm-47.4-32.9c-1.3 2.8-3.9 5.5-6.4 7.3l-7.3 6.2-7.3 6.2c-2.2 2.2-5.2 4.1-8.3 5.1 1.4-2.8 3.8-5.4 6.4-7.3l7.3-6.2 7.3-6.2c2.2-2.1 5.3-4.2 8.3-5.1Zm19-16c.9-2.5 2.3-4.1 4.5-5.7l5.4-4.6 5.4-4.6c1.9-1.9 3.7-2.9 6.3-3.5-1 2.5-2.3 4.1-4.5 5.7l-5.4 4.6-5.4 4.6c-1.9 1.8-3.7 3-6.3 3.5Zm-55.8-14.8c-.3 3.1-1.8 6.5-3.5 9l-4.8 8.3-4.8 8.3c-1.4 2.8-3.5 5.7-6 7.6.4-3.1 1.7-6.4 3.5-9l4.8-8.3 4.8-8.3c1.3-2.8 3.5-5.8 6-7.6Zm12.3-21.5c0-2.7.8-4.7 2.3-6.9l3.5-6.1 3.5-6.1c1.2-2.4 2.5-4 4.8-5.4 0 2.7-.7 4.6-2.3 6.9l-3.5 6.1-3.5 6.1c-1.1 2.4-2.5 4-4.8 5.4Z"/>
|
||||
</g>
|
||||
<path class="cls-15" d="m460.9 185.2 2.8-.5-2.8-.5c-1.2-.2-2.1-1.1-2.2-2.2l-.5-2.8-.5 2.8c-.2 1.2-1.1 2.1-2.2 2.2l-2.8.5 2.8.5c1.2.2 2.1 1.1 2.2 2.2l.5 2.8.5-2.8c.2-1.2 1.1-2.1 2.2-2.2Z"/>
|
||||
<path class="cls-16" d="M506.5 148.4c-22.1 5.7-29.8 27.2-29.8 27.2s-5.8-3.5-15.4-1.9c-8.1 1.3-16.5 8.4-19.1 16.6-16.4 0-23.1 28.3 9.7 28.3s158.3.2 158.3.2-58.4-50.2-71.7-61.7c-1.7-1.5-14.5-13.1-32.1-8.6Z"/>
|
||||
<path style="fill:#f8dca4;stroke-width:0" d="M169.9 362.5h350.9V458H169.9z"/>
|
||||
<path style="fill:none;stroke-width:4.8px;stroke-linecap:round;stroke-linejoin:round;stroke:#fff" d="m275.2 446.7 18.1-31.1"/>
|
||||
<path class="cls-16" d="M97.3 194.2h125.3c25.7 0 32.6-7.8 32.6-20s-17-16.1-19.7-16.1c-1.8-5.5-10.4-14.7-20.7-15.1-10.4-.4-19 3.9-23.7 11.6-5.8-13.2-20.3-15.8-30.9-14.6l-62.8 54.1Zm95.3-20.6c-.3.5-.7 1-1 1.6-.2-.6-.5-1.1-.8-1.6h1.8Z"/>
|
||||
<path class="cls-4" d="M174.5 180.8h-71.6c-11.6 0-18.1-8.2-18.1-22.4s12.1-33.2 36.7-33.2 34.1 14.2 37.1 22c10.8-1.7 25.4.9 31.5 14.7 5.4-7.7 13.4-12.1 23.7-11.6 10.4.4 18.4 8.2 20.7 15.1 11.2 0 15.1 2.2 15.1 7.8s-3.5 7.8-19.8 7.8h-55.2Z"/>
|
||||
<path class="cls-10" d="M384 370.8h-63.4c-1 0-1.8-.8-1.8-1.8s.8-1.8 1.8-1.8H384c1 0 1.8.8 1.8 1.8s-.8 1.8-1.8 1.8Zm-40.2 9.2h-38.5c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2h38.5c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2Zm-6.8 8.1h-18.9c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2H337c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2Zm16.7 0h-7.4c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2h7.4c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2Z"/>
|
||||
<circle class="cls-10" cx="349.4" cy="378.8" r="1.3"/>
|
||||
<path class="cls-10" d="M313.9 386.7c0 .6-.5 1-1 1s-1-.5-1-1 .5-1 1-1 1 .5 1 1Z"/>
|
||||
<path class="cls-6" d="M310.9 409.3C298.2 397 288 395 288 395s.8-10-6.9-17.4c-7.8-7.3-18.4-8.9-18.4-8.9s-2.5-21.6-21.2-36.3c-18.8-14.7-38.4-8.9-38.4-8.9s-22.9-35.5-52.7-37.8c-8.2-.6-15-.2-20.7.8-10.1-13.9-26.4-22.9-44.9-22.9-30.6 0-55.4 24.8-55.4 55.4s24.8 55.4 55.4 55.4 31.9-7.5 42.1-19.4l25.6 80 163.1 53.1s15.8-7.5 15.8-35-7.8-31.6-20.4-44Z"/>
|
||||
<path class="cls-2" d="M125.6 319.1s54.4-16.6 96.5 41.7c10-.4 39.4 6.6 45.1 31.6 5.8 25.1 8 42.2 8 42.2H135.3l-9.6-115.5Z"/>
|
||||
<path class="cls-5" d="M299.3 436.3c0-11.5-3.9-28.1-23.4-40.9-12.3-8-32.6-9.2-37.2-5.9-27.7-44.5-67.9-35.7-67.9-35.7l-13.1 73.3h96.8c-.2-.9 14.1 34.6 13.9 33.7 0 0 30.7 5.7 30.9-24.6Z"/>
|
||||
<path class="cls-6" d="M599.6 263.7c-18.8 0-35.4 9.4-45.5 23.7-6.3-1.5-14.4-2.4-24.4-1.6-29.8 2.3-52.6 37.8-52.6 37.8s-19.6-5.8-38.3 8.9c-18.8 14.7-21.2 36.3-21.2 36.3s-10.6 1.5-18.4 8.9-6.9 17.4-6.9 17.4-10.2 1.9-22.8 14.3c-12.6 12.3-17.6 33.8-17.6 49.2s.9 30.2.9 30.2l175-53.6 26.8-83.9c10 14.1 26.5 23.3 45.1 23.3 30.6 0 55.4-24.8 55.4-55.4s-24.8-55.4-55.4-55.4Z"/>
|
||||
<path class="cls-2" d="M573.1 322.3s-52.5-21.9-100.1 31.9c-9.9-1.4-36.6 3.9-48.1 27-11.5 23.2-12.1 41.2-12.1 41.2L552 436.3l21.1-114Z"/>
|
||||
<path class="cls-5" d="M368.8 434.1s3.1-28.7 36.5-41.5c12.3-4.7 28.6-3.2 34.1.8 33-53.1 80.9-42.6 80.9-42.6l15.6 87.4H420.5c.3-1-71.1 51.7-49.3 34.7l-2.5-38.8Z"/>
|
||||
<path d="M28.1 349.4s17.7-28 26.6-40.4c23.9-33.5 59.4-34 85.1 2.1 11.8 16.5 24.3 33.1 38.1 53.9 10.1 15.3 19.1-7.4 43.6 17.8 17.9 18.4 43.7 74.9 43.7 74.9l13.6-10.3v14.8l-230.1-7.6-20.6-105.1Z" style="stroke-width:3.3px;stroke-miterlimit:10;fill:#1a374a;fill-rule:evenodd;stroke:#fff"/>
|
||||
<path d="M519.1 352.6c7.9 2.9 16.3-11.7 22.3-22.2 18.2-31.9 63.6-37.1 86.2-4 5.9 8.6 18.4 30.1 26.8 45.3 3.6 6.5-41.8 83.8-41.8 83.8l-256.4 8s37.1-2.3 42.5-9.5c38.5-50.9 49.7-65.9 62.7-82.9 16.1-21.1 37.8-25.6 57.7-18.4Z" style="stroke-miterlimit:10;fill:#1a374a;fill-rule:evenodd;stroke:#fff;stroke-width:3.2px"/>
|
||||
<path class="cls-8" d="M292 424.1s-11.5 18.6-11.5 30.2h22.9c0-11.7-11.5-30.2-11.5-30.2Z"/>
|
||||
<path class="cls-12" d="M291.5 433.3h.9v21.1h-.9z"/>
|
||||
<path class="cls-12" d="m291.6 442-4.7-3.7.7-.3 4.7 3.8-.7.2zm.7 6.4-.7-.3 7.1-4.4.6.3-7 4.4z"/>
|
||||
<path class="cls-8" d="M250 391.2s15.6 38.5 15.6 62.8h-31.3c0-24.3 15.6-62.8 15.6-62.8Z"/>
|
||||
<path class="cls-12" d="M249.4 410.1h1.2V454h-1.2z"/>
|
||||
<path class="cls-12" d="m250.5 428.4 6.4-7.9-1-.5-6.4 7.8 1 .6zm-1 13.1 1-.6-9.6-9.2-.9.6 9.5 9.2z"/>
|
||||
<path class="cls-8" d="M275.6 413.9s-12.8 24.6-12.8 40.1h25.7c0-15.5-12.8-40.1-12.8-40.1Z"/>
|
||||
<path class="cls-12" d="M275.1 426h1v28h-1z"/>
|
||||
<path class="cls-12" d="m275.2 437.6-5.3-5 .8-.3 5.3 5-.8.3zm.8 8.4-.8-.4 7.8-5.8.8.4-7.8 5.8z"/>
|
||||
<path class="cls-8" d="M306.6 423.7s-13.4 18.6-13.4 30.2H320c0-11.7-13.4-30.2-13.4-30.2Z"/>
|
||||
<path class="cls-12" d="M306.1 432.8h1v21.1h-1z"/>
|
||||
<path class="cls-12" d="m306.2 441.6-5.5-3.7.9-.3 5.5 3.8-.9.2zm.8 6.4-.8-.3 8.2-4.4.8.3-8.2 4.4z"/>
|
||||
<path class="cls-8" d="M323.6 442.5s-4.9 7-4.9 11.4h9.9c0-4.4-4.9-11.4-4.9-11.4Z"/>
|
||||
<path class="cls-12" d="M323.4 446h.4v8h-.4z"/>
|
||||
<path class="cls-12" d="m323.4 449.3-2-1.4.3-.1 2 1.4-.3.1zm.3 2.4-.3-.1 3-1.7.3.1-3 1.7z"/>
|
||||
<path class="cls-8" d="M333.7 442.5s-7.6 7-7.6 11.4h15.2c0-4.4-7.6-11.4-7.6-11.4Z"/>
|
||||
<path class="cls-12" d="M333.4 446h.6v8h-.6z"/>
|
||||
<path class="cls-12" d="m333.4 449.3-3.1-1.4.5-.1 3.1 1.4-.5.1zm.5 2.4-.5-.1 4.7-1.7.4.1-4.6 1.7z"/>
|
||||
<path class="cls-8" d="M374.9 424.1s11.5 18.6 11.5 30.2h-22.9c0-11.7 11.5-30.2 11.5-30.2Z"/>
|
||||
<path class="cls-12" d="M374.4 433.3h.9v21.1h-.9z"/>
|
||||
<path class="cls-12" d="m375.3 442 4.7-3.7-.8-.3-4.7 3.8.8.2zm-.8 6.4.7-.3-7-4.4-.7.3 7 4.4z"/>
|
||||
<path class="cls-8" d="M416.8 391.2s-15.6 38.5-15.6 62.8h31.3c0-24.3-15.6-62.8-15.6-62.8Z"/>
|
||||
<path class="cls-12" d="M416.2 410.1h1.2V454h-1.2z"/>
|
||||
<path class="cls-12" d="m416.3 428.4-6.4-7.9 1-.5 6.5 7.8-1.1.6zm1 13.1-.9-.6 9.5-9.2 1 .6-9.6 9.2z"/>
|
||||
<path class="cls-8" d="M391.3 413.9s12.8 24.6 12.8 40.1h-25.7c0-15.5 12.8-40.1 12.8-40.1Z"/>
|
||||
<path class="cls-12" d="M390.8 426h1v28h-1z"/>
|
||||
<path class="cls-12" d="m391.7 437.6 5.3-5-.9-.3-5.2 5 .8.3zm-.8 8.4.8-.4-7.9-5.8-.8.4 7.9 5.8z"/>
|
||||
<path class="cls-8" d="M360.2 423.7s13.4 18.6 13.4 30.2h-26.8c0-11.7 13.4-30.2 13.4-30.2Z"/>
|
||||
<path class="cls-12" d="M359.7 432.8h1v21.1h-1z"/>
|
||||
<path class="cls-12" d="m360.7 441.6 5.4-3.7-.8-.3-5.5 3.8.9.2zm-.9 6.4.8-.3-8.2-4.4-.8.3 8.2 4.4z"/>
|
||||
<path class="cls-8" d="M343.3 442.5s4.9 7 4.9 11.4h-9.9c0-4.4 4.9-11.4 4.9-11.4Z"/>
|
||||
<path class="cls-12" d="M343.1 446h.4v8h-.4z"/>
|
||||
<path class="cls-12" d="m343.4 449.3 2.1-1.4-.3-.1-2.1 1.4.3.1zm-.3 2.4.3-.1-3-1.7-.3.1 3 1.7z"/>
|
||||
<path class="cls-8" d="M333.2 442.5s7.6 7 7.6 11.4h-15.2c0-4.4 7.6-11.4 7.6-11.4Z"/>
|
||||
<path class="cls-12" d="M332.9 446h.6v8h-.6z"/>
|
||||
<path class="cls-12" d="m333.4 449.3 3.2-1.4-.5-.1-3.2 1.4.5.1zm-.4 2.4.4-.1-4.6-1.7-.5.1 4.7 1.7z"/>
|
||||
<path class="cls-8" d="M38.4 453.9h617v90.7h-617z"/>
|
||||
<path d="m357 510.4 95.2 141.5h151.9c26.4 0 57.6-11.3 57.6-54V370c0-29.7-34.5-46.2-57.6-27.5l-247 167.9Z" style="opacity:.2;fill:#010101;stroke-width:0"/>
|
||||
<path class="cls-11" d="m397.1 507.8 56.4 163.1h151.9c26.4 0 57.6-11.3 57.6-54V366.7c0-29.7-34.5-46.2-57.6-27.5L397.1 507.8Zm-113.8 0-56.4 163.1H75c-26.4 0-57.6-11.3-57.6-54V366.7c0-29.7 34.5-46.2 57.6-27.5l208.3 168.6Z"/>
|
||||
<path class="cls-18" d="M227.8 670.2H32.4c-22.7 0-3.8-15.8-.3-18.2l232.2-159.2 18.5 15-54.9 162.4Zm245.3-196.8-65.4 52.8c-2.4 1.9-5.9 1.7-7.7-.7-1.9-2.3-1.4-5.8 1-7.7l65.4-52.8c2.4-1.9 5.9-1.7 7.7.7 1.9 2.3 1.4 5.8-1 7.7Zm25.6-20.7-4 3.2c-2.4 1.9-5.9 1.7-7.7-.7-1.9-2.3-1.4-5.8 1-7.7l4-3.2c2.4-1.9 5.9-1.7 7.7.7 1.9 2.3 1.4 5.8-1 7.7Zm16.3-13.1-.5.4c-2.4 1.9-5.8 1.6-7.7-.7-1.9-2.3-1.5-5.7.9-7.7l.5-.4c2.4-1.9 5.8-1.6 7.7.7 1.9 2.3 1.5 5.7-.9 7.7Z"/>
|
||||
<path class="cls-18" d="M452.1 670.2h195.4c22.7 0 3.8-15.8.3-18.2L415.6 492.8l-18.5 15L452 670.2Z"/>
|
||||
<path class="cls-4" d="M440.9 205c-8.2 0-8.1-11.1 5.2-11.1 2.6-8.2 11.2-15.5 19.4-15.5s15.1 5.2 15.1 5.2 6.9-30 29.8-31.5c16.9-1.1 28.5 12.5 28.5 12.5s8.2-6.9 21.6-6.9c10.4-10.4 20.3-17.1 41.4-14.7 21 2.4 34.5 26.3 34.5 42.7S618.3 205 614 205H441Z"/>
|
||||
<path class="cls-15" d="M411.9 129.6c2.7 0 2.6-3.6-1.7-3.6-.8-2.7-2.9-4.9-6.3-5-3.2-.1-4.9 1.7-4.9 1.7s-1.9-10.2-11-10.2-9.5 8-9.5 8-1.7-4.3-6.7-4.3c-9.5 0-9.3 13.5.2 13.5h39.9Z"/>
|
||||
<path d="M647.2 657.7c-13.7-9-292.8-187.5-292.8-187.5-4.3-2.8-9.2-4.1-14.1-4.2h-.4c-4.9 0-9.8 1.4-14.1 4.2 0 0-279.1 178.6-292.8 187.5-11.1 7.3-10 12.8-.2 12.8h614.4c9.9 0 10.9-5.5-.2-12.8Z" style="fill:#f5e6ca;stroke-width:0"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 14 KiB |
89
package.nix
89
package.nix
|
@ -1,89 +0,0 @@
|
|||
# TODO: move this to nixpkgs
|
||||
# This file aims to be a replacement for the nixpkgs derivation.
|
||||
|
||||
{
|
||||
lib,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
fetchFromGitHub,
|
||||
stdenv,
|
||||
apple-sdk,
|
||||
installShellFiles,
|
||||
installShellCompletions ? stdenv.buildPlatform.canExecute stdenv.hostPlatform,
|
||||
installManPages ? stdenv.buildPlatform.canExecute stdenv.hostPlatform,
|
||||
notmuch,
|
||||
gpgme,
|
||||
buildNoDefaultFeatures ? false,
|
||||
buildFeatures ? [ ],
|
||||
}:
|
||||
|
||||
let
|
||||
version = "1.0.0-beta.4";
|
||||
hash = "sha256-NrWBg0sjaz/uLsNs8/T4MkUgHOUvAWRix1O5usKsw6o=";
|
||||
cargoHash = "sha256-YS8IamapvmdrOPptQh2Ef9Yold0IK1XIeGs0kDIQ5b8=";
|
||||
in
|
||||
|
||||
rustPlatform.buildRustPackage rec {
|
||||
inherit cargoHash version;
|
||||
inherit buildNoDefaultFeatures buildFeatures;
|
||||
|
||||
pname = "himalaya";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
inherit hash;
|
||||
owner = "pimalaya";
|
||||
repo = "himalaya";
|
||||
rev = "v${version}";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
] ++ lib.optional (installManPages || installShellCompletions) installShellFiles;
|
||||
|
||||
buildInputs =
|
||||
[ ]
|
||||
++ lib.optional stdenv.hostPlatform.isDarwin apple-sdk
|
||||
++ lib.optional (builtins.elem "notmuch" buildFeatures) notmuch
|
||||
++ lib.optional (builtins.elem "pgp-gpg" buildFeatures) gpgme;
|
||||
|
||||
doCheck = false;
|
||||
auditable = false;
|
||||
|
||||
# unit tests only
|
||||
cargoTestFlags = [ "--lib" ];
|
||||
|
||||
postInstall =
|
||||
''
|
||||
mkdir -p $out/share/{applications,completions,man}
|
||||
cp assets/himalaya.desktop "$out"/share/applications/
|
||||
''
|
||||
+ lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
|
||||
"$out"/bin/himalaya man "$out"/share/man
|
||||
''
|
||||
+ lib.optionalString installManPages ''
|
||||
installManPage "$out"/share/man/*
|
||||
''
|
||||
+ lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
|
||||
"$out"/bin/himalaya completion bash > "$out"/share/completions/himalaya.bash
|
||||
"$out"/bin/himalaya completion elvish > "$out"/share/completions/himalaya.elvish
|
||||
"$out"/bin/himalaya completion fish > "$out"/share/completions/himalaya.fish
|
||||
"$out"/bin/himalaya completion powershell > "$out"/share/completions/himalaya.powershell
|
||||
"$out"/bin/himalaya completion zsh > "$out"/share/completions/himalaya.zsh
|
||||
''
|
||||
+ lib.optionalString installShellCompletions ''
|
||||
installShellCompletion "$out"/share/completions/himalaya.{bash,fish,zsh}
|
||||
'';
|
||||
|
||||
meta = rec {
|
||||
description = "CLI to manage emails";
|
||||
mainProgram = "himalaya";
|
||||
homepage = "https://github.com/pimalaya/himalaya";
|
||||
changelog = "${homepage}/blob/v${version}/CHANGELOG.md";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [
|
||||
soywod
|
||||
toastal
|
||||
yanganto
|
||||
];
|
||||
};
|
||||
}
|
27
rust-toolchain.nix
Normal file
27
rust-toolchain.nix
Normal file
|
@ -0,0 +1,27 @@
|
|||
fenix:
|
||||
|
||||
let
|
||||
file = ./rust-toolchain.toml;
|
||||
sha256 = "e4mlaJehWBymYxJGgnbuCObVlqMlQSilZ8FljG9zPHY=";
|
||||
in
|
||||
{
|
||||
fromFile = { system }: fenix.packages.${system}.fromToolchainFile {
|
||||
inherit file sha256;
|
||||
};
|
||||
|
||||
fromTarget = { pkgs, buildPlatform, targetPlatform ? null }:
|
||||
let
|
||||
inherit ((pkgs.lib.importTOML file).toolchain) channel;
|
||||
toolchain = fenix.packages.${buildPlatform};
|
||||
in
|
||||
if
|
||||
isNull targetPlatform
|
||||
then
|
||||
fenix.packages.${buildPlatform}.${channel}.toolchain
|
||||
else
|
||||
toolchain.combine [
|
||||
toolchain.${channel}.rustc
|
||||
toolchain.${channel}.cargo
|
||||
toolchain.targets.${targetPlatform}.${channel}.rust-std
|
||||
];
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.82.0"
|
||||
channel = "stable"
|
||||
profile = "default"
|
||||
components = ["rust-src", "rust-analyzer"]
|
||||
components = [ "rust-src", "rust-analyzer" ]
|
||||
|
|
BIN
screenshot.jpeg
BIN
screenshot.jpeg
Binary file not shown.
Before Width: | Height: | Size: 165 KiB |
18
shell.nix
18
shell.nix
|
@ -1,6 +1,12 @@
|
|||
{
|
||||
pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"),
|
||||
...
|
||||
}@args:
|
||||
|
||||
pimalaya.mkShell ({ rustToolchainFile = ./rust-toolchain.toml; } // removeAttrs args [ "pimalaya" ])
|
||||
# This file exists for legacy nix-shell
|
||||
# https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix
|
||||
# You generally do *not* have to modify this ever.
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
|
|
124
src/account/command/check_up.rs
Normal file
124
src/account/command/check_up.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use email::backend::context::BackendContextBuilder;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::OptionalAccountNameArg, backend, config::TomlConfig, printer::Printer,
|
||||
};
|
||||
|
||||
/// Check up the given account.
|
||||
///
|
||||
/// This command performs a checkup of the given account. It checks if
|
||||
/// the configuration is valid, if backend can be created and if
|
||||
/// sessions work as expected.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountCheckUpCommand {
|
||||
#[command(flatten)]
|
||||
pub account: OptionalAccountNameArg,
|
||||
}
|
||||
|
||||
impl AccountCheckUpCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing check up account command");
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
|
||||
printer.print_log("Checking configuration integrity…")?;
|
||||
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
account,
|
||||
#[cfg(feature = "account-sync")]
|
||||
true,
|
||||
)?;
|
||||
let used_backends = toml_account_config.get_used_backends();
|
||||
|
||||
printer.print_log("Checking backend context integrity…")?;
|
||||
|
||||
let ctx_builder = backend::BackendContextBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
Vec::from_iter(used_backends),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ctx = ctx_builder.clone().build().await?;
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
{
|
||||
printer.print_log("Checking Maildir integrity…")?;
|
||||
|
||||
let maildir = ctx_builder
|
||||
.maildir
|
||||
.as_ref()
|
||||
.and_then(|maildir| maildir.check_up())
|
||||
.and_then(|f| ctx.maildir.as_ref().and_then(|ctx| f(ctx)));
|
||||
|
||||
if let Some(maildir) = maildir.as_ref() {
|
||||
maildir.check_up().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
{
|
||||
printer.print_log("Checking IMAP integrity…")?;
|
||||
|
||||
let imap = ctx_builder
|
||||
.imap
|
||||
.as_ref()
|
||||
.and_then(|imap| imap.check_up())
|
||||
.and_then(|f| ctx.imap.as_ref().and_then(|ctx| f(ctx)));
|
||||
|
||||
if let Some(imap) = imap.as_ref() {
|
||||
imap.check_up().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
{
|
||||
printer.print_log("Checking Notmuch integrity…")?;
|
||||
|
||||
let notmuch = ctx_builder
|
||||
.notmuch
|
||||
.as_ref()
|
||||
.and_then(|notmuch| notmuch.check_up())
|
||||
.and_then(|f| ctx.notmuch.as_ref().and_then(|ctx| f(ctx)));
|
||||
|
||||
if let Some(notmuch) = notmuch.as_ref() {
|
||||
notmuch.check_up().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
{
|
||||
printer.print_log("Checking SMTP integrity…")?;
|
||||
|
||||
let smtp = ctx_builder
|
||||
.smtp
|
||||
.as_ref()
|
||||
.and_then(|smtp| smtp.check_up())
|
||||
.and_then(|f| ctx.smtp.as_ref().and_then(|ctx| f(ctx)));
|
||||
|
||||
if let Some(smtp) = smtp.as_ref() {
|
||||
smtp.check_up().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
{
|
||||
printer.print_log("Checking Sendmail integrity…")?;
|
||||
|
||||
let sendmail = ctx_builder
|
||||
.sendmail
|
||||
.as_ref()
|
||||
.and_then(|sendmail| sendmail.check_up())
|
||||
.and_then(|f| ctx.sendmail.as_ref().and_then(|ctx| f(ctx)));
|
||||
|
||||
if let Some(sendmail) = sendmail.as_ref() {
|
||||
sendmail.check_up().await?;
|
||||
}
|
||||
}
|
||||
|
||||
printer.print("Checkup successfully completed!")
|
||||
}
|
||||
}
|
|
@ -1,52 +1,113 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
use log::info;
|
||||
#[cfg(any(feature = "imap", feature = "smtp"))]
|
||||
use log::{debug, warn};
|
||||
|
||||
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
|
||||
#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))]
|
||||
use crate::ui::prompt;
|
||||
use crate::{account::arg::name::AccountNameArg, config::TomlConfig, printer::Printer};
|
||||
|
||||
/// Configure the given account.
|
||||
/// Configure an account.
|
||||
///
|
||||
/// This command allows you to configure an existing account or to
|
||||
/// create a new one, using the wizard. The `wizard` cargo feature is
|
||||
/// required.
|
||||
/// This command is mostly used to define or reset passwords managed
|
||||
/// by your global keyring. If you do not use the keyring system, you
|
||||
/// can skip this command.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountConfigureCommand {
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameArg,
|
||||
|
||||
/// Reset keyring passwords.
|
||||
///
|
||||
/// This argument will force passwords to be prompted again, then
|
||||
/// saved to your global keyring.
|
||||
#[arg(long, short)]
|
||||
pub reset: bool,
|
||||
}
|
||||
|
||||
impl AccountConfigureCommand {
|
||||
#[cfg(feature = "wizard")]
|
||||
pub async fn execute(
|
||||
self,
|
||||
mut config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing configure account command");
|
||||
|
||||
info!("executing account configure command");
|
||||
let account = &self.account.name;
|
||||
let (_, account_config) = config.into_toml_account_config(Some(account))?;
|
||||
|
||||
let path = match config_path {
|
||||
Some(path) => path.clone(),
|
||||
None => TomlConfig::default_path()?,
|
||||
};
|
||||
if self.reset {
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = account_config.imap {
|
||||
let reset = match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => config.reset().await,
|
||||
ImapAuthConfig::OAuth2(config) => config.reset().await,
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting imap secrets: {err}");
|
||||
debug!("error while resetting imap secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let account_name = Some(self.account.name.as_str());
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = account_config.smtp {
|
||||
let reset = match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => config.reset().await,
|
||||
SmtpAuthConfig::OAuth2(config) => config.reset().await,
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting smtp secrets: {err}");
|
||||
debug!("error while resetting smtp secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let account_config = config
|
||||
.accounts
|
||||
.remove(&self.account.name)
|
||||
.unwrap_or_default();
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = account_config.pgp {
|
||||
config.reset().await?;
|
||||
}
|
||||
}
|
||||
|
||||
wizard::edit(path, config, account_name, account_config).await?;
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = account_config.imap {
|
||||
match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt::passwd("IMAP password")).await
|
||||
}
|
||||
ImapAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt::secret("IMAP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = account_config.smtp {
|
||||
match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt::passwd("SMTP password")).await
|
||||
}
|
||||
SmtpAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt::secret("SMTP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wizard"))]
|
||||
pub async fn execute(self, _: TomlConfig, _: Option<&PathBuf>) -> Result<()> {
|
||||
color_eyre::eyre::bail!("This command requires the `wizard` cargo feature to work");
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = account_config.pgp {
|
||||
config
|
||||
.configure(&account_config.email, || {
|
||||
prompt::passwd("PGP secret key password")
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Account {account} successfully {}configured!",
|
||||
if self.reset { "re" } else { "" }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,233 +0,0 @@
|
|||
use std::{
|
||||
io::{stdout, Write},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{Result, Section};
|
||||
#[cfg(all(feature = "keyring", feature = "imap"))]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::ImapContextBuilder;
|
||||
#[cfg(feature = "maildir")]
|
||||
use email::maildir::MaildirContextBuilder;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::NotmuchContextBuilder;
|
||||
#[cfg(feature = "sendmail")]
|
||||
use email::sendmail::SendmailContextBuilder;
|
||||
#[cfg(all(feature = "keyring", feature = "smtp"))]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::SmtpContextBuilder;
|
||||
use email::{backend::BackendBuilder, config::Config};
|
||||
#[cfg(feature = "keyring")]
|
||||
use pimalaya_tui::terminal::prompt;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Backend, SendingBackend},
|
||||
terminal::config::TomlConfig as _,
|
||||
};
|
||||
|
||||
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
|
||||
|
||||
/// Diagnose and fix the given account.
|
||||
///
|
||||
/// This command diagnoses the given account and can even try to fix
|
||||
/// it. It mostly checks if the configuration is valid, if backends
|
||||
/// can be instanciated and if sessions work as expected.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountDoctorCommand {
|
||||
#[command(flatten)]
|
||||
pub account: OptionalAccountNameArg,
|
||||
|
||||
/// Try to fix the given account.
|
||||
///
|
||||
/// This argument can be used to (re)configure keyring entries for
|
||||
/// example.
|
||||
#[arg(long, short)]
|
||||
pub fix: bool,
|
||||
}
|
||||
|
||||
impl AccountDoctorCommand {
|
||||
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
|
||||
let mut stdout = stdout();
|
||||
|
||||
if let Some(name) = self.account.name.as_ref() {
|
||||
print!("Checking TOML configuration integrity for account {name}… ");
|
||||
} else {
|
||||
print!("Checking TOML configuration integrity for default account… ");
|
||||
}
|
||||
|
||||
stdout.flush()?;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
println!("OK");
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
if self.fix {
|
||||
if prompt::bool("Would you like to reset existing keyring entries?", false)? {
|
||||
print!("Resetting keyring entries… ");
|
||||
stdout.flush()?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
match toml_account_config.imap_auth_config() {
|
||||
Some(ImapAuthConfig::Password(config)) => config.reset().await?,
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
match toml_account_config.smtp_auth_config() {
|
||||
Some(SmtpAuthConfig::Password(config)) => config.reset().await?,
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
if let Some(config) = &toml_account_config.pgp {
|
||||
config.reset().await?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
match toml_account_config.imap_auth_config() {
|
||||
Some(ImapAuthConfig::Password(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::password("IMAP password")?))
|
||||
.await?;
|
||||
}
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(ImapAuthConfig::OAuth2(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
match toml_account_config.smtp_auth_config() {
|
||||
Some(SmtpAuthConfig::Password(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::password("SMTP password")?))
|
||||
.await?;
|
||||
}
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(SmtpAuthConfig::OAuth2(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
if let Some(config) = &toml_account_config.pgp {
|
||||
config
|
||||
.configure(&toml_account_config.email, || {
|
||||
Ok(prompt::password("PGP secret key password")?)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
match toml_account_config.backend {
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(Backend::Maildir(mdir_config)) => {
|
||||
print!("Checking Maildir integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(Backend::Imap(imap_config)) => {
|
||||
print!("Checking IMAP integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
|
||||
.with_pool_size(1);
|
||||
let res = BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await;
|
||||
|
||||
if self.fix {
|
||||
res?;
|
||||
} else {
|
||||
res.note("Run with --fix to (re)configure your account.")?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(Backend::Notmuch(notmuch_config)) => {
|
||||
print!("Checking Notmuch integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx =
|
||||
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let sending_backend = toml_account_config
|
||||
.message
|
||||
.and_then(|msg| msg.send)
|
||||
.and_then(|send| send.backend);
|
||||
|
||||
match sending_backend {
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(SendingBackend::Smtp(smtp_config)) => {
|
||||
print!("Checking SMTP integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
|
||||
let res = BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await;
|
||||
|
||||
if self.fix {
|
||||
res?;
|
||||
} else {
|
||||
res.note("Run with --fix to (re)configure your account.")?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "sendmail")]
|
||||
Some(SendingBackend::Sendmail(sendmail_config)) => {
|
||||
print!("Checking Sendmail integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx =
|
||||
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,41 +1,36 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Accounts, AccountsTable},
|
||||
terminal::cli::printer::Printer,
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::Accounts,
|
||||
config::TomlConfig,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
/// List all existing accounts.
|
||||
/// List all accounts.
|
||||
///
|
||||
/// This command lists all the accounts defined in your TOML
|
||||
/// configuration file.
|
||||
/// This command lists all accounts defined in your TOML configuration
|
||||
/// file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountListCommand {
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width, in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
#[command(flatten)]
|
||||
pub table: TableMaxWidthFlag,
|
||||
}
|
||||
|
||||
impl AccountListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list accounts command");
|
||||
|
||||
let accounts = Accounts::from(config.accounts.iter());
|
||||
let table = AccountsTable::from(accounts)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(config.account_list_table_preset())
|
||||
.with_some_name_color(config.account_list_table_name_color())
|
||||
.with_some_backends_color(config.account_list_table_backends_color())
|
||||
.with_some_default_color(config.account_list_table_default_color());
|
||||
let accounts: Accounts = config.accounts.iter().into();
|
||||
|
||||
printer.out(table)
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: &Default::default(),
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,50 @@
|
|||
mod check_up;
|
||||
mod configure;
|
||||
mod doctor;
|
||||
mod list;
|
||||
#[cfg(feature = "account-sync")]
|
||||
mod sync;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use self::sync::AccountSyncCommand;
|
||||
use self::{
|
||||
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
|
||||
check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand,
|
||||
};
|
||||
|
||||
/// Configure, list and diagnose your accounts.
|
||||
/// Manage accounts.
|
||||
///
|
||||
/// An account is a group of settings, identified by a unique
|
||||
/// name. This subcommand allows you to manage your accounts.
|
||||
/// An account is a set of settings, identified by an account
|
||||
/// name. Settings are directly taken from your TOML configuration
|
||||
/// file. This subcommand allows you to manage them.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountSubcommand {
|
||||
#[command(alias = "checkup")]
|
||||
CheckUp(AccountCheckUpCommand),
|
||||
|
||||
#[command(alias = "cfg")]
|
||||
Configure(AccountConfigureCommand),
|
||||
Doctor(AccountDoctorCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
List(AccountListCommand),
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(alias = "synchronize", alias = "synchronise")]
|
||||
Sync(AccountSyncCommand),
|
||||
}
|
||||
|
||||
impl AccountSubcommand {
|
||||
pub async fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Configure(cmd) => cmd.execute(config, config_path).await,
|
||||
Self::Doctor(cmd) => cmd.execute(&config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, &config).await,
|
||||
Self::CheckUp(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Configure(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
#[cfg(feature = "account-sync")]
|
||||
Self::Sync(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
359
src/account/command/sync.rs
Normal file
359
src/account/command/sync.rs
Normal file
|
@ -0,0 +1,359 @@
|
|||
use crate::{
|
||||
account::{arg::name::OptionalAccountNameArg, config::TomlAccountConfig},
|
||||
backend::{Backend, BackendContextBuilder, BackendKind},
|
||||
config::TomlConfig,
|
||||
printer::Printer,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Parser};
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::ImapContextBuilder;
|
||||
use email::maildir::config::MaildirConfig;
|
||||
#[cfg(feature = "maildir")]
|
||||
use email::maildir::MaildirContextBuilder;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::NotmuchContextBuilder;
|
||||
use email::{
|
||||
account::{config::AccountConfig, sync::AccountSyncBuilder},
|
||||
backend::BackendBuilder,
|
||||
folder::sync::config::FolderSyncStrategy,
|
||||
sync::SyncEvent,
|
||||
};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
ops::Deref,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
static MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
|
||||
});
|
||||
|
||||
static SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(
|
||||
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
static SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
|
||||
});
|
||||
|
||||
/// Synchronize an account.
|
||||
///
|
||||
/// This command allows you to synchronize all folders and emails
|
||||
/// (including envelopes and messages) of a given account into a local
|
||||
/// Maildir folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountSyncCommand {
|
||||
#[command(flatten)]
|
||||
pub account: OptionalAccountNameArg,
|
||||
|
||||
/// Run the synchronization without applying any changes.
|
||||
///
|
||||
/// Instead, a report will be printed to stdout containing all the
|
||||
/// changes the synchronization plan to do.
|
||||
#[arg(long, short)]
|
||||
pub dry_run: bool,
|
||||
|
||||
/// Synchronize only specific folders.
|
||||
///
|
||||
/// Only the given folders will be synchronized (including
|
||||
/// associated envelopes and messages). Useful when you need to
|
||||
/// speed up the synchronization process. A good usecase is to
|
||||
/// synchronize only the INBOX in order to quickly check for new
|
||||
/// messages.
|
||||
#[arg(long, short = 'f')]
|
||||
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
|
||||
#[arg(conflicts_with = "exclude_folder", conflicts_with = "all_folders")]
|
||||
pub include_folder: Vec<String>,
|
||||
|
||||
/// Omit specific folders from the synchronization.
|
||||
///
|
||||
/// The given folders will be excluded from the synchronization
|
||||
/// (including associated envelopes and messages). Useful when you
|
||||
/// have heavy folders that you do not want to take care of, or to
|
||||
/// speed up the synchronization process.
|
||||
#[arg(long, short = 'x')]
|
||||
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
|
||||
#[arg(conflicts_with = "include_folder", conflicts_with = "all_folders")]
|
||||
pub exclude_folder: Vec<String>,
|
||||
|
||||
/// Synchronize all exsting folders.
|
||||
#[arg(long, short = 'A')]
|
||||
#[arg(conflicts_with = "include_folder", conflicts_with = "exclude_folder")]
|
||||
pub all_folders: bool,
|
||||
}
|
||||
|
||||
impl AccountSyncCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing sync account command");
|
||||
|
||||
let included_folders = BTreeSet::from_iter(self.include_folder);
|
||||
let excluded_folders = BTreeSet::from_iter(self.exclude_folder);
|
||||
|
||||
let strategy = if !included_folders.is_empty() {
|
||||
Some(FolderSyncStrategy::Include(included_folders))
|
||||
} else if !excluded_folders.is_empty() {
|
||||
Some(FolderSyncStrategy::Exclude(excluded_folders))
|
||||
} else if self.all_folders {
|
||||
Some(FolderSyncStrategy::All)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let account = self.account.name.as_deref();
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, true)?;
|
||||
let account_name = account_config.name.as_str();
|
||||
|
||||
let backend_builder =
|
||||
AccountSyncBackendBuilder::new(toml_account_config, account_config.clone()).await?;
|
||||
let sync_builder = AccountSyncBuilder::new(backend_builder.into())?
|
||||
.with_dry_run(self.dry_run)
|
||||
.with_some_folders_filter(strategy);
|
||||
|
||||
if self.dry_run {
|
||||
let report = sync_builder.sync().await?;
|
||||
let mut hunks_count = report.folder.patch.len();
|
||||
|
||||
if !report.folder.patch.is_empty() {
|
||||
printer.print_log("Folders patch:")?;
|
||||
for (hunk, _) in report.folder.patch {
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
if !report.email.patch.is_empty() {
|
||||
printer.print_log("Envelopes patch:")?;
|
||||
for (hunk, _) in report.email.patch {
|
||||
hunks_count += 1;
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Estimated patch length for account {account_name} to be synchronized: {hunks_count}"
|
||||
))?;
|
||||
} else if printer.is_json() {
|
||||
sync_builder.sync().await?;
|
||||
printer.print(format!("Account {account_name} successfully synchronized!"))?;
|
||||
} else {
|
||||
let multi = MultiProgress::new();
|
||||
let sub_progresses = Mutex::new(HashMap::new());
|
||||
let main_progress = multi.add(
|
||||
ProgressBar::new(100)
|
||||
.with_style(MAIN_PROGRESS_STYLE.clone())
|
||||
.with_message("Listing folders…"),
|
||||
);
|
||||
|
||||
main_progress.tick();
|
||||
|
||||
let report = sync_builder
|
||||
.with_handler(move |evt| {
|
||||
match evt {
|
||||
SyncEvent::ListedAllFolders => {
|
||||
main_progress.set_message("Synchronizing folders…");
|
||||
}
|
||||
SyncEvent::ProcessedAllFolderHunks => {
|
||||
main_progress.set_message("Listing envelopes…");
|
||||
}
|
||||
SyncEvent::GeneratedEmailPatch(patches) => {
|
||||
let patches_len = patches.values().flatten().count();
|
||||
main_progress.set_length(patches_len as u64);
|
||||
main_progress.set_position(0);
|
||||
main_progress.set_message("Synchronizing emails…");
|
||||
|
||||
let mut envelopes_progresses = sub_progresses.lock().unwrap();
|
||||
for (folder, patch) in patches {
|
||||
let progress = ProgressBar::new(patch.len() as u64)
|
||||
.with_style(SUB_PROGRESS_STYLE.clone())
|
||||
.with_prefix(folder.clone())
|
||||
.with_finish(ProgressFinish::AndClear);
|
||||
let progress = multi.add(progress);
|
||||
envelopes_progresses.insert(folder, progress.clone());
|
||||
}
|
||||
}
|
||||
SyncEvent::ProcessedEmailHunk(hunk) => {
|
||||
main_progress.inc(1);
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
if let Some(progress) = progresses.get_mut(hunk.folder()) {
|
||||
progress.inc(1);
|
||||
if progress.position() == (progress.length().unwrap() - 1) {
|
||||
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
|
||||
} else {
|
||||
progress.set_message(format!("{hunk}…"));
|
||||
}
|
||||
}
|
||||
}
|
||||
SyncEvent::ProcessedAllEmailHunks => {
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
for progress in progresses.values() {
|
||||
progress.finish_and_clear()
|
||||
}
|
||||
progresses.clear();
|
||||
|
||||
main_progress.set_length(100);
|
||||
main_progress.set_position(100);
|
||||
main_progress.set_message("Expunging folders…");
|
||||
}
|
||||
SyncEvent::ExpungedAllFolders => {
|
||||
main_progress.finish_and_clear();
|
||||
}
|
||||
_ => {
|
||||
main_progress.tick();
|
||||
}
|
||||
};
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.sync()
|
||||
.await?;
|
||||
|
||||
let folders_patch_err = report
|
||||
.folder
|
||||
.patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !folders_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occurred while applying the folders patch:")?;
|
||||
folders_patch_err
|
||||
.iter()
|
||||
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
|
||||
}
|
||||
|
||||
let envelopes_patch_err = report
|
||||
.email
|
||||
.patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !envelopes_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occurred while applying the envelopes patch:")?;
|
||||
for (hunk, err) in envelopes_patch_err {
|
||||
printer.print_log(format!(" - {hunk}: {err}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
printer.print(format!("Account {account_name} successfully synchronized!"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccountSyncBackendBuilder {
|
||||
toml_account_config: Arc<TomlAccountConfig>,
|
||||
builder: BackendBuilder<BackendContextBuilder>,
|
||||
}
|
||||
|
||||
impl AccountSyncBackendBuilder {
|
||||
pub async fn new(
|
||||
toml_account_config: Arc<TomlAccountConfig>,
|
||||
account_config: Arc<AccountConfig>,
|
||||
) -> Result<Self> {
|
||||
#[allow(unused)]
|
||||
let used_backends = toml_account_config.get_used_backends();
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
let is_imap_used = used_backends.contains(&BackendKind::Imap);
|
||||
#[cfg(feature = "maildir")]
|
||||
let is_maildir_used = used_backends.contains(&BackendKind::Maildir);
|
||||
let is_maildir_for_sync_used = used_backends.contains(&BackendKind::MaildirForSync);
|
||||
#[cfg(feature = "notmuch")]
|
||||
let is_notmuch_used = used_backends.contains(&BackendKind::Notmuch);
|
||||
|
||||
let backend_ctx_builder = BackendContextBuilder {
|
||||
toml_account_config: toml_account_config.clone(),
|
||||
account_config: account_config.clone(),
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
imap: {
|
||||
let builder = toml_account_config
|
||||
.imap
|
||||
.as_ref()
|
||||
.filter(|_| is_imap_used)
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|config| {
|
||||
ImapContextBuilder::new(account_config.clone(), config)
|
||||
.with_prebuilt_credentials()
|
||||
});
|
||||
match builder {
|
||||
Some(builder) => Some(builder.await?),
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
maildir: toml_account_config
|
||||
.maildir
|
||||
.as_ref()
|
||||
.filter(|_| is_maildir_used)
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|mdir_config| MaildirContextBuilder::new(account_config.clone(), mdir_config)),
|
||||
|
||||
maildir_for_sync: Some(MaildirConfig {
|
||||
root_dir: account_config.get_sync_dir()?,
|
||||
})
|
||||
.filter(|_| is_maildir_for_sync_used)
|
||||
.map(Arc::new)
|
||||
.map(|mdir_config| MaildirContextBuilder::new(account_config.clone(), mdir_config)),
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
notmuch: toml_account_config
|
||||
.notmuch
|
||||
.as_ref()
|
||||
.filter(|_| is_notmuch_used)
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|notmuch_config| {
|
||||
NotmuchContextBuilder::new(account_config.clone(), notmuch_config)
|
||||
}),
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
smtp: None,
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
sendmail: None,
|
||||
};
|
||||
|
||||
let backend_builder = BackendBuilder::new(account_config.clone(), backend_ctx_builder);
|
||||
|
||||
Ok(Self {
|
||||
toml_account_config,
|
||||
builder: backend_builder,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn build(self) -> Result<Backend> {
|
||||
Ok(Backend {
|
||||
toml_account_config: self.toml_account_config,
|
||||
backend: self.builder.build().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AccountSyncBackendBuilder {
|
||||
type Target = BackendBuilder<BackendContextBuilder>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.builder
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountSyncBackendBuilder> for BackendBuilder<BackendContextBuilder> {
|
||||
fn from(backend_builder: AccountSyncBackendBuilder) -> Self {
|
||||
backend_builder.builder
|
||||
}
|
||||
}
|
|
@ -1,3 +1,232 @@
|
|||
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
|
||||
//! Deserialized account config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of an
|
||||
//! account in the accounts section of the user configuration file.
|
||||
|
||||
pub type TomlAccountConfig = HimalayaTomlAccountConfig;
|
||||
#[cfg(feature = "pgp")]
|
||||
use email::account::config::pgp::PgpConfig;
|
||||
#[cfg(feature = "account-sync")]
|
||||
use email::account::sync::config::SyncConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapConfig;
|
||||
#[cfg(feature = "maildir")]
|
||||
use email::maildir::config::MaildirConfig;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::config::NotmuchConfig;
|
||||
#[cfg(feature = "sendmail")]
|
||||
use email::sendmail::config::SendmailConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig,
|
||||
folder::config::FolderConfig, message::config::MessageConfig,
|
||||
};
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct TomlAccountConfig {
|
||||
pub default: Option<bool>,
|
||||
pub email: String,
|
||||
pub display_name: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub sync: Option<SyncConfig>,
|
||||
#[cfg(feature = "pgp")]
|
||||
pub pgp: Option<PgpConfig>,
|
||||
|
||||
pub folder: Option<FolderConfig>,
|
||||
pub envelope: Option<EnvelopeConfig>,
|
||||
pub flag: Option<FlagConfig>,
|
||||
pub message: Option<MessageConfig>,
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
pub imap: Option<ImapConfig>,
|
||||
#[cfg(feature = "maildir")]
|
||||
pub maildir: Option<MaildirConfig>,
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub notmuch: Option<NotmuchConfig>,
|
||||
#[cfg(feature = "smtp")]
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub sendmail: Option<SendmailConfig>,
|
||||
}
|
||||
|
||||
impl TomlAccountConfig {
|
||||
pub fn add_folder_kind(&self) -> Option<&BackendKind> {
|
||||
self.folder
|
||||
.as_ref()
|
||||
.and_then(|folder| folder.add.as_ref())
|
||||
.and_then(|add| add.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn list_folders_kind(&self) -> Option<&BackendKind> {
|
||||
self.folder
|
||||
.as_ref()
|
||||
.and_then(|folder| folder.list.as_ref())
|
||||
.and_then(|list| list.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn expunge_folder_kind(&self) -> Option<&BackendKind> {
|
||||
self.folder
|
||||
.as_ref()
|
||||
.and_then(|folder| folder.expunge.as_ref())
|
||||
.and_then(|expunge| expunge.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn purge_folder_kind(&self) -> Option<&BackendKind> {
|
||||
self.folder
|
||||
.as_ref()
|
||||
.and_then(|folder| folder.purge.as_ref())
|
||||
.and_then(|purge| purge.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn delete_folder_kind(&self) -> Option<&BackendKind> {
|
||||
self.folder
|
||||
.as_ref()
|
||||
.and_then(|folder| folder.delete.as_ref())
|
||||
.and_then(|delete| delete.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_envelope_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
.and_then(|envelope| envelope.get.as_ref())
|
||||
.and_then(|get| get.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn list_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
.and_then(|envelope| envelope.list.as_ref())
|
||||
.and_then(|list| list.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
.and_then(|envelope| envelope.watch.as_ref())
|
||||
.and_then(|watch| watch.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn add_flags_kind(&self) -> Option<&BackendKind> {
|
||||
self.flag
|
||||
.as_ref()
|
||||
.and_then(|flag| flag.add.as_ref())
|
||||
.and_then(|add| add.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn set_flags_kind(&self) -> Option<&BackendKind> {
|
||||
self.flag
|
||||
.as_ref()
|
||||
.and_then(|flag| flag.set.as_ref())
|
||||
.and_then(|set| set.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn remove_flags_kind(&self) -> Option<&BackendKind> {
|
||||
self.flag
|
||||
.as_ref()
|
||||
.and_then(|flag| flag.remove.as_ref())
|
||||
.and_then(|remove| remove.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn add_message_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|msg| msg.write.as_ref())
|
||||
.and_then(|add| add.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn peek_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|message| message.peek.as_ref())
|
||||
.and_then(|peek| peek.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|message| message.read.as_ref())
|
||||
.and_then(|get| get.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn copy_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|message| message.copy.as_ref())
|
||||
.and_then(|copy| copy.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn move_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|message| message.r#move.as_ref())
|
||||
.and_then(|move_| move_.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn delete_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|message| message.delete.as_ref())
|
||||
.and_then(|delete| delete.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn send_message_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|msg| msg.send.as_ref())
|
||||
.and_then(|send| send.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut used_backends = HashSet::default();
|
||||
|
||||
if let Some(ref kind) = self.backend {
|
||||
used_backends.insert(kind);
|
||||
}
|
||||
|
||||
if let Some(ref folder) = self.folder {
|
||||
used_backends.extend(folder.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(ref envelope) = self.envelope {
|
||||
used_backends.extend(envelope.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(ref flag) = self.flag {
|
||||
used_backends.extend(flag.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(ref msg) = self.message {
|
||||
used_backends.extend(msg.get_used_backends());
|
||||
}
|
||||
|
||||
used_backends
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,135 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::{collections::hash_map::Iter, fmt, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::table::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use self::config::TomlAccountConfig;
|
||||
|
||||
/// Represents the printable account.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Account {
|
||||
/// Represents the account name.
|
||||
pub name: String,
|
||||
/// Represents the backend name of the account.
|
||||
pub backend: String,
|
||||
/// Represents the default state of the account.
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
backend: backend.into(),
|
||||
default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Account {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Account {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("BACKENDS").bold().underline().white())
|
||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let default = if self.default { "yes" } else { "" };
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).shrinkable().green())
|
||||
.cell(Cell::new(&self.backend).blue())
|
||||
.cell(Cell::new(default).white())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of printable accounts.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Accounts(pub Vec<Account>);
|
||||
|
||||
impl Deref for Accounts {
|
||||
type Target = Vec<Account>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Accounts {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
|
||||
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
|
||||
let mut accounts: Vec<_> = map
|
||||
.map(|(name, account)| {
|
||||
#[allow(unused_mut)]
|
||||
let mut backends = String::new();
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if account.imap.is_some() {
|
||||
backends.push_str("imap");
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
if account.maildir.is_some() {
|
||||
if !backends.is_empty() {
|
||||
backends.push_str(", ")
|
||||
}
|
||||
backends.push_str("maildir");
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
if account.imap.is_some() {
|
||||
if !backends.is_empty() {
|
||||
backends.push_str(", ")
|
||||
}
|
||||
backends.push_str("notmuch");
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if account.smtp.is_some() {
|
||||
if !backends.is_empty() {
|
||||
backends.push_str(", ")
|
||||
}
|
||||
backends.push_str("smtp");
|
||||
}
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
if account.sendmail.is_some() {
|
||||
if !backends.is_empty() {
|
||||
backends.push_str(", ")
|
||||
}
|
||||
backends.push_str("sendmail");
|
||||
}
|
||||
|
||||
Account::new(name, &backends, account.default.unwrap_or_default())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// sort accounts by name
|
||||
accounts.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
|
||||
|
||||
Self(accounts)
|
||||
}
|
||||
}
|
||||
|
|
161
src/account/wizard.rs
Normal file
161
src/account/wizard.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use anyhow::{bail, Result};
|
||||
#[cfg(feature = "account-sync")]
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::Input;
|
||||
#[cfg(feature = "account-sync")]
|
||||
use email::account::sync::config::SyncConfig;
|
||||
use email_address::EmailAddress;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::wizard_prompt;
|
||||
#[cfg(feature = "account-discovery")]
|
||||
use crate::wizard_warn;
|
||||
use crate::{
|
||||
backend::{self, config::BackendConfig, BackendKind},
|
||||
message::config::{MessageConfig, MessageSendConfig},
|
||||
ui::THEME,
|
||||
};
|
||||
|
||||
use super::TomlAccountConfig;
|
||||
|
||||
pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||
let mut config = TomlAccountConfig::default();
|
||||
|
||||
config.email = Input::with_theme(&*THEME)
|
||||
.with_prompt("Email address")
|
||||
.validate_with(|email: &String| {
|
||||
if EmailAddress::is_valid(email) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Invalid email address: {email}")
|
||||
}
|
||||
})
|
||||
.interact()?;
|
||||
|
||||
let addr = EmailAddress::from_str(&config.email).unwrap();
|
||||
|
||||
#[cfg(feature = "account-discovery")]
|
||||
let autoconfig_email = config.email.to_owned();
|
||||
#[cfg(feature = "account-discovery")]
|
||||
let autoconfig = tokio::spawn(async move {
|
||||
email::account::discover::from_addr(&autoconfig_email)
|
||||
.await
|
||||
.ok()
|
||||
});
|
||||
|
||||
let account_name = Input::with_theme(&*THEME)
|
||||
.with_prompt("Account name")
|
||||
.default(addr.domain().split_once('.').unwrap().0.to_owned())
|
||||
.interact()?;
|
||||
|
||||
config.display_name = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Full display name")
|
||||
.default(addr.local_part().to_owned())
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
config.downloads_dir = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Downloads directory")
|
||||
.default(String::from("~/Downloads"))
|
||||
.interact()?
|
||||
.into(),
|
||||
);
|
||||
|
||||
let email = &config.email;
|
||||
#[cfg(feature = "account-discovery")]
|
||||
let autoconfig = autoconfig.await?;
|
||||
#[cfg(feature = "account-discovery")]
|
||||
let autoconfig = autoconfig.as_ref();
|
||||
|
||||
#[cfg(feature = "account-discovery")]
|
||||
if let Some(config) = autoconfig {
|
||||
if config.is_gmail() {
|
||||
println!();
|
||||
wizard_warn!("Warning: Google passwords cannot be used directly, see:");
|
||||
wizard_warn!("https://pimalaya.org/himalaya/cli/latest/configuration/gmail.html");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
match backend::wizard::configure(
|
||||
&account_name,
|
||||
email,
|
||||
#[cfg(feature = "account-discovery")]
|
||||
autoconfig,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendConfig::Imap(imap_config)) => {
|
||||
config.imap = Some(imap_config);
|
||||
config.backend = Some(BackendKind::Imap);
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendConfig::Maildir(mdir_config)) => {
|
||||
config.maildir = Some(mdir_config);
|
||||
config.backend = Some(BackendKind::Maildir);
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendConfig::Notmuch(notmuch_config)) => {
|
||||
config.notmuch = Some(notmuch_config);
|
||||
config.backend = Some(BackendKind::Notmuch);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
match backend::wizard::configure_sender(
|
||||
&account_name,
|
||||
email,
|
||||
#[cfg(feature = "account-discovery")]
|
||||
autoconfig,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(BackendConfig::Smtp(smtp_config)) => {
|
||||
config.smtp = Some(smtp_config);
|
||||
config.message = Some(MessageConfig {
|
||||
send: Some(MessageSendConfig {
|
||||
backend: Some(BackendKind::Smtp),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "sendmail")]
|
||||
Some(BackendConfig::Sendmail(sendmail_config)) => {
|
||||
config.sendmail = Some(sendmail_config);
|
||||
config.message = Some(MessageConfig {
|
||||
send: Some(MessageSendConfig {
|
||||
backend: Some(BackendKind::Sendmail),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
{
|
||||
let should_configure_sync = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Do you need an offline access to your account?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if should_configure_sync {
|
||||
config.sync = Some(SyncConfig {
|
||||
enable: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some((account_name, config)))
|
||||
}
|
24
src/backend/config.rs
Normal file
24
src/backend/config.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapConfig;
|
||||
#[cfg(feature = "maildir")]
|
||||
use email::maildir::config::MaildirConfig;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::config::NotmuchConfig;
|
||||
#[cfg(feature = "sendmail")]
|
||||
use email::sendmail::config::SendmailConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpConfig;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum BackendConfig {
|
||||
#[cfg(feature = "imap")]
|
||||
Imap(ImapConfig),
|
||||
#[cfg(feature = "maildir")]
|
||||
Maildir(MaildirConfig),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch(NotmuchConfig),
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp(SmtpConfig),
|
||||
#[cfg(feature = "sendmail")]
|
||||
Sendmail(SendmailConfig),
|
||||
}
|
763
src/backend/mod.rs
Normal file
763
src/backend/mod.rs
Normal file
|
@ -0,0 +1,763 @@
|
|||
pub mod config;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::{ImapContextBuilder, ImapContextSync};
|
||||
#[cfg(feature = "account-sync")]
|
||||
use email::maildir::config::MaildirConfig;
|
||||
#[cfg(any(feature = "account-sync", feature = "maildir"))]
|
||||
use email::maildir::{MaildirContextBuilder, MaildirContextSync};
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::{NotmuchContextBuilder, NotmuchContextSync};
|
||||
#[cfg(feature = "sendmail")]
|
||||
use email::sendmail::{SendmailContextBuilder, SendmailContextSync};
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::{SmtpContextBuilder, SmtpContextSync};
|
||||
use email::{
|
||||
account::config::AccountConfig,
|
||||
backend::{
|
||||
feature::BackendFeature, macros::BackendContext, mapper::SomeBackendContextBuilderMapper,
|
||||
},
|
||||
envelope::{get::GetEnvelope, list::ListEnvelopes, watch::WatchEnvelopes, Id, SingleId},
|
||||
flag::{add::AddFlags, remove::RemoveFlags, set::SetFlags, Flag, Flags},
|
||||
folder::{
|
||||
add::AddFolder, delete::DeleteFolder, expunge::ExpungeFolder, list::ListFolders,
|
||||
purge::PurgeFolder,
|
||||
},
|
||||
message::{
|
||||
add::AddMessage, copy::CopyMessages, delete::DeleteMessages, get::GetMessages,
|
||||
peek::PeekMessages, r#move::MoveMessages, send::SendMessage, Messages,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BackendKind {
|
||||
#[cfg(feature = "imap")]
|
||||
Imap,
|
||||
#[cfg(feature = "maildir")]
|
||||
Maildir,
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[serde(skip_deserializing)]
|
||||
MaildirForSync,
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch,
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp,
|
||||
#[cfg(feature = "sendmail")]
|
||||
Sendmail,
|
||||
None,
|
||||
}
|
||||
|
||||
impl ToString for BackendKind {
|
||||
fn to_string(&self) -> String {
|
||||
let kind = match self {
|
||||
#[cfg(feature = "imap")]
|
||||
Self::Imap => "IMAP",
|
||||
#[cfg(feature = "maildir")]
|
||||
Self::Maildir => "Maildir",
|
||||
#[cfg(feature = "account-sync")]
|
||||
Self::MaildirForSync => "Maildir",
|
||||
#[cfg(feature = "notmuch")]
|
||||
Self::Notmuch => "Notmuch",
|
||||
#[cfg(feature = "smtp")]
|
||||
Self::Smtp => "SMTP",
|
||||
#[cfg(feature = "sendmail")]
|
||||
Self::Sendmail => "Sendmail",
|
||||
Self::None => "None",
|
||||
};
|
||||
|
||||
kind.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BackendContextBuilder {
|
||||
pub toml_account_config: Arc<TomlAccountConfig>,
|
||||
pub account_config: Arc<AccountConfig>,
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
pub imap: Option<ImapContextBuilder>,
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
pub maildir: Option<MaildirContextBuilder>,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub maildir_for_sync: Option<MaildirContextBuilder>,
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub notmuch: Option<NotmuchContextBuilder>,
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
pub smtp: Option<SmtpContextBuilder>,
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub sendmail: Option<SendmailContextBuilder>,
|
||||
}
|
||||
|
||||
impl BackendContextBuilder {
|
||||
pub async fn new(
|
||||
toml_account_config: Arc<TomlAccountConfig>,
|
||||
account_config: Arc<AccountConfig>,
|
||||
kinds: Vec<&BackendKind>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
toml_account_config: toml_account_config.clone(),
|
||||
account_config: account_config.clone(),
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
imap: {
|
||||
let builder = toml_account_config
|
||||
.imap
|
||||
.as_ref()
|
||||
.filter(|_| kinds.contains(&&BackendKind::Imap))
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|imap_config| {
|
||||
ImapContextBuilder::new(account_config.clone(), imap_config)
|
||||
.with_prebuilt_credentials()
|
||||
});
|
||||
match builder {
|
||||
Some(builder) => Some(builder.await?),
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
maildir: toml_account_config
|
||||
.maildir
|
||||
.as_ref()
|
||||
.filter(|_| kinds.contains(&&BackendKind::Maildir))
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|mdir_config| MaildirContextBuilder::new(account_config.clone(), mdir_config)),
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
maildir_for_sync: Some(MaildirConfig {
|
||||
root_dir: account_config.get_sync_dir()?,
|
||||
})
|
||||
.filter(|_| kinds.contains(&&BackendKind::MaildirForSync))
|
||||
.map(Arc::new)
|
||||
.map(|mdir_config| MaildirContextBuilder::new(account_config.clone(), mdir_config)),
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
notmuch: toml_account_config
|
||||
.notmuch
|
||||
.as_ref()
|
||||
.filter(|_| kinds.contains(&&BackendKind::Notmuch))
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|notmuch_config| {
|
||||
NotmuchContextBuilder::new(account_config.clone(), notmuch_config)
|
||||
}),
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
smtp: toml_account_config
|
||||
.smtp
|
||||
.as_ref()
|
||||
.filter(|_| kinds.contains(&&BackendKind::Smtp))
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|smtp_config| SmtpContextBuilder::new(account_config.clone(), smtp_config)),
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
sendmail: toml_account_config
|
||||
.sendmail
|
||||
.as_ref()
|
||||
.filter(|_| kinds.contains(&&BackendKind::Sendmail))
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|sendmail_config| {
|
||||
SendmailContextBuilder::new(account_config.clone(), sendmail_config)
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
|
||||
type Context = BackendContext;
|
||||
|
||||
fn add_folder(&self) -> Option<BackendFeature<Self::Context, dyn AddFolder>> {
|
||||
match self.toml_account_config.add_folder_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.add_folder_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.add_folder_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.add_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.add_folder_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_folders(&self) -> Option<BackendFeature<Self::Context, dyn ListFolders>> {
|
||||
match self.toml_account_config.list_folders_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.list_folders_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.list_folders_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.list_folders()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.list_folders_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn expunge_folder(&self) -> Option<BackendFeature<Self::Context, dyn ExpungeFolder>> {
|
||||
match self.toml_account_config.expunge_folder_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.expunge_folder_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.expunge_folder_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.expunge_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.expunge_folder_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn purge_folder(&self) -> Option<BackendFeature<Self::Context, dyn PurgeFolder>> {
|
||||
match self.toml_account_config.purge_folder_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.purge_folder_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.purge_folder_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.purge_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.purge_folder_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_folder(&self) -> Option<BackendFeature<Self::Context, dyn DeleteFolder>> {
|
||||
match self.toml_account_config.delete_folder_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.delete_folder_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.delete_folder_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.delete_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.delete_folder_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_envelope(&self) -> Option<BackendFeature<Self::Context, dyn GetEnvelope>> {
|
||||
match self.toml_account_config.get_envelope_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.get_envelope_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.get_envelope_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.get_envelope()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.get_envelope_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn ListEnvelopes>> {
|
||||
match self.toml_account_config.list_envelopes_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.list_envelopes_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.list_envelopes_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.list_envelopes()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.list_envelopes_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
|
||||
match self.toml_account_config.watch_envelopes_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.watch_envelopes_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.watch_envelopes_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.watch_envelopes()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.watch_envelopes_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_flags(&self) -> Option<BackendFeature<Self::Context, dyn AddFlags>> {
|
||||
match self.toml_account_config.add_flags_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.add_flags_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.add_flags_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.add_flags()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.add_flags_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_flags(&self) -> Option<BackendFeature<Self::Context, dyn SetFlags>> {
|
||||
match self.toml_account_config.set_flags_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.set_flags_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.set_flags_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.set_flags()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.set_flags_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_flags(&self) -> Option<BackendFeature<Self::Context, dyn RemoveFlags>> {
|
||||
match self.toml_account_config.remove_flags_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.remove_flags_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.remove_flags_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.remove_flags()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.remove_flags_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_message(&self) -> Option<BackendFeature<Self::Context, dyn AddMessage>> {
|
||||
match self.toml_account_config.add_message_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.add_message_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.add_message_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.add_message()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.add_message_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_message(&self) -> Option<BackendFeature<Self::Context, dyn SendMessage>> {
|
||||
match self.toml_account_config.send_message_kind() {
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(BackendKind::Smtp) => self.send_message_with_some(&self.smtp),
|
||||
#[cfg(feature = "sendmail")]
|
||||
Some(BackendKind::Sendmail) => self.send_message_with_some(&self.sendmail),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_messages(&self) -> Option<BackendFeature<Self::Context, dyn PeekMessages>> {
|
||||
match self.toml_account_config.peek_messages_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.peek_messages_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.peek_messages_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.peek_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.peek_messages_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_messages(&self) -> Option<BackendFeature<Self::Context, dyn GetMessages>> {
|
||||
match self.toml_account_config.get_messages_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.get_messages_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.get_messages_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.get_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.get_messages_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_messages(&self) -> Option<BackendFeature<Self::Context, dyn CopyMessages>> {
|
||||
match self.toml_account_config.copy_messages_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.copy_messages_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.copy_messages_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.copy_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.copy_messages_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn move_messages(&self) -> Option<BackendFeature<Self::Context, dyn MoveMessages>> {
|
||||
match self.toml_account_config.move_messages_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.move_messages_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.move_messages_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.move_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.move_messages_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_messages(&self) -> Option<BackendFeature<Self::Context, dyn DeleteMessages>> {
|
||||
match self.toml_account_config.delete_messages_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => self.delete_messages_with_some(&self.imap),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.delete_messages_with_some(&self.maildir),
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
let f = self.maildir_for_sync.as_ref()?.delete_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.maildir_for_sync.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.delete_messages_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn build(self) -> Result<Self::Context> {
|
||||
let mut ctx = BackendContext::default();
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap) = self.imap {
|
||||
ctx.imap = Some(imap.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
if let Some(maildir) = self.maildir {
|
||||
ctx.maildir = Some(maildir.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
if let Some(maildir) = self.maildir_for_sync {
|
||||
ctx.maildir_for_sync = Some(maildir.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
if let Some(notmuch) = self.notmuch {
|
||||
ctx.notmuch = Some(notmuch.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(smtp) = self.smtp {
|
||||
ctx.smtp = Some(smtp.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
if let Some(sendmail) = self.sendmail {
|
||||
ctx.sendmail = Some(sendmail.build().await?);
|
||||
}
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BackendContext, Default)]
|
||||
pub struct BackendContext {
|
||||
#[cfg(feature = "imap")]
|
||||
pub imap: Option<ImapContextSync>,
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
pub maildir: Option<MaildirContextSync>,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub maildir_for_sync: Option<MaildirContextSync>,
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub notmuch: Option<NotmuchContextSync>,
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
pub smtp: Option<SmtpContextSync>,
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub sendmail: Option<SendmailContextSync>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
impl AsRef<Option<ImapContextSync>> for BackendContext {
|
||||
fn as_ref(&self) -> &Option<ImapContextSync> {
|
||||
&self.imap
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
impl AsRef<Option<MaildirContextSync>> for BackendContext {
|
||||
fn as_ref(&self) -> &Option<MaildirContextSync> {
|
||||
&self.maildir
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
impl AsRef<Option<NotmuchContextSync>> for BackendContext {
|
||||
fn as_ref(&self) -> &Option<NotmuchContextSync> {
|
||||
&self.notmuch
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
impl AsRef<Option<SmtpContextSync>> for BackendContext {
|
||||
fn as_ref(&self) -> &Option<SmtpContextSync> {
|
||||
&self.smtp
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
impl AsRef<Option<SendmailContextSync>> for BackendContext {
|
||||
fn as_ref(&self) -> &Option<SendmailContextSync> {
|
||||
&self.sendmail
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Backend {
|
||||
pub toml_account_config: Arc<TomlAccountConfig>,
|
||||
pub backend: email::backend::Backend<BackendContext>,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub async fn new(
|
||||
toml_account_config: Arc<TomlAccountConfig>,
|
||||
account_config: Arc<AccountConfig>,
|
||||
backend_kinds: impl IntoIterator<Item = &BackendKind>,
|
||||
with_features: impl Fn(&mut email::backend::BackendBuilder<BackendContextBuilder>),
|
||||
) -> Result<Self> {
|
||||
let backend_kinds = backend_kinds.into_iter().collect();
|
||||
let backend_ctx_builder = BackendContextBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
backend_kinds,
|
||||
)
|
||||
.await?;
|
||||
let mut backend_builder =
|
||||
email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder)
|
||||
.without_features();
|
||||
|
||||
with_features(&mut backend_builder);
|
||||
|
||||
Ok(Self {
|
||||
toml_account_config: toml_account_config.clone(),
|
||||
backend: backend_builder.build().await?,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_id_mapper(
|
||||
&self,
|
||||
folder: &str,
|
||||
backend_kind: Option<&BackendKind>,
|
||||
) -> Result<IdMapper> {
|
||||
#[allow(unused_mut)]
|
||||
let mut id_mapper = IdMapper::Dummy;
|
||||
|
||||
match backend_kind {
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => {
|
||||
if let Some(_) = &self.toml_account_config.maildir {
|
||||
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
if let Some(_) = &self.toml_account_config.notmuch {
|
||||
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
Ok(id_mapper)
|
||||
}
|
||||
|
||||
pub async fn list_envelopes(
|
||||
&self,
|
||||
folder: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Envelopes> {
|
||||
let backend_kind = self.toml_account_config.list_envelopes_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let envelopes = self.backend.list_envelopes(folder, page_size, page).await?;
|
||||
let envelopes = Envelopes::from_backend(&self.account_config, &id_mapper, envelopes)?;
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.add_flags(folder, &ids, flags).await
|
||||
}
|
||||
|
||||
pub async fn add_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.add_flag(folder, &ids, flag).await
|
||||
}
|
||||
|
||||
pub async fn set_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.set_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.set_flags(folder, &ids, flags).await
|
||||
}
|
||||
|
||||
pub async fn set_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.set_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.set_flag(folder, &ids, flag).await
|
||||
}
|
||||
|
||||
pub async fn remove_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.remove_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.remove_flags(folder, &ids, flags).await
|
||||
}
|
||||
|
||||
pub async fn remove_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.remove_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.remove_flag(folder, &ids, flag).await
|
||||
}
|
||||
|
||||
pub async fn add_message(&self, folder: &str, email: &[u8]) -> Result<SingleId> {
|
||||
let backend_kind = self.toml_account_config.add_message_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let id = self.backend.add_message(folder, email).await?;
|
||||
id_mapper.create_alias(&*id)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn peek_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
|
||||
let backend_kind = self.toml_account_config.get_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.peek_messages(folder, &ids).await
|
||||
}
|
||||
|
||||
pub async fn get_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
|
||||
let backend_kind = self.toml_account_config.get_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.get_messages(folder, &ids).await
|
||||
}
|
||||
|
||||
pub async fn copy_messages(
|
||||
&self,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: &[usize],
|
||||
) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.move_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend
|
||||
.copy_messages(from_folder, to_folder, &ids)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn move_messages(
|
||||
&self,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: &[usize],
|
||||
) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.move_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend
|
||||
.move_messages(from_folder, to_folder, &ids)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_messages(&self, folder: &str, ids: &[usize]) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.delete_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.delete_messages(folder, &ids).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Backend {
|
||||
type Target = email::backend::Backend<BackendContext>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.backend
|
||||
}
|
||||
}
|
98
src/backend/wizard.rs
Normal file
98
src/backend/wizard.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Select;
|
||||
#[cfg(feature = "account-discovery")]
|
||||
use email::account::discover::config::AutoConfig;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use crate::imap;
|
||||
#[cfg(feature = "maildir")]
|
||||
use crate::maildir;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use crate::notmuch;
|
||||
#[cfg(feature = "sendmail")]
|
||||
use crate::sendmail;
|
||||
#[cfg(feature = "smtp")]
|
||||
use crate::smtp;
|
||||
use crate::ui::THEME;
|
||||
|
||||
use super::{config::BackendConfig, BackendKind};
|
||||
|
||||
const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
|
||||
#[cfg(feature = "imap")]
|
||||
BackendKind::Imap,
|
||||
#[cfg(feature = "maildir")]
|
||||
BackendKind::Maildir,
|
||||
#[cfg(feature = "notmuch")]
|
||||
BackendKind::Notmuch,
|
||||
];
|
||||
|
||||
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
|
||||
#[cfg(feature = "smtp")]
|
||||
BackendKind::Smtp,
|
||||
#[cfg(feature = "sendmail")]
|
||||
BackendKind::Sendmail,
|
||||
];
|
||||
|
||||
pub(crate) async fn configure(
|
||||
account_name: &str,
|
||||
email: &str,
|
||||
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
|
||||
) -> Result<Option<BackendConfig>> {
|
||||
let kind = Select::with_theme(&*THEME)
|
||||
.with_prompt("Default email backend")
|
||||
.items(DEFAULT_BACKEND_KINDS)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));
|
||||
|
||||
let config = match kind {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(kind) if kind == BackendKind::Imap => Some(
|
||||
imap::wizard::configure(
|
||||
account_name,
|
||||
email,
|
||||
#[cfg(feature = "account-discovery")]
|
||||
autoconfig,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(crate) async fn configure_sender(
|
||||
account_name: &str,
|
||||
email: &str,
|
||||
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
|
||||
) -> Result<Option<BackendConfig>> {
|
||||
let kind = Select::with_theme(&*THEME)
|
||||
.with_prompt("Backend for sending messages")
|
||||
.items(SEND_MESSAGE_BACKEND_KINDS)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone));
|
||||
|
||||
let config = match kind {
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(kind) if kind == BackendKind::Smtp => Some(
|
||||
smtp::wizard::configure(
|
||||
account_name,
|
||||
email,
|
||||
#[cfg(feature = "account-discovery")]
|
||||
autoconfig,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
#[cfg(feature = "sendmail")]
|
||||
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
15
src/cache/arg/disable.rs
vendored
Normal file
15
src/cache/arg/disable.rs
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
use clap::Parser;
|
||||
|
||||
/// The disable cache flag parser.
|
||||
#[derive(Debug, Default, Parser)]
|
||||
pub struct CacheDisableFlag {
|
||||
/// Disable any sort of cache.
|
||||
///
|
||||
/// The action depends on commands it apply on. For example, when
|
||||
/// listing envelopes using the IMAP backend, this flag will
|
||||
/// ensure that envelopes are fetched from the IMAP server rather
|
||||
/// than the synchronized local Maildir.
|
||||
#[arg(long = "disable-cache", alias = "no-cache", global = true)]
|
||||
#[arg(name = "cache_disable")]
|
||||
pub disable: bool,
|
||||
}
|
1
src/cache/arg/mod.rs
vendored
Normal file
1
src/cache/arg/mod.rs
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod disable;
|
29
src/cache/args.rs
vendored
Normal file
29
src/cache/args.rs
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
//! This module provides arguments related to the cache.
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches};
|
||||
|
||||
const ARG_DISABLE_CACHE: &str = "disable-cache";
|
||||
|
||||
/// Represents the disable cache flag argument. This argument allows
|
||||
/// the user to disable any sort of cache.
|
||||
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||
[Arg::new(ARG_DISABLE_CACHE)
|
||||
.help("Disable any sort of cache")
|
||||
.long_help(
|
||||
"Disable any sort of cache.
|
||||
|
||||
The action depends on commands it apply on. For example, when listing
|
||||
envelopes using the IMAP backend, this flag will ensure that envelopes
|
||||
are fetched from the IMAP server and not from the synchronized local
|
||||
Maildir.",
|
||||
)
|
||||
.long("disable-cache")
|
||||
.alias("no-cache")
|
||||
.global(true)
|
||||
.action(ArgAction::SetTrue)]
|
||||
}
|
||||
|
||||
/// Represents the disable cache flag parser.
|
||||
pub fn parse_disable_cache_arg(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DISABLE_CACHE)
|
||||
}
|
145
src/cache/mod.rs
vendored
Normal file
145
src/cache/mod.rs
vendored
Normal file
|
@ -0,0 +1,145 @@
|
|||
pub mod arg;
|
||||
pub mod args;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dirs::data_dir;
|
||||
use email::account::config::AccountConfig;
|
||||
use log::debug;
|
||||
use sled::{Config, Db};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IdMapper {
|
||||
Dummy,
|
||||
Mapper(Db),
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn new(account_config: &AccountConfig, folder: &str) -> Result<Self> {
|
||||
let digest = md5::compute(account_config.name.clone() + folder);
|
||||
let db_path = data_dir()
|
||||
.ok_or(anyhow!("cannot get XDG data directory"))?
|
||||
.join("himalaya")
|
||||
.join(".id-mappers")
|
||||
.join(format!("{digest:x}"));
|
||||
|
||||
let conn = Config::new()
|
||||
.path(&db_path)
|
||||
.idgen_persist_interval(1)
|
||||
.open()
|
||||
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
|
||||
|
||||
Ok(Self::Mapper(conn))
|
||||
}
|
||||
|
||||
pub fn create_alias<I>(&self, id: I) -> Result<String>
|
||||
where
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let id = id.as_ref();
|
||||
match self {
|
||||
Self::Dummy => Ok(id.to_owned()),
|
||||
Self::Mapper(conn) => {
|
||||
debug!("creating alias for id {id}…");
|
||||
|
||||
let alias = conn
|
||||
.generate_id()
|
||||
.with_context(|| format!("cannot create alias for id {id}"))?
|
||||
.to_string();
|
||||
debug!("created alias {alias} for id {id}");
|
||||
|
||||
conn.insert(&id, alias.as_bytes())
|
||||
.with_context(|| format!("cannot insert alias {alias} for id {id}"))?;
|
||||
|
||||
Ok(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
|
||||
where
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let id = id.as_ref();
|
||||
match self {
|
||||
Self::Dummy => Ok(id.to_owned()),
|
||||
Self::Mapper(conn) => {
|
||||
debug!("getting alias for id {id}…");
|
||||
|
||||
let alias = conn
|
||||
.get(id)
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
|
||||
let alias = match alias {
|
||||
Some(alias) => {
|
||||
let alias = String::from_utf8_lossy(alias.as_ref());
|
||||
debug!("found alias {alias} for id {id}");
|
||||
alias.to_string()
|
||||
}
|
||||
None => {
|
||||
debug!("alias not found, creating it…");
|
||||
self.create_alias(id)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id<A>(&self, alias: A) -> Result<String>
|
||||
where
|
||||
A: ToString,
|
||||
{
|
||||
let alias = alias.to_string();
|
||||
|
||||
match self {
|
||||
Self::Dummy => Ok(alias.to_string()),
|
||||
Self::Mapper(conn) => {
|
||||
debug!("getting id from alias {alias}…");
|
||||
|
||||
let id = conn
|
||||
.iter()
|
||||
.flat_map(|entry| entry)
|
||||
.find_map(|(entry_id, entry_alias)| {
|
||||
if entry_alias.as_ref() == alias.as_bytes() {
|
||||
let entry_id = String::from_utf8_lossy(entry_id.as_ref());
|
||||
Some(entry_id.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?;
|
||||
debug!("found id {id} from alias {alias}");
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ids(&self, aliases: impl IntoIterator<Item = impl ToString>) -> Result<Vec<String>> {
|
||||
let aliases: Vec<String> = aliases.into_iter().map(|alias| alias.to_string()).collect();
|
||||
|
||||
match self {
|
||||
Self::Dummy => Ok(aliases),
|
||||
Self::Mapper(conn) => {
|
||||
let aliases: HashSet<&str> = aliases.iter().map(|alias| alias.as_str()).collect();
|
||||
let ids: Vec<String> = conn
|
||||
.iter()
|
||||
.flat_map(|entry| entry)
|
||||
.filter_map(|(entry_id, entry_alias)| {
|
||||
let alias = String::from_utf8_lossy(entry_alias.as_ref());
|
||||
if aliases.contains(alias.as_ref()) {
|
||||
let entry_id = String::from_utf8_lossy(entry_id.as_ref());
|
||||
Some(entry_id.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
98
src/cli.rs
98
src/cli.rs
|
@ -1,22 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
long_version,
|
||||
terminal::{
|
||||
cli::{
|
||||
arg::path_parser,
|
||||
printer::{OutputFmt, Printer},
|
||||
},
|
||||
config::TomlConfig as _,
|
||||
},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
account::command::AccountSubcommand,
|
||||
completion::command::CompletionGenerateCommand,
|
||||
config::TomlConfig,
|
||||
config::{self, TomlConfig},
|
||||
envelope::command::EnvelopeSubcommand,
|
||||
flag::command::FlagSubcommand,
|
||||
folder::command::FolderSubcommand,
|
||||
|
@ -25,30 +14,28 @@ use crate::{
|
|||
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
|
||||
template::command::TemplateSubcommand,
|
||||
},
|
||||
output::{ColorFmt, OutputFmt},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = env!("CARGO_PKG_NAME"))]
|
||||
#[command(author, version, about)]
|
||||
#[command(long_version = long_version!())]
|
||||
#[command(name = "himalaya", author, version, about)]
|
||||
#[command(propagate_version = true, infer_subcommands = true)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<HimalayaCommand>,
|
||||
|
||||
/// Override the default configuration file path.
|
||||
/// Override the default configuration file path
|
||||
///
|
||||
/// The given paths are shell-expanded then canonicalized (if
|
||||
/// applicable). If the first path does not point to a valid file,
|
||||
/// the wizard will propose to assist you in the creation of the
|
||||
/// configuration file. Other paths are merged with the first one,
|
||||
/// which allows you to separate your public config from your
|
||||
/// private(s) one(s).
|
||||
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
|
||||
#[arg(value_name = "PATH", value_parser = path_parser)]
|
||||
pub config_paths: Vec<PathBuf>,
|
||||
/// The given path is shell-expanded then canonicalized (if
|
||||
/// applicable). If the path does not point to a valid file, the
|
||||
/// wizard will propose to assist you in the creation of the
|
||||
/// configuration file.
|
||||
#[arg(short, long = "config", global = true)]
|
||||
#[arg(value_name = "PATH", value_parser = config::path_parser)]
|
||||
pub config_path: Option<PathBuf>,
|
||||
|
||||
/// Customize the output format.
|
||||
/// Customize the output format
|
||||
///
|
||||
/// The output format determine how to display commands output to
|
||||
/// the terminal.
|
||||
|
@ -63,19 +50,29 @@ pub struct Cli {
|
|||
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
|
||||
pub output: OutputFmt,
|
||||
|
||||
/// Enable logs with spantrace.
|
||||
/// Control when to use colors
|
||||
///
|
||||
/// This is the same as running the command with `RUST_LOG=debug`
|
||||
/// environment variable.
|
||||
#[arg(long, global = true, conflicts_with = "trace")]
|
||||
pub debug: bool,
|
||||
|
||||
/// Enable verbose logs with backtrace.
|
||||
/// 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.
|
||||
///
|
||||
/// This is the same as running the command with `RUST_LOG=trace`
|
||||
/// and `RUST_BACKTRACE=1` environment variables.
|
||||
#[arg(long, global = true, conflicts_with = "debug")]
|
||||
pub trace: bool,
|
||||
/// The possible values are:
|
||||
///
|
||||
/// - never: colors will never be used
|
||||
///
|
||||
/// - always: colors will always be used regardless of where output is sent
|
||||
///
|
||||
/// - ansi: like 'always', but emits ANSI escapes (even in a Windows console)
|
||||
///
|
||||
/// - auto: himalaya tries to be smart
|
||||
#[arg(long, short = 'C', global = true)]
|
||||
#[arg(value_name = "MODE", value_enum, default_value_t = Default::default())]
|
||||
pub color: ColorFmt,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
|
@ -119,34 +116,39 @@ pub enum HimalayaCommand {
|
|||
}
|
||||
|
||||
impl HimalayaCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::Account(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, config, config_paths.first()).await
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Folder(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Envelope(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Flag(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Message(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Attachment(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Template(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Manual(cmd) => cmd.execute(printer).await,
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
use std::io;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{value_parser, CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
use color_eyre::Result;
|
||||
use tracing::info;
|
||||
use log::info;
|
||||
use std::io;
|
||||
|
||||
use crate::cli::Cli;
|
||||
|
||||
/// Print completion script for the given shell to stdout.
|
||||
/// Print completion script for a shell to stdout.
|
||||
///
|
||||
/// This command allows you to generate completion script for a given
|
||||
/// shell. The script is printed to the standard output. If you want
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
use pimalaya_tui::himalaya::config::HimalayaTomlConfig;
|
||||
|
||||
pub type TomlConfig = HimalayaTomlConfig;
|
253
src/config/mod.rs
Normal file
253
src/config/mod.rs
Normal file
|
@ -0,0 +1,253 @@
|
|||
pub mod wizard;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dirs::{config_dir, home_dir};
|
||||
use email::{
|
||||
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
|
||||
flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use toml;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::backend::BackendKind;
|
||||
use crate::{account::config::TomlAccountConfig, wizard_prompt, wizard_warn};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TomlConfig {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
pub accounts: HashMap<String, TomlAccountConfig>,
|
||||
}
|
||||
|
||||
impl TomlConfig {
|
||||
/// Read and parse the TOML configuration at the given path.
|
||||
///
|
||||
/// Returns an error if the configuration file cannot be read or
|
||||
/// if its content cannot be parsed.
|
||||
fn from_path(path: &Path) -> Result<Self> {
|
||||
let content =
|
||||
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
|
||||
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
|
||||
}
|
||||
|
||||
/// Create and save a TOML configuration using the wizard.
|
||||
///
|
||||
/// If the user accepts the confirmation, the wizard starts and
|
||||
/// help him to create his configuration file. Otherwise the
|
||||
/// program stops.
|
||||
///
|
||||
/// NOTE: the wizard can only be used with interactive shells.
|
||||
async fn from_wizard(path: PathBuf) -> Result<Self> {
|
||||
use dialoguer::Confirm;
|
||||
use std::process;
|
||||
|
||||
wizard_warn!("Cannot find existing configuration at {path:?}.");
|
||||
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to create one with the wizard?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if !confirm {
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
wizard::configure(path).await
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration from default paths.
|
||||
pub async fn from_default_paths() -> Result<Self> {
|
||||
match Self::first_valid_default_path() {
|
||||
Some(path) => Self::from_path(&path),
|
||||
None => Self::from_wizard(Self::default_path()?).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration at the optional given
|
||||
/// path.
|
||||
///
|
||||
/// If the given path exists, then read and parse the TOML
|
||||
/// configuration from it.
|
||||
///
|
||||
/// If the given path does not exist, then create it using the
|
||||
/// wizard.
|
||||
///
|
||||
/// If no path is given, then either read and parse the TOML
|
||||
/// configuration at the first valid default path, otherwise
|
||||
/// create it using the wizard. wizard.
|
||||
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
|
||||
match path.map(Into::into) {
|
||||
Some(ref path) if path.exists() => Self::from_path(path),
|
||||
Some(path) => Self::from_wizard(path).await,
|
||||
_ => Self::from_default_paths().await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default configuration path.
|
||||
///
|
||||
/// Returns an error if the XDG configuration directory cannot be
|
||||
/// found.
|
||||
pub fn default_path() -> Result<PathBuf> {
|
||||
Ok(config_dir()
|
||||
.ok_or(anyhow!("cannot get XDG config directory"))?
|
||||
.join("himalaya")
|
||||
.join("config.toml"))
|
||||
}
|
||||
|
||||
/// Get the first default configuration path that points to a
|
||||
/// valid file.
|
||||
///
|
||||
/// Tries paths in this order:
|
||||
///
|
||||
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
|
||||
/// `$XDG_CONFIG_DIR` in other OSes.)
|
||||
/// - `$HOME/.config/himalaya/config.toml`
|
||||
/// - `$HOME/.himalayarc`
|
||||
pub fn first_valid_default_path() -> Option<PathBuf> {
|
||||
Self::default_path()
|
||||
.ok()
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
||||
.filter(|p| p.exists())
|
||||
}
|
||||
|
||||
pub fn into_toml_account_config(
|
||||
&self,
|
||||
account_name: Option<&str>,
|
||||
) -> Result<(String, TomlAccountConfig)> {
|
||||
#[allow(unused_mut)]
|
||||
let (account_name, mut toml_account_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(name, account)| {
|
||||
account
|
||||
.default
|
||||
.filter(|default| *default)
|
||||
.map(|_| (name.to_owned(), account.clone()))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
.ok_or_else(|| anyhow!("cannot find account {name}")),
|
||||
}?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap_config) = toml_account_config.imap.as_mut() {
|
||||
imap_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
|
||||
smtp_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
Ok((account_name, toml_account_config))
|
||||
}
|
||||
|
||||
/// Build account configurations from a given account name.
|
||||
pub fn into_account_configs(
|
||||
self,
|
||||
account_name: Option<&str>,
|
||||
#[cfg(feature = "account-sync")] disable_cache: bool,
|
||||
) -> Result<(Arc<TomlAccountConfig>, Arc<AccountConfig>)> {
|
||||
#[cfg_attr(not(feature = "account-sync"), allow(unused_mut))]
|
||||
let (account_name, mut toml_account_config) =
|
||||
self.into_toml_account_config(account_name)?;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
if let Some(true) = toml_account_config.sync.as_ref().and_then(|c| c.enable) {
|
||||
if !disable_cache {
|
||||
toml_account_config.backend = Some(BackendKind::MaildirForSync);
|
||||
}
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
display_name: self.display_name,
|
||||
signature: self.signature,
|
||||
signature_delim: self.signature_delim,
|
||||
downloads_dir: self.downloads_dir,
|
||||
|
||||
accounts: HashMap::from_iter(self.accounts.clone().into_iter().map(
|
||||
|(name, config)| {
|
||||
(
|
||||
name.clone(),
|
||||
AccountConfig {
|
||||
name,
|
||||
email: config.email,
|
||||
display_name: config.display_name,
|
||||
signature: config.signature,
|
||||
signature_delim: config.signature_delim,
|
||||
downloads_dir: config.downloads_dir,
|
||||
folder: config.folder.map(|c| FolderConfig {
|
||||
aliases: c.alias,
|
||||
list: c.list.map(|c| c.remote),
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: c.sync,
|
||||
}),
|
||||
envelope: config.envelope.map(|c| EnvelopeConfig {
|
||||
list: c.list.map(|c| c.remote),
|
||||
watch: c.watch.map(|c| c.remote),
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: c.sync,
|
||||
}),
|
||||
flag: config.flag.map(|c| FlagConfig {
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: c.sync,
|
||||
}),
|
||||
message: config.message.map(|c| MessageConfig {
|
||||
read: c.read.map(|c| c.remote),
|
||||
write: c.write.map(|c| c.remote),
|
||||
send: c.send.map(|c| c.remote),
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: c.sync,
|
||||
}),
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: config.sync,
|
||||
#[cfg(feature = "pgp")]
|
||||
pgp: config.pgp,
|
||||
},
|
||||
)
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
let account_config = config.account(account_name)?;
|
||||
|
||||
Ok((Arc::new(toml_account_config), Arc::new(account_config)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a configuration file path as [`PathBuf`].
|
||||
///
|
||||
/// The path is shell-expanded then canonicalized (if applicable).
|
||||
pub fn path_parser(path: &str) -> Result<PathBuf, String> {
|
||||
expand::try_path(path)
|
||||
.map(canonicalize::path)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
533
src/config/wizard.rs
Normal file
533
src/config/wizard.rs
Normal file
|
@ -0,0 +1,533 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use shellexpand_utils::expand;
|
||||
use std::{fs, path::PathBuf, process};
|
||||
use toml_edit::{Document, Item};
|
||||
|
||||
use crate::{account, ui::THEME};
|
||||
|
||||
use super::TomlConfig;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! wizard_warn {
|
||||
($($arg:tt)*) => {
|
||||
println!("{}", console::style(format!($($arg)*)).yellow().bold());
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! wizard_prompt {
|
||||
($($arg:tt)*) => {
|
||||
format!("{}", console::style(format!($($arg)*)).italic())
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! wizard_log {
|
||||
($($arg:tt)*) => {
|
||||
println!();
|
||||
println!("{}", console::style(format!($($arg)*)).underlined());
|
||||
println!();
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn configure(path: PathBuf) -> Result<TomlConfig> {
|
||||
wizard_log!("Configuring your first account:");
|
||||
|
||||
let mut config = TomlConfig::default();
|
||||
|
||||
while let Some((name, account_config)) = account::wizard::configure().await? {
|
||||
config.accounts.insert(name, account_config);
|
||||
|
||||
if !Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to configure another account?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
wizard_log!("Configuring another account:");
|
||||
}
|
||||
|
||||
// If one account is setup, make it the default. If multiple
|
||||
// accounts are setup, decide which will be the default. If no
|
||||
// accounts are setup, exit the process.
|
||||
let default_account = match config.accounts.len() {
|
||||
0 => {
|
||||
wizard_warn!("No account configured, exiting.");
|
||||
process::exit(0);
|
||||
}
|
||||
1 => Some(config.accounts.values_mut().next().unwrap()),
|
||||
_ => {
|
||||
let accounts = config.accounts.clone();
|
||||
let accounts: Vec<&String> = accounts.keys().collect();
|
||||
|
||||
println!("{} accounts have been configured.", accounts.len());
|
||||
|
||||
Select::with_theme(&*THEME)
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Which account would you like to set as your default?"
|
||||
))
|
||||
.items(&accounts)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
.and_then(|idx| config.accounts.get_mut(accounts[idx]))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(account) = default_account {
|
||||
account.default = Some(true);
|
||||
} else {
|
||||
process::exit(0)
|
||||
}
|
||||
|
||||
let path = Input::with_theme(&*THEME)
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Where would you like to save your configuration?"
|
||||
))
|
||||
.default(path.to_string_lossy().to_string())
|
||||
.interact()?;
|
||||
let path = expand::path(path);
|
||||
|
||||
println!("Writing the configuration to {path:?}…");
|
||||
let toml = pretty_serialize(&config)?;
|
||||
fs::create_dir_all(path.parent().unwrap_or(&path))?;
|
||||
fs::write(path, toml)?;
|
||||
|
||||
println!("Exiting the wizard…");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn pretty_serialize(config: &TomlConfig) -> Result<String> {
|
||||
let mut doc: Document = toml::to_string(&config)?.parse()?;
|
||||
|
||||
doc.iter_mut().for_each(|(_, item)| {
|
||||
if let Some(item) = item.as_table_mut() {
|
||||
item.iter_mut().for_each(|(_, item)| {
|
||||
set_table_dotted(item, "folder");
|
||||
if let Some(item) = get_table_mut(item, "folder") {
|
||||
let keys = ["alias", "add", "list", "expunge", "purge", "delete", "sync"];
|
||||
set_tables_dotted(item, keys);
|
||||
|
||||
if let Some(item) = get_table_mut(item, "sync") {
|
||||
set_tables_dotted(item, ["filter", "permissions"]);
|
||||
}
|
||||
}
|
||||
|
||||
set_table_dotted(item, "envelope");
|
||||
if let Some(item) = get_table_mut(item, "envelope") {
|
||||
set_tables_dotted(item, ["list", "get"]);
|
||||
}
|
||||
|
||||
set_table_dotted(item, "flag");
|
||||
if let Some(item) = get_table_mut(item, "flag") {
|
||||
set_tables_dotted(item, ["add", "set", "remove"]);
|
||||
}
|
||||
|
||||
set_table_dotted(item, "message");
|
||||
if let Some(item) = get_table_mut(item, "message") {
|
||||
let keys = ["add", "send", "peek", "get", "copy", "move", "delete"];
|
||||
set_tables_dotted(item, keys);
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
set_table_dotted(item, "maildir");
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
{
|
||||
set_table_dotted(item, "imap");
|
||||
if let Some(item) = get_table_mut(item, "imap") {
|
||||
set_tables_dotted(item, ["passwd", "oauth2"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
set_table_dotted(item, "notmuch");
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
{
|
||||
set_table_dotted(item, "smtp");
|
||||
if let Some(item) = get_table_mut(item, "smtp") {
|
||||
set_tables_dotted(item, ["passwd", "oauth2"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
set_table_dotted(item, "sendmail");
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
set_table_dotted(item, "sync");
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
set_table_dotted(item, "pgp");
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
Ok(doc.to_string())
|
||||
}
|
||||
|
||||
fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> {
|
||||
item.get_mut(key).filter(|item| item.is_table())
|
||||
}
|
||||
|
||||
fn set_table_dotted(item: &mut Item, key: &str) {
|
||||
if let Some(table) = get_table_mut(item, key).and_then(|item| item.as_table_mut()) {
|
||||
table.set_dotted(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a str>) {
|
||||
for key in keys {
|
||||
set_table_dotted(item, key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{account::config::TomlAccountConfig, config::TomlConfig};
|
||||
|
||||
use super::pretty_serialize;
|
||||
|
||||
fn assert_eq(config: TomlAccountConfig, expected_toml: &str) {
|
||||
let config = TomlConfig {
|
||||
accounts: HashMap::from_iter([("test".into(), config)]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let toml = pretty_serialize(&config).expect("serialize error");
|
||||
assert_eq!(toml, expected_toml);
|
||||
|
||||
let expected_config = toml::from_str(&toml).expect("deserialize error");
|
||||
assert_eq!(config, expected_config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_serialize_default() {
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[test]
|
||||
fn pretty_serialize_sync_all() {
|
||||
use email::account::sync::config::SyncConfig;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sync: Some(SyncConfig {
|
||||
enable: Some(false),
|
||||
dir: Some("/tmp/test".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
sync.enable = false
|
||||
sync.dir = "/tmp/test"
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[test]
|
||||
fn pretty_serialize_sync_include() {
|
||||
use email::{
|
||||
account::sync::config::SyncConfig,
|
||||
folder::sync::config::{FolderSyncConfig, FolderSyncStrategy},
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::folder::config::FolderConfig;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sync: Some(SyncConfig {
|
||||
enable: Some(true),
|
||||
dir: Some("/tmp/test".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
folder: Some(FolderConfig {
|
||||
sync: Some(FolderSyncConfig {
|
||||
filter: FolderSyncStrategy::Include(BTreeSet::from_iter(["test".into()])),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
sync.enable = true
|
||||
sync.dir = "/tmp/test"
|
||||
folder.sync.filter.include = ["test"]
|
||||
folder.sync.permissions.create = true
|
||||
folder.sync.permissions.delete = true
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
#[test]
|
||||
fn pretty_serialize_imap_passwd_cmd() {
|
||||
use email::{
|
||||
account::config::passwd::PasswdConfig,
|
||||
imap::config::{ImapAuthConfig, ImapConfig},
|
||||
};
|
||||
use secret::Secret;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
imap: Some(ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 143,
|
||||
login: "test@localhost".into(),
|
||||
auth: ImapAuthConfig::Passwd(PasswdConfig(Secret::new_cmd("pass show test"))),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
imap.host = "localhost"
|
||||
imap.port = 143
|
||||
imap.login = "test@localhost"
|
||||
imap.passwd.cmd = "pass show test"
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
#[test]
|
||||
fn pretty_serialize_imap_passwd_cmds() {
|
||||
use email::{
|
||||
account::config::passwd::PasswdConfig,
|
||||
imap::config::{ImapAuthConfig, ImapConfig},
|
||||
};
|
||||
use secret::Secret;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
imap: Some(ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 143,
|
||||
login: "test@localhost".into(),
|
||||
auth: ImapAuthConfig::Passwd(PasswdConfig(Secret::new_cmd(vec![
|
||||
"pass show test",
|
||||
"tr -d '[:blank:]'",
|
||||
]))),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
imap.host = "localhost"
|
||||
imap.port = 143
|
||||
imap.login = "test@localhost"
|
||||
imap.passwd.cmd = ["pass show test", "tr -d '[:blank:]'"]
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
#[test]
|
||||
fn pretty_serialize_imap_oauth2() {
|
||||
use email::{
|
||||
account::config::oauth2::OAuth2Config,
|
||||
imap::config::{ImapAuthConfig, ImapConfig},
|
||||
};
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
imap: Some(ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 143,
|
||||
login: "test@localhost".into(),
|
||||
auth: ImapAuthConfig::OAuth2(OAuth2Config {
|
||||
client_id: "client-id".into(),
|
||||
auth_url: "auth-url".into(),
|
||||
token_url: "token-url".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
imap.host = "localhost"
|
||||
imap.port = 143
|
||||
imap.login = "test@localhost"
|
||||
imap.oauth2.method = "xoauth2"
|
||||
imap.oauth2.client-id = "client-id"
|
||||
imap.oauth2.auth-url = "auth-url"
|
||||
imap.oauth2.token-url = "token-url"
|
||||
imap.oauth2.pkce = false
|
||||
imap.oauth2.scopes = []
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
#[test]
|
||||
fn pretty_serialize_maildir() {
|
||||
use email::maildir::config::MaildirConfig;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
maildir: Some(MaildirConfig {
|
||||
root_dir: "/tmp/test".into(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
maildir.root-dir = "/tmp/test"
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[test]
|
||||
fn pretty_serialize_smtp_passwd_cmd() {
|
||||
use email::{
|
||||
account::config::passwd::PasswdConfig,
|
||||
smtp::config::{SmtpAuthConfig, SmtpConfig},
|
||||
};
|
||||
use secret::Secret;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
smtp: Some(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 143,
|
||||
login: "test@localhost".into(),
|
||||
auth: SmtpAuthConfig::Passwd(PasswdConfig(Secret::new_cmd("pass show test"))),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 143
|
||||
smtp.login = "test@localhost"
|
||||
smtp.passwd.cmd = "pass show test"
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[test]
|
||||
fn pretty_serialize_smtp_passwd_cmds() {
|
||||
use email::{
|
||||
account::config::passwd::PasswdConfig,
|
||||
smtp::config::{SmtpAuthConfig, SmtpConfig},
|
||||
};
|
||||
use secret::Secret;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
smtp: Some(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 143,
|
||||
login: "test@localhost".into(),
|
||||
auth: SmtpAuthConfig::Passwd(PasswdConfig(Secret::new_cmd(vec![
|
||||
"pass show test",
|
||||
"tr -d '[:blank:]'",
|
||||
]))),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 143
|
||||
smtp.login = "test@localhost"
|
||||
smtp.passwd.cmd = ["pass show test", "tr -d '[:blank:]'"]
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[test]
|
||||
fn pretty_serialize_smtp_oauth2() {
|
||||
use email::{
|
||||
account::config::oauth2::OAuth2Config,
|
||||
smtp::config::{SmtpAuthConfig, SmtpConfig},
|
||||
};
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
smtp: Some(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 143,
|
||||
login: "test@localhost".into(),
|
||||
auth: SmtpAuthConfig::OAuth2(OAuth2Config {
|
||||
client_id: "client-id".into(),
|
||||
auth_url: "auth-url".into(),
|
||||
token_url: "token-url".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 143
|
||||
smtp.login = "test@localhost"
|
||||
smtp.oauth2.method = "xoauth2"
|
||||
smtp.oauth2.client-id = "client-id"
|
||||
smtp.oauth2.auth-url = "auth-url"
|
||||
smtp.oauth2.token-url = "token-url"
|
||||
smtp.oauth2.pkce = false
|
||||
smtp.oauth2.scopes = []
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp-cmds")]
|
||||
#[test]
|
||||
fn pretty_serialize_pgp_cmds() {
|
||||
use email::account::config::pgp::PgpConfig;
|
||||
|
||||
assert_eq(
|
||||
TomlAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
pgp: Some(PgpConfig::Cmds(Default::default())),
|
||||
..Default::default()
|
||||
},
|
||||
r#"[accounts.test]
|
||||
email = "test@localhost"
|
||||
pgp.backend = "cmds"
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,31 +1,27 @@
|
|||
use std::{process::exit, sync::Arc};
|
||||
|
||||
use ariadne::{Color, Label, Report, ReportKind, Source};
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, config::Config, email::search_query,
|
||||
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalArg,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
};
|
||||
|
||||
/// Search and sort envelopes as a list.
|
||||
/// List all envelopes.
|
||||
///
|
||||
/// This command allows you to list envelopes included in the given
|
||||
/// folder, matching the given query.
|
||||
/// This command allows you to list all envelopes included in the
|
||||
/// given folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct EnvelopeListCommand {
|
||||
pub struct ListEnvelopesCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
pub folder: FolderNameOptionalArg,
|
||||
|
||||
/// The page number.
|
||||
///
|
||||
|
@ -40,113 +36,40 @@ pub struct EnvelopeListCommand {
|
|||
#[arg(long, short = 's', value_name = "NUMBER")]
|
||||
pub page_size: Option<usize>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub table: TableMaxWidthFlag,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
|
||||
/// The list envelopes filter and sort query.
|
||||
///
|
||||
/// The query can be a filter query, a sort query or both
|
||||
/// together.
|
||||
///
|
||||
/// A filter query is composed of operators and conditions. There
|
||||
/// is 3 operators and 8 conditions:
|
||||
///
|
||||
/// • not <condition> → filter envelopes that do not match the
|
||||
/// condition
|
||||
///
|
||||
/// • <condition> and <condition> → filter envelopes that match
|
||||
/// both conditions
|
||||
///
|
||||
/// • <condition> or <condition> → filter envelopes that match
|
||||
/// one of the conditions
|
||||
///
|
||||
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
|
||||
/// date
|
||||
///
|
||||
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
|
||||
/// before the given one
|
||||
///
|
||||
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
|
||||
/// after the given one
|
||||
///
|
||||
/// ◦ from <pattern> → filter envelopes with senders matching the
|
||||
/// given pattern
|
||||
///
|
||||
/// ◦ to <pattern> → filter envelopes with recipients matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ subject <pattern> → filter envelopes with subject matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ body <pattern> → filter envelopes with text bodies matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ flag <flag> → filter envelopes matching the given flag
|
||||
///
|
||||
/// A sort query starts by "order by", and is composed of kinds
|
||||
/// and orders. There is 4 kinds and 2 orders:
|
||||
///
|
||||
/// • date [order] → sort envelopes by date
|
||||
///
|
||||
/// • from [order] → sort envelopes by sender
|
||||
///
|
||||
/// • to [order] → sort envelopes by recipient
|
||||
///
|
||||
/// • subject [order] → sort envelopes by subject
|
||||
///
|
||||
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
|
||||
/// order
|
||||
///
|
||||
/// ◦ <kind> desc → sort envelopes by the given kind in
|
||||
/// descending order
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// subject foo and body bar → filter envelopes containing "foo"
|
||||
/// in their subject and "bar" in their text bodies
|
||||
///
|
||||
/// order by date desc subject → sort envelopes by descending date
|
||||
/// (most recent first), then by ascending subject
|
||||
///
|
||||
/// subject foo and body bar order by date desc subject →
|
||||
/// combination of the 2 previous examples
|
||||
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
|
||||
pub query: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for EnvelopeListCommand {
|
||||
impl Default for ListEnvelopesCommand {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
folder: Default::default(),
|
||||
page: 1,
|
||||
page_size: Default::default(),
|
||||
table: Default::default(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
cache: Default::default(),
|
||||
account: Default::default(),
|
||||
query: Default::default(),
|
||||
table_max_width: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvelopeListCommand {
|
||||
impl ListEnvelopesCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list envelopes command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let toml_account_config = Arc::new(toml_account_config);
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let page = 1.max(self.page) - 1;
|
||||
|
@ -154,65 +77,26 @@ impl EnvelopeListCommand {
|
|||
.page_size
|
||||
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
let list_envelopes_kind = toml_account_config.list_envelopes_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_list_envelopes(BackendFeatureSource::Context)
|
||||
},
|
||||
account_config.clone(),
|
||||
list_envelopes_kind,
|
||||
|builder| builder.set_list_envelopes(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let query = self
|
||||
.query
|
||||
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
|
||||
let query = match query {
|
||||
None => None,
|
||||
Some(Ok(query)) => Some(query),
|
||||
Some(Err(main_err)) => {
|
||||
let source = "query";
|
||||
let search_query::error::Error::ParseError(errs, query) = &main_err;
|
||||
for err in errs {
|
||||
Report::build(ReportKind::Error, source, err.span().start)
|
||||
.with_message(main_err.to_string())
|
||||
.with_label(
|
||||
Label::new((source, err.span().into_range()))
|
||||
.with_message(err.reason().to_string())
|
||||
.with_color(Color::Red),
|
||||
)
|
||||
.finish()
|
||||
.eprint((source, Source::from(&query)))
|
||||
.unwrap();
|
||||
}
|
||||
let envelopes = backend.list_envelopes(folder, page_size, page).await?;
|
||||
|
||||
exit(0)
|
||||
}
|
||||
};
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &account_config.get_message_read_format(),
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
let opts = ListEnvelopesOptions {
|
||||
page,
|
||||
page_size,
|
||||
query,
|
||||
};
|
||||
|
||||
let envelopes = backend.list_envelopes(folder, opts).await?;
|
||||
let table = EnvelopesTable::from(envelopes)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(toml_account_config.envelope_list_table_preset())
|
||||
.with_some_unseen_char(toml_account_config.envelope_list_table_unseen_char())
|
||||
.with_some_replied_char(toml_account_config.envelope_list_table_replied_char())
|
||||
.with_some_flagged_char(toml_account_config.envelope_list_table_flagged_char())
|
||||
.with_some_attachment_char(toml_account_config.envelope_list_table_attachment_char())
|
||||
.with_some_id_color(toml_account_config.envelope_list_table_id_color())
|
||||
.with_some_flags_color(toml_account_config.envelope_list_table_flags_color())
|
||||
.with_some_subject_color(toml_account_config.envelope_list_table_subject_color())
|
||||
.with_some_sender_color(toml_account_config.envelope_list_table_sender_color())
|
||||
.with_some_date_color(toml_account_config.envelope_list_table_date_color());
|
||||
|
||||
printer.out(table)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
pub mod list;
|
||||
pub mod thread;
|
||||
pub mod watch;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
|
||||
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
|
||||
|
||||
/// List, search and sort your envelopes.
|
||||
/// Manage envelopes.
|
||||
///
|
||||
/// An envelope is a small representation of a message. It contains an
|
||||
/// identifier (given by the backend), some flags as well as few
|
||||
|
@ -18,10 +17,10 @@ use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
|
|||
#[derive(Debug, Subcommand)]
|
||||
pub enum EnvelopeSubcommand {
|
||||
#[command(alias = "lst")]
|
||||
List(EnvelopeListCommand),
|
||||
List(ListEnvelopesCommand),
|
||||
|
||||
#[command()]
|
||||
Thread(EnvelopeThreadCommand),
|
||||
Watch(WatchEnvelopesCommand),
|
||||
}
|
||||
|
||||
impl EnvelopeSubcommand {
|
||||
|
@ -29,7 +28,7 @@ impl EnvelopeSubcommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Watch(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
use ariadne::{Label, Report, ReportKind, Source};
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, config::Config, email::search_query,
|
||||
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, config::EnvelopesTree},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{process::exit, sync::Arc};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Search and sort envelopes as a thread.
|
||||
///
|
||||
/// This command allows you to thread envelopes included in the given
|
||||
/// folder, matching the given query.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct EnvelopeThreadCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
/// Show only threads that contain the given envelope identifier.
|
||||
#[arg(long, short)]
|
||||
pub id: Option<usize>,
|
||||
|
||||
/// The list envelopes filter and sort query.
|
||||
///
|
||||
/// See `envelope list --help` for more information.
|
||||
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
|
||||
pub query: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl EnvelopeThreadCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing thread envelopes command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_thread_envelopes(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let query = self
|
||||
.query
|
||||
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
|
||||
let query = match query {
|
||||
None => None,
|
||||
Some(Ok(query)) => Some(query),
|
||||
Some(Err(main_err)) => {
|
||||
let source = "query";
|
||||
let search_query::error::Error::ParseError(errs, query) = &main_err;
|
||||
for err in errs {
|
||||
Report::build(ReportKind::Error, source, err.span().start)
|
||||
.with_message(main_err.to_string())
|
||||
.with_label(
|
||||
Label::new((source, err.span().into_range()))
|
||||
.with_message(err.reason().to_string())
|
||||
.with_color(ariadne::Color::Red),
|
||||
)
|
||||
.finish()
|
||||
.eprint((source, Source::from(&query)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
exit(0)
|
||||
}
|
||||
};
|
||||
|
||||
let opts = ListEnvelopesOptions {
|
||||
page: 0,
|
||||
page_size: 0,
|
||||
query,
|
||||
};
|
||||
|
||||
let envelopes = match self.id {
|
||||
Some(id) => backend.thread_envelope(folder, id, opts).await,
|
||||
None => backend.thread_envelopes(folder, opts).await,
|
||||
}?;
|
||||
|
||||
let tree = EnvelopesTree::new(account_config, envelopes);
|
||||
|
||||
printer.out(tree)
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
|
||||
// use petgraph::graphmap::DiGraphMap;
|
||||
|
||||
// use super::write_tree;
|
||||
|
||||
// macro_rules! e {
|
||||
// ($id:literal) => {
|
||||
// ThreadedEnvelope {
|
||||
// id: $id,
|
||||
// message_id: $id,
|
||||
// from: "",
|
||||
// subject: "",
|
||||
// date: Default::default(),
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn tree_1() {
|
||||
// let config = AccountConfig::default();
|
||||
// let mut buf = Vec::new();
|
||||
// let mut graph = DiGraphMap::new();
|
||||
// graph.add_edge(e!("0"), e!("1"), 0);
|
||||
// graph.add_edge(e!("0"), e!("2"), 0);
|
||||
// graph.add_edge(e!("0"), e!("3"), 0);
|
||||
|
||||
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
|
||||
// let buf = String::from_utf8_lossy(&buf);
|
||||
|
||||
// let expected = "
|
||||
// 0
|
||||
// ├─ 1
|
||||
// ├─ 2
|
||||
// └─ 3
|
||||
// ";
|
||||
// assert_eq!(expected.trim_start(), buf)
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn tree_2() {
|
||||
// let config = AccountConfig::default();
|
||||
// let mut buf = Vec::new();
|
||||
// let mut graph = DiGraphMap::new();
|
||||
// graph.add_edge(e!("0"), e!("1"), 0);
|
||||
// graph.add_edge(e!("1"), e!("2"), 1);
|
||||
// graph.add_edge(e!("1"), e!("3"), 1);
|
||||
|
||||
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
|
||||
// let buf = String::from_utf8_lossy(&buf);
|
||||
|
||||
// let expected = "
|
||||
// 0
|
||||
// └─ 1
|
||||
// ├─ 2
|
||||
// └─ 3
|
||||
// ";
|
||||
// assert_eq!(expected.trim_start(), buf)
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn tree_3() {
|
||||
// let config = AccountConfig::default();
|
||||
// let mut buf = Vec::new();
|
||||
// let mut graph = DiGraphMap::new();
|
||||
// graph.add_edge(e!("0"), e!("1"), 0);
|
||||
// graph.add_edge(e!("1"), e!("2"), 1);
|
||||
// graph.add_edge(e!("2"), e!("22"), 2);
|
||||
// graph.add_edge(e!("1"), e!("3"), 1);
|
||||
// graph.add_edge(e!("0"), e!("4"), 0);
|
||||
// graph.add_edge(e!("4"), e!("5"), 1);
|
||||
// graph.add_edge(e!("5"), e!("6"), 2);
|
||||
|
||||
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
|
||||
// let buf = String::from_utf8_lossy(&buf);
|
||||
|
||||
// let expected = "
|
||||
// 0
|
||||
// ├─ 1
|
||||
// │ ├─ 2
|
||||
// │ │ └─ 22
|
||||
// │ └─ 3
|
||||
// └─ 4
|
||||
// └─ 5
|
||||
// └─ 6
|
||||
// ";
|
||||
// assert_eq!(expected.trim_start(), buf)
|
||||
// }
|
||||
// }
|
57
src/email/envelope/command/watch.rs
Normal file
57
src/email/envelope/command/watch.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use email::{backend::feature::BackendFeatureSource, envelope::watch::WatchEnvelopes};
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
|
||||
};
|
||||
|
||||
/// Watch envelopes for changes.
|
||||
///
|
||||
/// This command allows you to watch a folder and execute hooks when
|
||||
/// changes occur on envelopes.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct WatchEnvelopesCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl WatchEnvelopesCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing watch envelopes command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let watch_envelopes_kind = toml_account_config.watch_envelopes_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
watch_envelopes_kind,
|
||||
|builder| builder.set_watch_envelopes(BackendFeatureSource::Context),
|
||||
)
|
||||
.await?;
|
||||
|
||||
printer.print_log(format!(
|
||||
"Start watching folder {folder} for envelopes changes…"
|
||||
))?;
|
||||
|
||||
backend.watch_envelopes(folder).await
|
||||
}
|
||||
}
|
92
src/email/envelope/config.rs
Normal file
92
src/email/envelope/config.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
#[cfg(feature = "account-sync")]
|
||||
use email::envelope::sync::config::EnvelopeSyncConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::backend::BackendKind;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct EnvelopeConfig {
|
||||
pub list: Option<ListEnvelopesConfig>,
|
||||
pub watch: Option<WatchEnvelopesConfig>,
|
||||
pub get: Option<GetEnvelopeConfig>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub sync: Option<EnvelopeSyncConfig>,
|
||||
}
|
||||
|
||||
impl EnvelopeConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(list) = &self.list {
|
||||
kinds.extend(list.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(watch) = &self.watch {
|
||||
kinds.extend(watch.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(get) = &self.get {
|
||||
kinds.extend(get.get_used_backends());
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ListEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::envelope::list::config::EnvelopeListConfig,
|
||||
}
|
||||
|
||||
impl ListEnvelopesConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct WatchEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::envelope::watch::config::WatchEnvelopeConfig,
|
||||
}
|
||||
|
||||
impl WatchEnvelopesConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct GetEnvelopeConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl GetEnvelopeConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use clap::Parser;
|
||||
use email::flag::{Flag, Flags};
|
||||
use tracing::debug;
|
||||
use log::debug;
|
||||
|
||||
/// The ids and/or flags arguments parser.
|
||||
#[derive(Debug, Parser)]
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Add flag(s) to the given envelope.
|
||||
/// Add flag(s) to an envelope.
|
||||
///
|
||||
/// This command allows you to attach the given flag(s) to the given
|
||||
/// envelope(s).
|
||||
|
@ -28,6 +26,10 @@ pub struct FlagAddCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -38,27 +40,24 @@ impl FlagAddCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
let add_flags_kind = toml_account_config.add_flags_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
add_flags_kind,
|
||||
|builder| builder.set_add_flags(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.add_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.out(format!("Flag(s) {flags} successfully added!\n"))
|
||||
printer.print(format!("Flag(s) {flags} successfully added!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,19 @@ mod add;
|
|||
mod remove;
|
||||
mod set;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
|
||||
|
||||
/// Add, change and remove your envelopes flags.
|
||||
/// Manage flags.
|
||||
///
|
||||
/// A flag is a tag associated to an envelope. Existing flags are
|
||||
/// seen, answered, flagged, deleted, draft. Other flags are
|
||||
/// considered custom, which are not always supported.
|
||||
/// considered custom, which are not always supported (the
|
||||
/// synchronization does not take care of them yet).
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum FlagSubcommand {
|
||||
#[command(arg_required_else_help = true)]
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Remove flag(s) from a given envelope.
|
||||
/// Remove flag(s) from an envelope.
|
||||
///
|
||||
/// This command allows you to remove the given flag(s) from the given
|
||||
/// envelope(s).
|
||||
|
@ -28,6 +26,10 @@ pub struct FlagRemoveCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -38,27 +40,24 @@ impl FlagRemoveCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_remove_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
let remove_flags_kind = toml_account_config.remove_flags_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
remove_flags_kind,
|
||||
|builder| builder.set_remove_flags(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.remove_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.out(format!("Flag(s) {flags} successfully removed!\n"))
|
||||
printer.print(format!("Flag(s) {flags} successfully removed!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Replace flag(s) of a given envelope.
|
||||
/// Replace flag(s) of an envelope.
|
||||
///
|
||||
/// This command allows you to replace existing flags of the given
|
||||
/// envelope(s) with the given flag(s).
|
||||
|
@ -28,6 +26,10 @@ pub struct FlagSetCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -38,27 +40,24 @@ impl FlagSetCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_set_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
let set_flags_kind = toml_account_config.set_flags_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
set_flags_kind,
|
||||
|builder| builder.set_set_flags(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.set_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
|
||||
printer.print(format!("Flag(s) {flags} successfully replaced!"))
|
||||
}
|
||||
}
|
||||
|
|
86
src/email/envelope/flag/config.rs
Normal file
86
src/email/envelope/flag/config.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
#[cfg(feature = "account-sync")]
|
||||
use email::flag::sync::config::FlagSyncConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::backend::BackendKind;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FlagConfig {
|
||||
pub add: Option<FlagAddConfig>,
|
||||
pub set: Option<FlagSetConfig>,
|
||||
pub remove: Option<FlagRemoveConfig>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub sync: Option<FlagSyncConfig>,
|
||||
}
|
||||
|
||||
impl FlagConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(add) = &self.add {
|
||||
kinds.extend(add.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(set) = &self.set {
|
||||
kinds.extend(set.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(remove) = &self.remove {
|
||||
kinds.extend(remove.get_used_backends());
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FlagAddConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FlagAddConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FlagSetConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FlagSetConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FlagRemoveConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FlagRemoveConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
|
@ -1,2 +1,48 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
|
||||
use serde::Serialize;
|
||||
use std::{collections::HashSet, ops};
|
||||
|
||||
/// Represents the flag variants.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize)]
|
||||
pub enum Flag {
|
||||
Seen,
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Draft,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&email::flag::Flag> for Flag {
|
||||
fn from(flag: &email::flag::Flag) -> Self {
|
||||
use email::flag::Flag::*;
|
||||
match flag {
|
||||
Seen => Flag::Seen,
|
||||
Answered => Flag::Answered,
|
||||
Flagged => Flag::Flagged,
|
||||
Deleted => Flag::Deleted,
|
||||
Draft => Flag::Draft,
|
||||
Custom(flag) => Flag::Custom(flag.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<email::flag::Flags> for Flags {
|
||||
fn from(flags: email::flag::Flags) -> Self {
|
||||
Flags(flags.iter().map(Flag::from).collect())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,123 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod flag;
|
||||
|
||||
use anyhow::Result;
|
||||
use email::account::config::AccountConfig;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
cache::IdMapper,
|
||||
flag::{Flag, Flags},
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
#[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,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for Envelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("FROM").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_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;
|
||||
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of envelopes.
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Envelopes(Vec<Envelope>);
|
||||
|
||||
impl Envelopes {
|
||||
pub fn from_backend(
|
||||
config: &AccountConfig,
|
||||
id_mapper: &IdMapper,
|
||||
envelopes: email::envelope::Envelopes,
|
||||
) -> Result<Envelopes> {
|
||||
let envelopes = envelopes
|
||||
.iter()
|
||||
.map(|envelope| {
|
||||
Ok(Envelope {
|
||||
id: id_mapper.get_or_create_alias(&envelope.id)?,
|
||||
flags: envelope.flags.clone().into(),
|
||||
subject: envelope.subject.clone(),
|
||||
from: Mailbox {
|
||||
name: envelope.from.name.clone(),
|
||||
addr: envelope.from.addr.clone(),
|
||||
},
|
||||
date: envelope.format_date(config),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Envelopes(envelopes))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Envelopes {
|
||||
type Target = Vec<Envelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Envelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
use std::{fs, path::PathBuf};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Download all attachments found in the given message.
|
||||
/// Download all attachments for the given message.
|
||||
///
|
||||
/// This command allows you to download all attachments found for the
|
||||
/// given message to your downloads directory.
|
||||
|
@ -26,6 +25,10 @@ pub struct AttachmentDownloadCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -37,25 +40,20 @@ impl AttachmentDownloadCommand {
|
|||
let folder = &self.folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let get_messages_kind = toml_account_config.get_messages_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let emails = backend.get_messages(folder, ids).await?;
|
||||
|
@ -69,14 +67,14 @@ impl AttachmentDownloadCommand {
|
|||
let attachments = email.attachments()?;
|
||||
|
||||
if attachments.is_empty() {
|
||||
printer.log(format!("No attachment found for message {id}!\n"))?;
|
||||
printer.print_log(format!("No attachment found for message {id}!"))?;
|
||||
continue;
|
||||
} else {
|
||||
emails_count += 1;
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"{} attachment(s) found for message {id}!\n",
|
||||
printer.print_log(format!(
|
||||
"{} attachment(s) found for message {id}!",
|
||||
attachments.len()
|
||||
))?;
|
||||
|
||||
|
@ -86,7 +84,7 @@ impl AttachmentDownloadCommand {
|
|||
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||
.into();
|
||||
let filepath = account_config.get_download_file_path(&filename)?;
|
||||
printer.log(format!("Downloading {:?}…\n", filepath))?;
|
||||
printer.print_log(format!("Downloading {:?}…", filepath))?;
|
||||
fs::write(&filepath, &attachment.body)
|
||||
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
|
||||
attachments_count += 1;
|
||||
|
@ -94,10 +92,10 @@ impl AttachmentDownloadCommand {
|
|||
}
|
||||
|
||||
match attachments_count {
|
||||
0 => printer.out("No attachment found!\n"),
|
||||
1 => printer.out("Downloaded 1 attachment!\n"),
|
||||
n => printer.out(format!(
|
||||
"Downloaded {} attachment(s) from {} messages(s)!\n",
|
||||
0 => printer.print("No attachment found!"),
|
||||
1 => printer.print("Downloaded 1 attachment!"),
|
||||
n => printer.print(format!(
|
||||
"Downloaded {} attachment(s) from {} messages(s)!",
|
||||
n, emails_count,
|
||||
)),
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
mod download;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::download::AttachmentDownloadCommand;
|
||||
|
||||
/// Download your message attachments.
|
||||
/// Manage attachments.
|
||||
///
|
||||
/// A message body can be composed of multiple MIME parts. An
|
||||
/// attachment is the representation of a binary part of a message
|
||||
/// body.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AttachmentSubcommand {
|
||||
#[command(arg_required_else_help = true, alias = "dl")]
|
||||
#[command(arg_required_else_help = true)]
|
||||
Download(AttachmentDownloadCommand),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Copy the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
/// Copy a message from a source folder to a target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageCopyCommand {
|
||||
#[command(flatten)]
|
||||
|
@ -29,6 +26,10 @@ pub struct MessageCopyCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -41,29 +42,26 @@ impl MessageCopyCommand {
|
|||
let target = &self.target_folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_copy_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
let copy_messages_kind = toml_account_config.copy_messages_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
copy_messages_kind,
|
||||
|builder| builder.set_copy_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.copy_messages(source, target, ids).await?;
|
||||
|
||||
printer.out(format!(
|
||||
"Message(s) successfully copied from {source} to {target}!\n"
|
||||
printer.print(format!(
|
||||
"Message(s) successfully copied from {source} to {target}!"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Mark as deleted the message associated to the given envelope id(s).
|
||||
/// Mark as deleted a message from a folder.
|
||||
///
|
||||
/// This command does not really delete the message: if the given
|
||||
/// folder points to the trash folder, it adds the "deleted" flag to
|
||||
|
@ -28,6 +25,10 @@ pub struct MessageDeleteCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -39,27 +40,24 @@ impl MessageDeleteCommand {
|
|||
let folder = &self.folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_delete_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
let delete_messages_kind = toml_account_config.delete_messages_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
delete_messages_kind,
|
||||
|builder| builder.set_delete_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.delete_messages(folder, ids).await?;
|
||||
|
||||
printer.out(format!("Message(s) successfully removed from {folder}!\n"))
|
||||
printer.print(format!("Message(s) successfully removed from {folder}!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Edit the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to edit the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
/// edition process finishes, you can choose between saving or sending
|
||||
/// the final message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageEditCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// List of headers that should be visible at the top of the
|
||||
/// message.
|
||||
///
|
||||
/// If a given header is not found in the message, it will not be
|
||||
/// visible. If no header is given, defaults to the one set up in
|
||||
/// your TOML configuration file.
|
||||
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||
pub headers: Vec<String>,
|
||||
|
||||
/// Edit the message on place.
|
||||
///
|
||||
/// If set, the original message being edited will be removed at
|
||||
/// the end of the command. Useful when you need, for example, to
|
||||
/// edit a draft, send it then remove it from the Drafts folder.
|
||||
#[arg(long, short = 'p')]
|
||||
pub on_place: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageEditCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing edit message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
.with_delete_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message"))?
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
|
||||
|
||||
if self.on_place {
|
||||
backend.delete_messages(folder, &[id]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
use std::{
|
||||
env::temp_dir,
|
||||
fs,
|
||||
io::{stdout, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Export the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to export a message. A message can be
|
||||
/// fully exported in one single file, or exported in multiple files
|
||||
/// (one per MIME part found in the message). This is useful, for
|
||||
/// example, to read a HTML message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageExportCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// Export the full raw message as one unique .eml file.
|
||||
///
|
||||
/// The raw message represents the headers and the body as it is
|
||||
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||
/// useful for debugging faulty messages, but also for
|
||||
/// saving/sending/transfering messages.
|
||||
#[arg(long, short = 'F')]
|
||||
pub full: bool,
|
||||
|
||||
/// Try to open the exported message, when applicable.
|
||||
///
|
||||
/// This argument only works with full message export, or when
|
||||
/// HTML or plain text is present in the export.
|
||||
#[arg(long, short = 'O')]
|
||||
pub open: bool,
|
||||
|
||||
/// Where the message should be exported to.
|
||||
///
|
||||
/// The destination should point to a valid directory. If `--full`
|
||||
/// is given, it can also point to a .eml file.
|
||||
#[arg(long, short, alias = "dest")]
|
||||
pub destination: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageExportCommand {
|
||||
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing export message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let id = &self.envelope.id;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let msgs = backend.get_messages(folder, &[*id]).await?;
|
||||
let msg = msgs.first().ok_or(eyre!("cannot find message {id}"))?;
|
||||
|
||||
if self.full {
|
||||
let bytes = msg.raw()?;
|
||||
|
||||
match self.destination {
|
||||
Some(mut dest) if dest.is_dir() => {
|
||||
dest.push(format!("{id}.eml"));
|
||||
fs::write(&dest, bytes)?;
|
||||
let dest = dest.display();
|
||||
println!("Message {id} successfully exported at {dest}!");
|
||||
}
|
||||
Some(dest) => {
|
||||
fs::write(&dest, bytes)?;
|
||||
let dest = dest.display();
|
||||
println!("Message {id} successfully exported at {dest}!");
|
||||
}
|
||||
None => {
|
||||
stdout().write_all(bytes)?;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let dest = match self.destination {
|
||||
Some(dest) if dest.is_dir() => {
|
||||
let dest = msg.download_parts(&dest)?;
|
||||
let d = dest.display();
|
||||
println!("Message {id} successfully exported in {d}!");
|
||||
dest
|
||||
}
|
||||
Some(dest) if dest.is_file() => {
|
||||
let dest = dest.parent().unwrap_or(&dest);
|
||||
let dest = msg.download_parts(&dest)?;
|
||||
let d = dest.display();
|
||||
println!("Message {id} successfully exported in {d}!");
|
||||
dest
|
||||
}
|
||||
Some(dest) => {
|
||||
return Err(eyre!("Destination {} does not exist!", dest.display()));
|
||||
}
|
||||
None => {
|
||||
let dest = temp_dir();
|
||||
let dest = msg.download_parts(&dest)?;
|
||||
let d = dest.display();
|
||||
println!("Message {id} successfully exported in {d}!");
|
||||
dest
|
||||
}
|
||||
};
|
||||
|
||||
if self.open {
|
||||
let index_html = dest.join("index.html");
|
||||
if index_html.exists() {
|
||||
return Ok(open::that(index_html)?);
|
||||
}
|
||||
|
||||
let plain_txt = dest.join("plain.txt");
|
||||
if plain_txt.exists() {
|
||||
return Ok(open::that(plain_txt)?);
|
||||
}
|
||||
|
||||
println!("--open was passed but nothing to open, ignoring");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,23 +1,22 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||
printer::Printer,
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Forward the message associated to the given envelope id.
|
||||
/// Forward a message.
|
||||
///
|
||||
/// This command allows you to forward the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
|
@ -37,6 +36,10 @@ pub struct MessageForwardCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -47,25 +50,24 @@ impl MessageForwardCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let add_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
|
@ -73,8 +75,8 @@ impl MessageForwardCommand {
|
|||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message"))?
|
||||
.to_forward_tpl_builder(account_config.clone())
|
||||
.ok_or(anyhow!("cannot find message"))?
|
||||
.to_forward_tpl_builder(&account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::{debug, info};
|
||||
use mail_builder::MessageBuilder;
|
||||
use url::Url;
|
||||
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, printer::Printer,
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Parse and edit a message from the given mailto URL string.
|
||||
/// Parse and edit a message from a mailto URL string.
|
||||
///
|
||||
/// This command allows you to edit a message from the mailto format
|
||||
/// using the editor defined in your environment variable
|
||||
|
@ -24,6 +24,10 @@ pub struct MessageMailtoCommand {
|
|||
#[arg()]
|
||||
pub url: Url,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -32,6 +36,8 @@ impl MessageMailtoCommand {
|
|||
pub fn new(url: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: Url::parse(url)?,
|
||||
#[cfg(feature = "account-sync")]
|
||||
cache: Default::default(),
|
||||
account: Default::default(),
|
||||
})
|
||||
}
|
||||
|
@ -39,59 +45,54 @@ impl MessageMailtoCommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing mailto message command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let add_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut msg = Vec::<u8>::new();
|
||||
let mut body = Vec::<u8>::new();
|
||||
|
||||
msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n");
|
||||
let mut builder = MessageBuilder::new().to(self.url.path());
|
||||
let mut body = String::new();
|
||||
|
||||
for (key, val) in self.url.query_pairs() {
|
||||
if key.eq_ignore_ascii_case("body") {
|
||||
body.extend(val.as_bytes());
|
||||
} else {
|
||||
msg.extend(key.as_bytes());
|
||||
msg.extend(b": ");
|
||||
msg.extend(val.as_bytes());
|
||||
msg.extend(b"\r\n");
|
||||
match key.to_lowercase().as_bytes() {
|
||||
b"cc" => builder = builder.cc(val.to_string()),
|
||||
b"bcc" => builder = builder.bcc(val.to_string()),
|
||||
b"subject" => builder = builder.subject(val),
|
||||
b"body" => body += &val,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
msg.extend(b"\r\n");
|
||||
msg.extend(body);
|
||||
|
||||
if let Some(sig) = account_config.find_full_signature() {
|
||||
msg.extend(b"\r\n");
|
||||
msg.extend(sig.as_bytes());
|
||||
match account_config.find_full_signature() {
|
||||
Ok(Some(ref signature)) => builder = builder.text_body(body + "\n\n" + signature),
|
||||
Ok(None) => builder = builder.text_body(body),
|
||||
Err(err) => {
|
||||
debug!("cannot add signature to mailto message, skipping it: {err}");
|
||||
debug!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let tpl = account_config
|
||||
.generate_tpl_interpreter()
|
||||
.with_show_only_headers(account_config.get_message_write_headers())
|
||||
.build()
|
||||
.from_bytes(msg)
|
||||
.await?
|
||||
.into();
|
||||
.from_msg_builder(builder)
|
||||
.await?;
|
||||
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod edit;
|
||||
pub mod export;
|
||||
pub mod forward;
|
||||
pub mod mailto;
|
||||
pub mod r#move;
|
||||
|
@ -9,24 +7,21 @@ pub mod read;
|
|||
pub mod reply;
|
||||
pub mod save;
|
||||
pub mod send;
|
||||
pub mod thread;
|
||||
pub mod write;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::{
|
||||
copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand,
|
||||
export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand,
|
||||
r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand,
|
||||
save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand,
|
||||
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
|
||||
mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand,
|
||||
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
|
||||
write::MessageWriteCommand,
|
||||
};
|
||||
|
||||
/// Read, write, send, copy, move and delete your messages.
|
||||
/// Manage messages.
|
||||
///
|
||||
/// A message is the content of an email. It is composed of headers
|
||||
/// (located at the top of the message) and a body (located at the
|
||||
|
@ -37,22 +32,16 @@ pub enum MessageSubcommand {
|
|||
#[command(arg_required_else_help = true)]
|
||||
Read(MessageReadCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Export(MessageExportCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Thread(MessageThreadCommand),
|
||||
|
||||
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||
Write(MessageWriteCommand),
|
||||
|
||||
#[command()]
|
||||
Reply(MessageReplyCommand),
|
||||
|
||||
#[command(aliases = ["fwd", "fd"])]
|
||||
Forward(MessageForwardCommand),
|
||||
|
||||
Edit(MessageEditCommand),
|
||||
|
||||
#[command()]
|
||||
Mailto(MessageMailtoCommand),
|
||||
|
||||
Save(MessageSaveCommand),
|
||||
|
@ -77,12 +66,9 @@ impl MessageSubcommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Read(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Export(cmd) => cmd.execute(config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Write(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Reply(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Forward(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Edit(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Mailto(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Save(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Send(cmd) => cmd.execute(printer, config).await,
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Move the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
/// Move a message from a source folder to a target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageMoveCommand {
|
||||
#[command(flatten)]
|
||||
|
@ -30,6 +27,10 @@ pub struct MessageMoveCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -42,29 +43,26 @@ impl MessageMoveCommand {
|
|||
let target = &self.target_folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_move_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
let move_messages_kind = toml_account_config.move_messages_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
move_messages_kind,
|
||||
|builder| builder.set_move_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.move_messages(source, target, ids).await?;
|
||||
|
||||
printer.out(format!(
|
||||
"Message(s) successfully moved from {source} to {target}!\n"
|
||||
printer.print(format!(
|
||||
"Message(s) successfully moved from {source} to {target}!"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
use mml::message::FilterParts;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Read a human-friendly version of the message associated to the
|
||||
/// given envelope id(s).
|
||||
/// Read a message.
|
||||
///
|
||||
/// This command allows you to read a message. When reading a message,
|
||||
/// the "seen" flag is automatically applied to the corresponding
|
||||
/// envelope. To prevent this behaviour, use the "--preview" flag.
|
||||
/// envelope. To prevent this behaviour, use the --preview flag.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageReadCommand {
|
||||
#[command(flatten)]
|
||||
|
@ -34,10 +31,31 @@ pub struct MessageReadCommand {
|
|||
#[arg(long, short)]
|
||||
pub preview: bool,
|
||||
|
||||
/// Read the raw version of the given message.
|
||||
///
|
||||
/// The raw message represents the headers and the body as it is
|
||||
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||
/// useful for debugging faulty messages, but also for
|
||||
/// saving/sending/transfering messages.
|
||||
#[arg(long, short)]
|
||||
#[arg(conflicts_with = "no_headers")]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub raw: bool,
|
||||
|
||||
/// Read only body of text/html parts.
|
||||
///
|
||||
/// This argument is useful when you need to read the HTML version
|
||||
/// of a message. Combined with --no-headers, you can write it to
|
||||
/// a .html file and open it with your favourite browser.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
pub html: bool,
|
||||
|
||||
/// Read only the body of the message.
|
||||
///
|
||||
/// All headers will be removed from the message.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub no_headers: bool,
|
||||
|
||||
|
@ -48,9 +66,14 @@ pub struct MessageReadCommand {
|
|||
/// visible. If no header is given, defaults to the one set up in
|
||||
/// your TOML configuration file.
|
||||
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
#[arg(conflicts_with = "no_headers")]
|
||||
pub headers: Vec<String>,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -62,26 +85,20 @@ impl MessageReadCommand {
|
|||
let folder = &self.folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let get_messages_kind = toml_account_config.get_messages_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
.with_peek_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let emails = if self.preview {
|
||||
|
@ -96,22 +113,32 @@ impl MessageReadCommand {
|
|||
for email in emails.to_vec() {
|
||||
bodies.push_str(glue);
|
||||
|
||||
let tpl = email
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if self.no_headers {
|
||||
tpl = tpl.with_hide_all_headers();
|
||||
} else if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
if self.raw {
|
||||
// emails do not always have valid utf8, uses "lossy" to
|
||||
// display what can be displayed
|
||||
bodies.push_str(&String::from_utf8_lossy(email.raw()?));
|
||||
} else {
|
||||
let tpl = email
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if self.no_headers {
|
||||
tpl = tpl.with_hide_all_headers();
|
||||
} else if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
bodies.push_str(&tpl);
|
||||
if self.html {
|
||||
tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into()));
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
bodies.push_str(&tpl);
|
||||
}
|
||||
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.out(bodies)
|
||||
printer.print(bodies)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config, flag::Flag};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
|
||||
printer::Printer,
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Reply to the message associated to the given envelope id.
|
||||
/// Reply to a message.
|
||||
///
|
||||
/// This command allows you to reply to the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
|
@ -40,6 +39,10 @@ pub struct MessageReplyCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -49,25 +52,24 @@ impl MessageReplyCommand {
|
|||
info!("executing reply message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let add_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
|
@ -75,18 +77,13 @@ impl MessageReplyCommand {
|
|||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(account_config.clone())
|
||||
.ok_or(anyhow!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(&account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.with_reply_all(self.reply.all)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
|
||||
|
||||
backend.add_flag(folder, &[id], Flag::Answered).await?;
|
||||
|
||||
Ok(())
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Save the given raw message to the given folder.
|
||||
/// Save a message to a folder.
|
||||
///
|
||||
/// This command allows you to add a raw message to the given folder.
|
||||
#[derive(Debug, Parser)]
|
||||
|
@ -27,6 +23,10 @@ pub struct MessageSaveCommand {
|
|||
#[command(flatten)]
|
||||
pub message: MessageRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -37,23 +37,20 @@ impl MessageSaveCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
},
|
||||
let add_message_kind = toml_account_config.add_message_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
add_message_kind,
|
||||
|builder| builder.set_add_message(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let is_tty = io::stdin().is_terminal();
|
||||
|
@ -71,6 +68,6 @@ impl MessageSaveCommand {
|
|||
|
||||
backend.add_message(folder, msg.as_bytes()).await?;
|
||||
|
||||
printer.out(format!("Message successfully saved to {folder}!\n"))
|
||||
printer.print(format!("Message successfully saved to {folder}!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::info;
|
||||
use email::{backend::feature::BackendFeatureSource, message::send::SendMessageThenSaveCopy};
|
||||
use log::info;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
message::arg::MessageRawArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Send the given raw message.
|
||||
/// Send a message.
|
||||
///
|
||||
/// This command allows you to send a raw message and to save a copy
|
||||
/// to your send folder.
|
||||
|
@ -22,6 +20,10 @@ pub struct MessageSendCommand {
|
|||
#[command(flatten)]
|
||||
pub message: MessageRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -30,23 +32,27 @@ impl MessageSendCommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing send message command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
|
||||
toml_account_config
|
||||
.add_message_kind()
|
||||
.filter(|_| account_config.should_save_copy_sent_message()),
|
||||
);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
send_message_kind,
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let msg = if io::stdin().is_terminal() {
|
||||
|
@ -62,6 +68,6 @@ impl MessageSendCommand {
|
|||
|
||||
backend.send_message_then_save_copy(msg.as_bytes()).await?;
|
||||
|
||||
printer.out("Message successfully sent!")
|
||||
printer.print("Message successfully sent!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::envelope::arg::ids::EnvelopeIdArg;
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Read human-friendly version of messages associated to the
|
||||
/// given envelope id's thread.
|
||||
///
|
||||
/// This command allows you to thread a message. When threading a message,
|
||||
/// the "seen" flag is automatically applied to the corresponding
|
||||
/// envelope. To prevent this behaviour, use the --preview flag.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageThreadCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// Thread the message without applying the "seen" flag to its
|
||||
/// corresponding envelope.
|
||||
#[arg(long, short)]
|
||||
pub preview: bool,
|
||||
|
||||
/// Thread only the body of the message.
|
||||
///
|
||||
/// All headers will be removed from the message.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub no_headers: bool,
|
||||
|
||||
/// List of headers that should be visible at the top of the
|
||||
/// message.
|
||||
///
|
||||
/// If a given header is not found in the message, it will not be
|
||||
/// visible. If no header is given, defaults to the one set up in
|
||||
/// your TOML configuration file.
|
||||
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||
#[arg(conflicts_with = "no_headers")]
|
||||
pub headers: Vec<String>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageThreadCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing thread message(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let id = &self.envelope.id;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
.with_peek_messages(BackendFeatureSource::Context)
|
||||
.with_thread_envelopes(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let envelopes = backend
|
||||
.thread_envelope(folder, *id, Default::default())
|
||||
.await?;
|
||||
|
||||
let ids: Vec<_> = envelopes
|
||||
.graph()
|
||||
.nodes()
|
||||
.map(|e| e.id.parse::<usize>().unwrap())
|
||||
.collect();
|
||||
|
||||
let emails = if self.preview {
|
||||
backend.peek_messages(folder, &ids).await
|
||||
} else {
|
||||
backend.get_messages(folder, &ids).await
|
||||
}?;
|
||||
|
||||
let mut glue = "";
|
||||
let mut bodies = String::default();
|
||||
|
||||
for (i, email) in emails.to_vec().iter().enumerate() {
|
||||
bodies.push_str(glue);
|
||||
bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1]));
|
||||
|
||||
let tpl = email
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if self.no_headers {
|
||||
tpl = tpl.with_hide_all_headers();
|
||||
} else if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
|
||||
bodies.push_str(&tpl);
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.out(bodies)
|
||||
}
|
||||
}
|
|
@ -1,24 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, message::Message},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::{backend::feature::BackendFeatureSource, message::Message};
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||
printer::Printer,
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Compose a new message, from scratch.
|
||||
/// Write a new message.
|
||||
///
|
||||
/// This command allows you to write a new message using the editor
|
||||
/// defined in your environment variable $EDITOR. When the edition
|
||||
|
@ -32,6 +28,10 @@ pub struct MessageWriteCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,28 +40,27 @@ impl MessageWriteCommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing write message command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let add_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = Message::new_tpl_builder(account_config.clone())
|
||||
let tpl = Message::new_tpl_builder(&account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
|
|
179
src/email/message/config.rs
Normal file
179
src/email/message/config.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
#[cfg(feature = "account-sync")]
|
||||
use email::message::sync::config::MessageSyncConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::backend::BackendKind;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageConfig {
|
||||
pub write: Option<MessageAddConfig>,
|
||||
pub send: Option<MessageSendConfig>,
|
||||
pub peek: Option<MessagePeekConfig>,
|
||||
pub read: Option<MessageGetConfig>,
|
||||
pub copy: Option<MessageCopyConfig>,
|
||||
pub r#move: Option<MessageMoveConfig>,
|
||||
pub delete: Option<MessageDeleteConfig>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub sync: Option<MessageSyncConfig>,
|
||||
}
|
||||
|
||||
impl MessageConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(add) = &self.write {
|
||||
kinds.extend(add.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(send) = &self.send {
|
||||
kinds.extend(send.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(peek) = &self.peek {
|
||||
kinds.extend(peek.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(get) = &self.read {
|
||||
kinds.extend(get.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(copy) = &self.copy {
|
||||
kinds.extend(copy.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(move_) = &self.r#move {
|
||||
kinds.extend(move_.get_used_backends());
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageAddConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::message::add::config::MessageWriteConfig,
|
||||
}
|
||||
|
||||
impl MessageAddConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageSendConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::message::send::config::MessageSendConfig,
|
||||
}
|
||||
|
||||
impl MessageSendConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessagePeekConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl MessagePeekConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageGetConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::message::get::config::MessageReadConfig,
|
||||
}
|
||||
|
||||
impl MessageGetConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageCopyConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl MessageCopyConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageMoveConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl MessageMoveConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct MessageDeleteConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl MessageDeleteConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod arg;
|
||||
pub mod attachment;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod template;
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Generate a template for forwarding a message.
|
||||
|
@ -36,6 +34,10 @@ pub struct TemplateForwardCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -46,25 +48,20 @@ impl TemplateForwardCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let get_messages_kind = toml_account_config.get_messages_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
|
@ -72,13 +69,13 @@ impl TemplateForwardCommand {
|
|||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message {id}"))?
|
||||
.to_forward_tpl_builder(account_config)
|
||||
.ok_or(anyhow!("cannot find message {id}"))?
|
||||
.to_forward_tpl_builder(&account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
printer.out(tpl)
|
||||
printer.print(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,25 +4,25 @@ mod save;
|
|||
mod send;
|
||||
mod write;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::{
|
||||
forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand,
|
||||
send::TemplateSendCommand, write::TemplateWriteCommand,
|
||||
};
|
||||
|
||||
/// Generate, save and send message templates.
|
||||
/// Manage templates.
|
||||
///
|
||||
/// A template is an editable version of a message (headers +
|
||||
/// body). It uses a specific language called MML that allows you to
|
||||
/// attach file or encrypt content. This subcommand allows you manage
|
||||
/// them.
|
||||
///
|
||||
/// Learn more about MML at: <https://crates.io/crates/mml-lib>.
|
||||
/// You can learn more about MML at
|
||||
/// <https://crates.io/crates/mml-lib>.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum TemplateSubcommand {
|
||||
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Generate a template for replying to a message.
|
||||
|
@ -40,6 +38,10 @@ pub struct TemplateReplyCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -51,39 +53,34 @@ impl TemplateReplyCommand {
|
|||
let folder = &self.folder.name;
|
||||
let id = self.envelope.id;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let get_messages_kind = toml_account_config.get_messages_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(account_config)
|
||||
.ok_or(anyhow!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(&account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.with_reply_all(self.reply.all)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
printer.out(tpl)
|
||||
printer.print(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use log::info;
|
||||
use mml::MmlCompilerBuilder;
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::info;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
email::template::arg::TemplateRawArg, folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Save a template to a folder.
|
||||
|
@ -31,6 +27,10 @@ pub struct TemplateSaveCommand {
|
|||
#[command(flatten)]
|
||||
pub template: TemplateRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -41,25 +41,20 @@ impl TemplateSaveCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let add_message_kind = toml_account_config.add_message_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
},
|
||||
add_message_kind,
|
||||
|builder| builder.set_add_message(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let is_tty = io::stdin().is_terminal();
|
||||
|
@ -78,13 +73,13 @@ impl TemplateSaveCommand {
|
|||
#[allow(unused_mut)]
|
||||
let mut compiler = MmlCompilerBuilder::new();
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
#[cfg(feature = "pgp")]
|
||||
compiler.set_some_pgp(account_config.pgp.clone());
|
||||
|
||||
let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
|
||||
|
||||
backend.add_message(folder, &msg).await?;
|
||||
|
||||
printer.out(format!("Template successfully saved to {folder}!\n"))
|
||||
printer.print(format!("Template successfully saved to {folder}!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use email::{backend::feature::BackendFeatureSource, message::send::SendMessageThenSaveCopy};
|
||||
use log::info;
|
||||
use mml::MmlCompilerBuilder;
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
email::template::arg::TemplateRawArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Send a template.
|
||||
|
@ -28,6 +23,10 @@ pub struct TemplateSendCommand {
|
|||
#[command(flatten)]
|
||||
pub template: TemplateRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -36,25 +35,27 @@ impl TemplateSendCommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing send template command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
|
||||
toml_account_config
|
||||
.add_message_kind()
|
||||
.filter(|_| account_config.should_save_copy_sent_message()),
|
||||
);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
send_message_kind,
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = if io::stdin().is_terminal() {
|
||||
|
@ -71,13 +72,13 @@ impl TemplateSendCommand {
|
|||
#[allow(unused_mut)]
|
||||
let mut compiler = MmlCompilerBuilder::new();
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
#[cfg(feature = "pgp")]
|
||||
compiler.set_some_pgp(account_config.pgp.clone());
|
||||
|
||||
let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
|
||||
|
||||
backend.send_message_then_save_copy(&msg).await?;
|
||||
|
||||
printer.out("Message successfully sent!")
|
||||
printer.print("Message successfully sent!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{config::Config, message::Message};
|
||||
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
use email::message::Message;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Generate a template for writing a new message from scratch.
|
||||
|
@ -23,6 +23,10 @@ pub struct TemplateWriteCommand {
|
|||
#[command(flatten)]
|
||||
pub body: TemplateRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -31,18 +35,18 @@ impl TemplateWriteCommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing write template command");
|
||||
|
||||
let (_, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (_, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let tpl = Message::new_tpl_builder(Arc::new(account_config))
|
||||
let tpl = Message::new_tpl_builder(&account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
printer.out(tpl)
|
||||
printer.print(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,14 +10,6 @@ pub struct FolderNameOptionalFlag {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
impl Default for FolderNameOptionalFlag {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: INBOX.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional folder name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderNameOptionalArg {
|
||||
|
|
|
@ -1,61 +1,55 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::add::AddFolder},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::add::AddFolder};
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Create the given folder.
|
||||
/// Create a new folder.
|
||||
///
|
||||
/// This command allows you to create a new folder using the given
|
||||
/// name.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderAddCommand {
|
||||
pub struct AddFolderCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FolderAddCommand {
|
||||
impl AddFolderCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing create folder command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let add_folder_kind = toml_account_config.add_folder_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
add_folder_kind,
|
||||
|builder| builder.set_add_folder(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.add_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully created!\n"))
|
||||
printer.print(format!("Folder {folder} successfully created!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
use std::{process, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
|
||||
};
|
||||
use tracing::info;
|
||||
use dialoguer::Confirm;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder};
|
||||
use log::info;
|
||||
use std::process;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Delete the given folder.
|
||||
/// Delete a folder.
|
||||
///
|
||||
/// All emails from the given folder are definitely deleted. The
|
||||
/// folder is also deleted after execution of the command.
|
||||
|
@ -25,11 +21,12 @@ pub struct FolderDeleteCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
#[arg(long, short)]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
impl FolderDeleteCommand {
|
||||
|
@ -38,36 +35,34 @@ impl FolderDeleteCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
if !self.yes {
|
||||
let confirm = format!("Do you really want to delete the folder {folder}");
|
||||
let confirm = format!("{confirm}? All emails will be definitely deleted.");
|
||||
let confirm_msg = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted.");
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(confirm_msg)
|
||||
.default(false)
|
||||
.report(false)
|
||||
.interact_opt()?;
|
||||
if let Some(false) | None = confirm {
|
||||
process::exit(0);
|
||||
};
|
||||
|
||||
if !prompt::bool(confirm, false)? {
|
||||
process::exit(0);
|
||||
};
|
||||
}
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let delete_folder_kind = toml_account_config.delete_folder_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_delete_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
delete_folder_kind,
|
||||
|builder| builder.set_delete_folder(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.delete_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully deleted!\n"))
|
||||
printer.print(format!("Folder {folder} successfully deleted!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, config::Config, folder::expunge::ExpungeFolder,
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::expunge::ExpungeFolder};
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Expunge the given folder.
|
||||
/// Expunge a folder.
|
||||
///
|
||||
/// The concept of expunging is similar to the IMAP one: it definitely
|
||||
/// deletes emails from the given folder that contain the "deleted"
|
||||
|
@ -25,6 +20,10 @@ pub struct FolderExpungeCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -34,27 +33,24 @@ impl FolderExpungeCommand {
|
|||
info!("executing expunge folder command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_expunge_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
let expunge_folder_kind = toml_account_config.expunge_folder_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
expunge_folder_kind,
|
||||
|builder| builder.set_expunge_folder(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.expunge_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully expunged!\n"))
|
||||
printer.print(format!("Folder {folder} successfully expunged!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::list::ListFolders},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{
|
||||
backend::BackendBuilder,
|
||||
config::{Folders, FoldersTable},
|
||||
},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::list::ListFolders};
|
||||
use log::info;
|
||||
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
folder::Folders,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
};
|
||||
|
||||
/// List all folders.
|
||||
///
|
||||
|
@ -23,51 +20,44 @@ use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
|||
#[derive(Debug, Parser)]
|
||||
pub struct FolderListCommand {
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
pub table: TableMaxWidthFlag,
|
||||
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width, in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FolderListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list folders command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let toml_account_config = Arc::new(toml_account_config);
|
||||
let list_folders_kind = toml_account_config.list_folders_kind();
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_list_folders(BackendFeatureSource::Context)
|
||||
},
|
||||
account_config.clone(),
|
||||
list_folders_kind,
|
||||
|builder| builder.set_list_folders(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let folders = Folders::from(backend.list_folders().await?);
|
||||
let table = FoldersTable::from(folders)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(toml_account_config.folder_list_table_preset())
|
||||
.with_some_name_color(toml_account_config.folder_list_table_name_color())
|
||||
.with_some_desc_color(toml_account_config.folder_list_table_desc_color());
|
||||
let folders: Folders = backend.list_folders().await?.into();
|
||||
|
||||
printer.out(table)?;
|
||||
Ok(())
|
||||
printer.print_table(
|
||||
Box::new(folders),
|
||||
PrintTableOpts {
|
||||
format: &account_config.get_message_read_format(),
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,25 +4,24 @@ mod expunge;
|
|||
mod list;
|
||||
mod purge;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::{
|
||||
add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
|
||||
add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
|
||||
list::FolderListCommand, purge::FolderPurgeCommand,
|
||||
};
|
||||
|
||||
/// Create, list and purge your folders (as known as mailboxes).
|
||||
/// Manage folders.
|
||||
///
|
||||
/// A folder (as known as mailbox, or directory) is a messages
|
||||
/// container. This subcommand allows you to manage them.
|
||||
/// A folder (as known as mailbox, or directory) contains one or more
|
||||
/// emails. This subcommand allows you to manage them.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum FolderSubcommand {
|
||||
#[command(visible_alias = "create", alias = "new")]
|
||||
Add(FolderAddCommand),
|
||||
Add(AddFolderCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
List(FolderListCommand),
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
use std::{process, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config, folder::purge::PurgeFolder};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
|
||||
};
|
||||
use tracing::info;
|
||||
use dialoguer::Confirm;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
|
||||
use log::info;
|
||||
use std::process;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Purge the given folder.
|
||||
/// Purge a folder.
|
||||
///
|
||||
/// All emails from the given folder are definitely deleted. The
|
||||
/// purged folder will remain empty after execution of the command.
|
||||
|
@ -22,11 +21,12 @@ pub struct FolderPurgeCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
#[arg(long, short)]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
impl FolderPurgeCommand {
|
||||
|
@ -35,36 +35,34 @@ impl FolderPurgeCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
if !self.yes {
|
||||
let confirm = format!("Do you really want to purge the folder {folder}");
|
||||
let confirm = format!("{confirm}? All emails will be definitely deleted.");
|
||||
|
||||
if !prompt::bool(confirm, false)? {
|
||||
process::exit(0);
|
||||
};
|
||||
let confirm_msg = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted.");
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(confirm_msg)
|
||||
.default(false)
|
||||
.report(false)
|
||||
.interact_opt()?;
|
||||
if let Some(false) | None = confirm {
|
||||
process::exit(0);
|
||||
};
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_purge_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
let purge_folder_kind = toml_account_config.purge_folder_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
purge_folder_kind,
|
||||
|builder| builder.set_purge_folder(BackendFeatureSource::Context),
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.purge_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully purged!\n"))
|
||||
printer.print(format!("Folder {folder} successfully purged!"))
|
||||
}
|
||||
}
|
||||
|
|
135
src/folder/config.rs
Normal file
135
src/folder/config.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
#[cfg(feature = "account-sync")]
|
||||
use email::folder::sync::config::FolderSyncConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::backend::BackendKind;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FolderConfig {
|
||||
#[serde(alias = "aliases")]
|
||||
pub alias: Option<HashMap<String, String>>,
|
||||
pub add: Option<FolderAddConfig>,
|
||||
pub list: Option<FolderListConfig>,
|
||||
pub expunge: Option<FolderExpungeConfig>,
|
||||
pub purge: Option<FolderPurgeConfig>,
|
||||
pub delete: Option<FolderDeleteConfig>,
|
||||
#[cfg(feature = "account-sync")]
|
||||
pub sync: Option<FolderSyncConfig>,
|
||||
}
|
||||
|
||||
impl FolderConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(add) = &self.add {
|
||||
kinds.extend(add.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(list) = &self.list {
|
||||
kinds.extend(list.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(expunge) = &self.expunge {
|
||||
kinds.extend(expunge.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(purge) = &self.purge {
|
||||
kinds.extend(purge.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(delete) = &self.delete {
|
||||
kinds.extend(delete.get_used_backends());
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FolderAddConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FolderAddConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FolderListConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::folder::list::config::FolderListConfig,
|
||||
}
|
||||
|
||||
impl FolderListConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FolderExpungeConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FolderExpungeConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FolderPurgeConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FolderPurgeConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct FolderDeleteConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl FolderDeleteConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
|
@ -1,2 +1,67 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folder {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl From<&email::folder::Folder> for Folder {
|
||||
fn from(folder: &email::folder::Folder) -> Self {
|
||||
Folder {
|
||||
name: folder.name.clone(),
|
||||
desc: folder.desc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Folder {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("DESC").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).blue())
|
||||
.cell(Cell::new(&self.desc).green())
|
||||
}
|
||||
}
|
||||
|
||||
#[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<email::folder::Folders> for Folders {
|
||||
fn from(folders: email::folder::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)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
1
src/imap/mod.rs
Normal file
1
src/imap/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod wizard;
|
557
src/imap/wizard.rs
Normal file
557
src/imap/wizard.rs
Normal file
|
@ -0,0 +1,557 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::{Confirm, Input, Password, Select};
|
||||
#[cfg(feature = "account-discovery")]
|
||||
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
|
||||
use email::{
|
||||
account::config::{
|
||||
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
||||
passwd::PasswdConfig,
|
||||
},
|
||||
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
|
||||
};
|
||||
use oauth::v2_0::{AuthorizationCodeGrant, Client};
|
||||
use secret::Secret;
|
||||
|
||||
use crate::{
|
||||
backend::config::BackendConfig,
|
||||
ui::{prompt, THEME},
|
||||
wizard_log, wizard_prompt,
|
||||
};
|
||||
|
||||
const ENCRYPTIONS: &[ImapEncryptionKind] = &[
|
||||
ImapEncryptionKind::Tls,
|
||||
ImapEncryptionKind::StartTls,
|
||||
ImapEncryptionKind::None,
|
||||
];
|
||||
|
||||
const XOAUTH2: &str = "XOAUTH2";
|
||||
const OAUTHBEARER: &str = "OAUTHBEARER";
|
||||
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
||||
|
||||
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
|
||||
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
|
||||
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
|
||||
const CMD: &str = "Ask me a shell command that exposes my password";
|
||||
|
||||
#[cfg(feature = "account-discovery")]
|
||||
pub(crate) async fn configure(
|
||||
account_name: &str,
|
||||
email: &str,
|
||||
autoconfig: Option<&AutoConfig>,
|
||||
) -> Result<BackendConfig> {
|
||||
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
|
||||
let autoconfig_server = autoconfig.and_then(|c| {
|
||||
c.email_provider()
|
||||
.incoming_servers()
|
||||
.into_iter()
|
||||
.find(|server| matches!(server.server_type(), ServerType::Imap))
|
||||
});
|
||||
|
||||
let autoconfig_host = autoconfig_server
|
||||
.and_then(|s| s.hostname())
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
let default_host =
|
||||
autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1));
|
||||
|
||||
let host = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP hostname")
|
||||
.default(default_host)
|
||||
.interact()?;
|
||||
|
||||
let autoconfig_encryption = autoconfig_server
|
||||
.and_then(|imap| {
|
||||
imap.security_type().map(|encryption| match encryption {
|
||||
SecurityType::Plain => ImapEncryptionKind::None,
|
||||
SecurityType::Starttls => ImapEncryptionKind::StartTls,
|
||||
SecurityType::Tls => ImapEncryptionKind::Tls,
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let default_encryption_idx = match &autoconfig_encryption {
|
||||
ImapEncryptionKind::Tls => 0,
|
||||
ImapEncryptionKind::StartTls => 1,
|
||||
ImapEncryptionKind::None => 2,
|
||||
};
|
||||
|
||||
let encryption_idx = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP encryption")
|
||||
.items(ENCRYPTIONS)
|
||||
.default(default_encryption_idx)
|
||||
.interact_opt()?;
|
||||
|
||||
let autoconfig_port = autoconfig_server
|
||||
.and_then(|s| s.port())
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| match &autoconfig_encryption {
|
||||
ImapEncryptionKind::Tls => 993,
|
||||
ImapEncryptionKind::StartTls => 143,
|
||||
ImapEncryptionKind::None => 143,
|
||||
});
|
||||
|
||||
let (encryption, default_port) = match encryption_idx {
|
||||
Some(idx) if idx == default_encryption_idx => {
|
||||
(Some(autoconfig_encryption), autoconfig_port)
|
||||
}
|
||||
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
|
||||
(Some(ImapEncryptionKind::Tls), 993)
|
||||
}
|
||||
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
|
||||
(Some(ImapEncryptionKind::StartTls), 143)
|
||||
}
|
||||
_ => (Some(ImapEncryptionKind::None), 143),
|
||||
};
|
||||
|
||||
let port = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP port")
|
||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||
.default(default_port.to_string())
|
||||
.interact()
|
||||
.map(|input| input.parse::<u16>().unwrap())?;
|
||||
|
||||
let autoconfig_login = autoconfig_server.map(|imap| match imap.username() {
|
||||
Some("%EMAILLOCALPART%") => email.rsplit_once('@').unwrap().0.to_owned(),
|
||||
Some("%EMAILADDRESS%") => email.to_owned(),
|
||||
_ => email.to_owned(),
|
||||
});
|
||||
|
||||
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
|
||||
|
||||
let login = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP login")
|
||||
.default(default_login)
|
||||
.interact()?;
|
||||
|
||||
let default_oauth2_enabled = autoconfig_server
|
||||
.and_then(|imap| {
|
||||
imap.authentication_type()
|
||||
.into_iter()
|
||||
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
|
||||
})
|
||||
.filter(|_| autoconfig_oauth2.is_some())
|
||||
.unwrap_or_default();
|
||||
|
||||
let oauth2_enabled = Confirm::new()
|
||||
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
|
||||
.default(default_oauth2_enabled)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let auth = if oauth2_enabled {
|
||||
let mut config = OAuth2Config::default();
|
||||
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
|
||||
let redirect_port = OAuth2Config::get_first_available_port()?;
|
||||
|
||||
let method_idx = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 mechanism")
|
||||
.items(OAUTH2_MECHANISMS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
config.method = match method_idx {
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||
_ => OAuth2Method::XOAuth2,
|
||||
};
|
||||
|
||||
config.client_id = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 client id")
|
||||
.interact()?;
|
||||
|
||||
let client_secret: String = Password::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 client secret")
|
||||
.interact()?;
|
||||
config.client_secret =
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"));
|
||||
config
|
||||
.client_secret
|
||||
.set_keyring_entry_secret(&client_secret)
|
||||
.await?;
|
||||
|
||||
let default_auth_url = autoconfig_oauth2
|
||||
.map(|o| o.auth_url().to_owned())
|
||||
.unwrap_or_default();
|
||||
config.auth_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 authorization URL")
|
||||
.default(default_auth_url)
|
||||
.interact()?;
|
||||
|
||||
let default_token_url = autoconfig_oauth2
|
||||
.map(|o| o.token_url().to_owned())
|
||||
.unwrap_or_default();
|
||||
config.token_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 token URL")
|
||||
.default(default_token_url)
|
||||
.interact()?;
|
||||
|
||||
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
|
||||
|
||||
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
|
||||
Ok(match &autoconfig_scopes {
|
||||
Some(scopes) => Select::with_theme(&*THEME)
|
||||
.with_prompt(prompt)
|
||||
.items(scopes)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
.and_then(|idx| scopes.get(idx))
|
||||
.map(|scope| scope.to_string()),
|
||||
None => Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt(prompt)
|
||||
.default(String::default())
|
||||
.interact()?
|
||||
.to_owned(),
|
||||
)
|
||||
.filter(|scope| !scope.is_empty()),
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
|
||||
config.scopes = OAuth2Scopes::Scope(scope);
|
||||
}
|
||||
|
||||
let confirm_additional_scope = || -> Result<bool> {
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to add more IMAP OAuth 2.0 scopes?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(confirm)
|
||||
};
|
||||
|
||||
while confirm_additional_scope()? {
|
||||
let mut scopes = match config.scopes {
|
||||
OAuth2Scopes::Scope(scope) => vec![scope],
|
||||
OAuth2Scopes::Scopes(scopes) => scopes,
|
||||
};
|
||||
|
||||
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
|
||||
scopes.push(scope)
|
||||
}
|
||||
|
||||
config.scopes = OAuth2Scopes::Scopes(scopes);
|
||||
}
|
||||
|
||||
config.pkce = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to enable PKCE verification?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or(true);
|
||||
|
||||
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
|
||||
|
||||
let client = Client::new(
|
||||
config.client_id.clone(),
|
||||
client_secret,
|
||||
config.auth_url.clone(),
|
||||
config.token_url.clone(),
|
||||
)?
|
||||
.with_redirect_host(redirect_host.to_owned())
|
||||
.with_redirect_port(redirect_port)
|
||||
.build()?;
|
||||
|
||||
let mut auth_code_grant = AuthorizationCodeGrant::new()
|
||||
.with_redirect_host(redirect_host.to_owned())
|
||||
.with_redirect_port(redirect_port);
|
||||
|
||||
if config.pkce {
|
||||
auth_code_grant = auth_code_grant.with_pkce();
|
||||
}
|
||||
|
||||
for scope in config.scopes.clone() {
|
||||
auth_code_grant = auth_code_grant.with_scope(scope);
|
||||
}
|
||||
|
||||
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
|
||||
|
||||
println!("{redirect_url}");
|
||||
println!();
|
||||
|
||||
let (access_token, refresh_token) = auth_code_grant
|
||||
.wait_for_redirection(&client, csrf_token)
|
||||
.await?;
|
||||
|
||||
config.access_token =
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"));
|
||||
config
|
||||
.access_token
|
||||
.set_keyring_entry_secret(access_token)
|
||||
.await?;
|
||||
|
||||
if let Some(refresh_token) = &refresh_token {
|
||||
config.refresh_token =
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"));
|
||||
config
|
||||
.refresh_token
|
||||
.set_keyring_entry_secret(refresh_token)
|
||||
.await?;
|
||||
}
|
||||
|
||||
ImapAuthConfig::OAuth2(config)
|
||||
} else {
|
||||
let secret_idx = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP authentication strategy")
|
||||
.items(SECRETS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let secret = match secret_idx {
|
||||
Some(idx) if SECRETS[idx] == KEYRING => {
|
||||
let secret = Secret::new_keyring_entry(format!("{account_name}-imap-passwd"));
|
||||
secret
|
||||
.set_keyring_entry_secret(prompt::passwd("IMAP password")?)
|
||||
.await?;
|
||||
secret
|
||||
}
|
||||
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
|
||||
Some(idx) if SECRETS[idx] == CMD => Secret::new_cmd(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Shell command")
|
||||
.default(format!("pass show {account_name}-imap-passwd"))
|
||||
.interact()?,
|
||||
),
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
ImapAuthConfig::Passwd(PasswdConfig(secret))
|
||||
};
|
||||
|
||||
let config = ImapConfig {
|
||||
host,
|
||||
port,
|
||||
encryption,
|
||||
login,
|
||||
auth,
|
||||
watch: None,
|
||||
};
|
||||
|
||||
Ok(BackendConfig::Imap(config))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "account-discovery"))]
|
||||
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||
let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1);
|
||||
|
||||
let host = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP hostname")
|
||||
.default(default_host)
|
||||
.interact()?;
|
||||
|
||||
let encryption_idx = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP encryption")
|
||||
.items(ENCRYPTIONS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let (encryption, default_port) = match encryption_idx {
|
||||
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
|
||||
(Some(ImapEncryptionKind::Tls), 993)
|
||||
}
|
||||
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
|
||||
(Some(ImapEncryptionKind::StartTls), 143)
|
||||
}
|
||||
_ => (Some(ImapEncryptionKind::None), 143),
|
||||
};
|
||||
|
||||
let port = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP port")
|
||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||
.default(default_port.to_string())
|
||||
.interact()
|
||||
.map(|input| input.parse::<u16>().unwrap())?;
|
||||
|
||||
let default_login = email.to_owned();
|
||||
|
||||
let login = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP login")
|
||||
.default(default_login)
|
||||
.interact()?;
|
||||
|
||||
let oauth2_enabled = Confirm::new()
|
||||
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let auth = if oauth2_enabled {
|
||||
let mut config = OAuth2Config::default();
|
||||
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
|
||||
let redirect_port = OAuth2Config::get_first_available_port()?;
|
||||
|
||||
let method_idx = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 mechanism")
|
||||
.items(OAUTH2_MECHANISMS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
config.method = match method_idx {
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||
_ => OAuth2Method::XOAuth2,
|
||||
};
|
||||
|
||||
config.client_id = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 client id")
|
||||
.interact()?;
|
||||
|
||||
let client_secret: String = Password::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 client secret")
|
||||
.interact()?;
|
||||
config.client_secret =
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"));
|
||||
config
|
||||
.client_secret
|
||||
.set_keyring_entry_secret(&client_secret)
|
||||
.await?;
|
||||
|
||||
config.auth_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 authorization URL")
|
||||
.interact()?;
|
||||
|
||||
config.token_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 token URL")
|
||||
.interact()?;
|
||||
|
||||
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
|
||||
Ok(Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt(prompt)
|
||||
.default(String::default())
|
||||
.interact()?
|
||||
.to_owned(),
|
||||
)
|
||||
.filter(|scope| !scope.is_empty()))
|
||||
};
|
||||
|
||||
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
|
||||
config.scopes = OAuth2Scopes::Scope(scope);
|
||||
}
|
||||
|
||||
let confirm_additional_scope = || -> Result<bool> {
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to add more IMAP OAuth 2.0 scopes?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(confirm)
|
||||
};
|
||||
|
||||
while confirm_additional_scope()? {
|
||||
let mut scopes = match config.scopes {
|
||||
OAuth2Scopes::Scope(scope) => vec![scope],
|
||||
OAuth2Scopes::Scopes(scopes) => scopes,
|
||||
};
|
||||
|
||||
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
|
||||
scopes.push(scope)
|
||||
}
|
||||
|
||||
config.scopes = OAuth2Scopes::Scopes(scopes);
|
||||
}
|
||||
|
||||
config.pkce = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to enable PKCE verification?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or(true);
|
||||
|
||||
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
|
||||
|
||||
let client = Client::new(
|
||||
config.client_id.clone(),
|
||||
client_secret,
|
||||
config.auth_url.clone(),
|
||||
config.token_url.clone(),
|
||||
)?
|
||||
.with_redirect_host(redirect_host.to_owned())
|
||||
.with_redirect_port(redirect_port)
|
||||
.build()?;
|
||||
|
||||
let mut auth_code_grant = AuthorizationCodeGrant::new()
|
||||
.with_redirect_host(redirect_host.to_owned())
|
||||
.with_redirect_port(redirect_port);
|
||||
|
||||
if config.pkce {
|
||||
auth_code_grant = auth_code_grant.with_pkce();
|
||||
}
|
||||
|
||||
for scope in config.scopes.clone() {
|
||||
auth_code_grant = auth_code_grant.with_scope(scope);
|
||||
}
|
||||
|
||||
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
|
||||
|
||||
println!("{redirect_url}");
|
||||
println!();
|
||||
|
||||
let (access_token, refresh_token) = auth_code_grant
|
||||
.wait_for_redirection(&client, csrf_token)
|
||||
.await?;
|
||||
|
||||
config.access_token =
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"));
|
||||
config
|
||||
.access_token
|
||||
.set_keyring_entry_secret(access_token)
|
||||
.await?;
|
||||
|
||||
if let Some(refresh_token) = &refresh_token {
|
||||
config.refresh_token =
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"));
|
||||
config
|
||||
.refresh_token
|
||||
.set_keyring_entry_secret(refresh_token)
|
||||
.await?;
|
||||
}
|
||||
|
||||
ImapAuthConfig::OAuth2(config)
|
||||
} else {
|
||||
let secret_idx = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP authentication strategy")
|
||||
.items(SECRETS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let secret = match secret_idx {
|
||||
Some(idx) if SECRETS[idx] == KEYRING => {
|
||||
let secret = Secret::new_keyring_entry(format!("{account_name}-imap-passwd"));
|
||||
secret
|
||||
.set_keyring_entry_secret(prompt::passwd("IMAP password")?)
|
||||
.await?;
|
||||
secret
|
||||
}
|
||||
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
|
||||
Some(idx) if SECRETS[idx] == CMD => Secret::new_cmd(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Shell command")
|
||||
.default(format!("pass show {account_name}-imap-passwd"))
|
||||
.interact()?,
|
||||
),
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
ImapAuthConfig::Passwd(PasswdConfig(secret))
|
||||
};
|
||||
|
||||
let config = ImapConfig {
|
||||
host,
|
||||
port,
|
||||
encryption,
|
||||
login,
|
||||
auth,
|
||||
watch: None,
|
||||
};
|
||||
|
||||
Ok(BackendConfig::Imap(config))
|
||||
}
|
15
src/lib.rs
15
src/lib.rs
|
@ -1,10 +1,25 @@
|
|||
pub mod account;
|
||||
pub mod backend;
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod completion;
|
||||
pub mod config;
|
||||
pub mod email;
|
||||
pub mod folder;
|
||||
#[cfg(feature = "imap")]
|
||||
pub mod imap;
|
||||
#[cfg(feature = "maildir")]
|
||||
pub mod maildir;
|
||||
pub mod manual;
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub mod notmuch;
|
||||
pub mod output;
|
||||
pub mod printer;
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp")]
|
||||
pub mod smtp;
|
||||
pub mod ui;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::email::{envelope, flag, message};
|
||||
|
|
1
src/maildir/mod.rs
Normal file
1
src/maildir/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod wizard;
|
23
src/maildir/wizard.rs
Normal file
23
src/maildir/wizard.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use dirs::home_dir;
|
||||
use email::maildir::config::MaildirConfig;
|
||||
|
||||
use crate::{backend::config::BackendConfig, ui::THEME};
|
||||
|
||||
pub(crate) fn configure() -> Result<BackendConfig> {
|
||||
let mut config = MaildirConfig::default();
|
||||
|
||||
let mut input = Input::with_theme(&*THEME);
|
||||
|
||||
if let Some(home) = home_dir() {
|
||||
input.default(home.join("Mail").display().to_string());
|
||||
};
|
||||
|
||||
config.root_dir = input
|
||||
.with_prompt("Maildir directory")
|
||||
.interact_text()?
|
||||
.into();
|
||||
|
||||
Ok(BackendConfig::Maildir(config))
|
||||
}
|
46
src/main.rs
46
src/main.rs
|
@ -1,28 +1,31 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV};
|
||||
use himalaya::{
|
||||
cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand,
|
||||
message::command::mailto::MessageMailtoCommand,
|
||||
};
|
||||
use pimalaya_tui::terminal::{
|
||||
cli::{printer::StdoutPrinter, tracing},
|
||||
config::TomlConfig as _,
|
||||
cli::Cli, config::TomlConfig, envelope::command::list::ListEnvelopesCommand,
|
||||
message::command::mailto::MessageMailtoCommand, printer::StdoutPrinter,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let tracing = tracing::install()?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err((_, err)) = coredump::register_panic_handler() {
|
||||
debug!("cannot register coredump panic handler: {err}");
|
||||
trace!("{err:?}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
secret::keyring::set_global_service_name("himalaya-cli");
|
||||
LoggerBuilder::new()
|
||||
.parse_env(Env::new().filter_or(DEFAULT_FILTER_ENV, "warn"))
|
||||
.format_timestamp(None)
|
||||
.init();
|
||||
|
||||
// if the first argument starts by "mailto:", execute straight the
|
||||
// mailto message command
|
||||
let mailto = std::env::args()
|
||||
if let Some(ref url) = std::env::args()
|
||||
.nth(1)
|
||||
.filter(|arg| arg.starts_with("mailto:"));
|
||||
|
||||
if let Some(ref url) = mailto {
|
||||
.filter(|arg| arg.starts_with("mailto:"))
|
||||
{
|
||||
let mut printer = StdoutPrinter::default();
|
||||
let config = TomlConfig::from_default_paths().await?;
|
||||
|
||||
|
@ -32,16 +35,15 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
let cli = Cli::parse();
|
||||
let mut printer = StdoutPrinter::new(cli.output);
|
||||
let res = match cli.command {
|
||||
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
|
||||
let mut printer = StdoutPrinter::new(cli.output, cli.color);
|
||||
|
||||
match cli.command {
|
||||
Some(cmd) => cmd.execute(&mut printer, cli.config_path.as_ref()).await,
|
||||
None => {
|
||||
let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
|
||||
EnvelopeListCommand::default()
|
||||
let config = TomlConfig::from_some_path_or_default(cli.config_path.as_ref()).await?;
|
||||
ListEnvelopesCommand::default()
|
||||
.execute(&mut printer, &config)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
tracing.with_debug_and_trace_notes(res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_mangen::Man;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
use log::info;
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use tracing::info;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::cli::Cli;
|
||||
use crate::{cli::Cli, printer::Printer};
|
||||
|
||||
/// Generate manual pages to the given directory.
|
||||
/// Generate manual pages to a directory.
|
||||
///
|
||||
/// This command allows you to generate manual pages (following the
|
||||
/// man page format) to the given directory. If the directory does not
|
||||
|
@ -35,7 +33,7 @@ impl ManualGenerateCommand {
|
|||
Man::new(cmd).render(&mut buffer)?;
|
||||
|
||||
fs::create_dir_all(&self.dir)?;
|
||||
printer.log(format!("Generating man page for command {cmd_name}…\n"))?;
|
||||
printer.print_log(format!("Generating man page for command {cmd_name}…"))?;
|
||||
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
|
||||
|
||||
for subcmd in subcmds {
|
||||
|
@ -44,18 +42,16 @@ impl ManualGenerateCommand {
|
|||
let mut buffer = Vec::new();
|
||||
Man::new(subcmd).render(&mut buffer)?;
|
||||
|
||||
printer.log(format!(
|
||||
"Generating man page for subcommand {subcmd_name}…\n"
|
||||
))?;
|
||||
printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?;
|
||||
fs::write(
|
||||
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
|
||||
buffer,
|
||||
)?;
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"{subcmds_len} man page(s) successfully generated in {}!\n",
|
||||
self.dir.display()
|
||||
printer.print(format!(
|
||||
"{subcmds_len} man page(s) successfully generated in {:?}!",
|
||||
self.dir
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
|
|
1
src/notmuch/mod.rs
Normal file
1
src/notmuch/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod wizard;
|
24
src/notmuch/wizard.rs
Normal file
24
src/notmuch/wizard.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use email::notmuch::config::NotmuchConfig;
|
||||
|
||||
use crate::{backend::config::BackendConfig, ui::THEME};
|
||||
|
||||
pub(crate) fn configure() -> Result<BackendConfig> {
|
||||
let mut config = NotmuchConfig::default();
|
||||
|
||||
let default_database_path = NotmuchConfig::get_default_database_path()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
config.database_path = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Notmuch database path")
|
||||
.default(default_database_path)
|
||||
.interact_text()?
|
||||
.into(),
|
||||
);
|
||||
|
||||
Ok(BackendConfig::Notmuch(config))
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue