mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-21 16:53:39 +00:00
Compare commits
294 commits
v1.0.0-bet
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cf008c0ca7 | ||
![]() |
d697cbc16b | ||
![]() |
26feecd80a | ||
![]() |
55bd9247e7 | ||
![]() |
15a9a4a69f | ||
![]() |
2b25a4d1fb | ||
![]() |
12afd84d2b | ||
![]() |
5632fdac3b | ||
![]() |
21a97b83f3 | ||
![]() |
0f6217e6e5 | ||
![]() |
9d773e947b | ||
![]() |
c79cabc168 | ||
![]() |
f5695cad53 | ||
![]() |
25edd9e106 | ||
![]() |
1e26033e28 | ||
![]() |
354848baf4 | ||
![]() |
d51ba0850a | ||
![]() |
e3cbbbc6c4 | ||
![]() |
50b8d3667e | ||
![]() |
9aa408ae17 | ||
![]() |
97e40b5f59 | ||
![]() |
dc5b8a34c8 | ||
![]() |
0eb7bfc419 | ||
![]() |
082e680b32 | ||
![]() |
1928b36859 | ||
![]() |
7ab615054f | ||
![]() |
b503027585 | ||
![]() |
3ceef291a1 | ||
![]() |
5eeda248fd | ||
![]() |
118a3f9779 | ||
![]() |
4953aae860 | ||
![]() |
bf3d5342c2 | ||
![]() |
1db785ac0b | ||
![]() |
ce0b2dd8d3 | ||
![]() |
6e658fef33 | ||
![]() |
0302a77f73 | ||
![]() |
77d2292e5c | ||
![]() |
f9f2aaeab7 | ||
![]() |
4b731f3cca | ||
![]() |
7aa576400a | ||
![]() |
eafeeb28a4 | ||
![]() |
85a12a54c0 | ||
![]() |
1a193f3ec3 | ||
![]() |
60dc3afd5b | ||
![]() |
f92b6a6bb5 | ||
![]() |
86baf1c483 | ||
![]() |
d262418baa | ||
![]() |
eb65464e34 | ||
![]() |
0917caa400 | ||
![]() |
f0fbd3d213 | ||
![]() |
d2ee5dbf98 | ||
![]() |
0ae35beb0d | ||
![]() |
0db15511c5 | ||
![]() |
b55935cc39 | ||
![]() |
b6a062d8bd | ||
![]() |
cb077131b2 | ||
![]() |
6644801452 | ||
![]() |
9e15acf14f | ||
![]() |
085aea0fe9 | ||
![]() |
f94e592d63 | ||
![]() |
6a67d18683 | ||
![]() |
eca47cf2f7 | ||
![]() |
4cf8b2ded0 | ||
![]() |
74fcc0d44f | ||
![]() |
6dc448b062 | ||
![]() |
7806de626e | ||
![]() |
b6faf069cb | ||
![]() |
250ef63030 | ||
![]() |
4781f92ce8 | ||
![]() |
6b45314f1a | ||
![]() |
842db08710 | ||
![]() |
69e66b307a | ||
![]() |
d7c565cadc | ||
![]() |
91ca961e3d | ||
![]() |
5a1a835791 | ||
![]() |
98715db67b | ||
![]() |
d1c5d0397e | ||
![]() |
8f9e016936 | ||
![]() |
b4f337ea0d | ||
![]() |
065493ac7a | ||
![]() |
e17c2544f3 | ||
![]() |
f55fa1faad | ||
![]() |
eb07cb60d7 | ||
![]() |
41f5aa4fe4 | ||
![]() |
126756a2a4 | ||
![]() |
2054618ce8 | ||
![]() |
e8a74bb156 | ||
![]() |
d3cf63a39e | ||
![]() |
dd860c5bf0 | ||
![]() |
bc8f0b3c51 | ||
![]() |
c51e411dc1 | ||
![]() |
4fb6d6569d | ||
![]() |
aa698e0572 | ||
![]() |
6206970f47 | ||
![]() |
e64286c341 | ||
![]() |
54de48c98a | ||
![]() |
5fbd407b44 | ||
![]() |
5330073b98 | ||
![]() |
105b8309dd | ||
![]() |
49de48151b | ||
![]() |
a38897d880 | ||
![]() |
b22aa9dcb0 | ||
![]() |
025eebb549 | ||
![]() |
199355b7d9 | ||
![]() |
51ba814ac1 | ||
![]() |
17af039600 | ||
![]() |
676eb30cd0 | ||
![]() |
8bb28cf7a3 | ||
![]() |
c8f226bbaa | ||
![]() |
c23cb86691 | ||
![]() |
d826d8ddde | ||
![]() |
abbd67e5e1 | ||
![]() |
bbb09ec03b | ||
![]() |
18cc8a8769 | ||
![]() |
fc5c6816a5 | ||
![]() |
358affb6bf | ||
![]() |
624a44f773 | ||
![]() |
eb959c29e3 | ||
![]() |
2b1973dbce | ||
![]() |
fda9cf1e4f | ||
![]() |
379d1dc97d | ||
![]() |
72d4397012 | ||
![]() |
311c87ecca | ||
![]() |
2b670d90f0 | ||
![]() |
38488e93db | ||
![]() |
82aca9c9ba | ||
![]() |
623e2e0f68 | ||
![]() |
557d341b24 | ||
![]() |
73fbb8ebf6 | ||
![]() |
f04362572f | ||
![]() |
7ae109eaae | ||
![]() |
fcfb7adb16 | ||
![]() |
ba4d2758cd | ||
![]() |
44d94be99d | ||
![]() |
3852b5abca | ||
![]() |
f250551c91 | ||
![]() |
d9db1af6b2 | ||
![]() |
c6b674ed1d | ||
![]() |
b6d690188a | ||
![]() |
7a9a4b5b1f | ||
![]() |
80e0b54a26 | ||
![]() |
af1cd2b895 | ||
![]() |
3e6a07821c | ||
![]() |
4d243aaa9d | ||
![]() |
bed96518c2 | ||
![]() |
3290074618 | ||
![]() |
2b115dd284 | ||
![]() |
f35fa7cdb4 | ||
![]() |
dc5ca1999b | ||
![]() |
d9699a3cb9 | ||
![]() |
524b22cb5e | ||
![]() |
cffce1140a | ||
![]() |
c4ae8626ed | ||
![]() |
a9bb2d287f | ||
![]() |
6f86884ab4 | ||
![]() |
7641090c4e | ||
![]() |
ddef9e5cc8 | ||
![]() |
942bf5d163 | ||
![]() |
8c08b67be3 | ||
![]() |
4fb7ff93db | ||
![]() |
83621cc0c0 | ||
![]() |
78b2be8499 | ||
![]() |
e814b8aec2 | ||
![]() |
772f689eda | ||
![]() |
924d3cfefd | ||
![]() |
4597b01e78 | ||
![]() |
90c3bef172 | ||
![]() |
590d0a0e8c | ||
![]() |
d62486f4c5 | ||
![]() |
5c4b03474e | ||
![]() |
14c77e4629 | ||
![]() |
a0485ff8d1 | ||
![]() |
c36e72b5f6 | ||
![]() |
2e3a3397a5 | ||
![]() |
53dc4c2e97 | ||
![]() |
a88843669a | ||
![]() |
59ed5f8687 | ||
![]() |
130629309c | ||
![]() |
d7c4abf2e3 | ||
![]() |
36f3690cba | ||
![]() |
396a91a322 | ||
![]() |
d4b81a8294 | ||
![]() |
92814d6043 | ||
![]() |
fecbae001c | ||
![]() |
ff1996107b | ||
![]() |
d54e2b31b7 | ||
![]() |
c44cac50eb | ||
![]() |
7b55da8c40 | ||
![]() |
52aa6336a0 | ||
![]() |
7c17f801eb | ||
![]() |
b6068ef9e7 | ||
![]() |
6ff3771135 | ||
![]() |
151adf09e6 | ||
![]() |
0101f7bf34 | ||
![]() |
3b271c3e67 | ||
![]() |
a0dea19cdf | ||
![]() |
2386d0f517 | ||
![]() |
2083e106f8 | ||
![]() |
32b72fb769 | ||
![]() |
55ecb547c1 | ||
![]() |
24c9e3b384 | ||
![]() |
63cf9ca3da | ||
![]() |
08f299f186 | ||
![]() |
553ecd3c23 | ||
![]() |
47d61d2c3d | ||
![]() |
e1f6739be3 | ||
![]() |
ab56c493aa | ||
![]() |
adadc78743 | ||
![]() |
ff56268fb3 | ||
![]() |
5d0ea22e3c | ||
![]() |
90183fc302 | ||
![]() |
a6344d682d | ||
![]() |
2d53144c7c | ||
![]() |
a61ff559e6 | ||
![]() |
6b2e018ea3 | ||
![]() |
0cf6ba01ce | ||
![]() |
360284184e | ||
![]() |
a6300e6498 | ||
![]() |
6789993913 | ||
![]() |
068bb4c853 | ||
![]() |
34fbf5b603 | ||
![]() |
681837b48d | ||
![]() |
ee91a41fbb | ||
![]() |
e31bbf4b7b | ||
![]() |
2b5e2c1c14 | ||
![]() |
bdb78f98ba | ||
![]() |
74ec31014c | ||
![]() |
afd7d79e41 | ||
![]() |
a2fa0dcf55 | ||
![]() |
cfc88118bb | ||
![]() |
cce0baf81a | ||
![]() |
b92d7b4a08 | ||
![]() |
6f5f943875 | ||
![]() |
5a22cab781 | ||
![]() |
c5b33b9623 | ||
![]() |
248a7b97a2 | ||
![]() |
bd2a425832 | ||
![]() |
3f4a1e7eb2 | ||
![]() |
bec2522e7f | ||
![]() |
3044dda8f4 | ||
![]() |
f793d60ca2 | ||
![]() |
3fa617cf8f | ||
![]() |
cd3f5ff6a6 | ||
![]() |
3d9c45e374 | ||
![]() |
48382b3e45 | ||
![]() |
b93642b3bc | ||
![]() |
519955fb96 | ||
![]() |
470815a227 | ||
![]() |
d823f32c31 | ||
![]() |
cf064f8e0d | ||
![]() |
8ccabf1fc0 | ||
![]() |
daf2c7c87a | ||
![]() |
444efc6beb | ||
![]() |
0ccee5082a | ||
![]() |
b45944ef46 | ||
![]() |
d85bc1e8ae | ||
![]() |
146f5f628a | ||
![]() |
d26314cd48 | ||
![]() |
f9b92e6e7a | ||
![]() |
c6cf93a276 | ||
![]() |
f1371f42e4 | ||
![]() |
ec3f915922 | ||
![]() |
16d273febc | ||
![]() |
b773218c94 | ||
![]() |
1b35da2d07 | ||
![]() |
6cbfc57c83 | ||
![]() |
2eff215934 | ||
![]() |
55ba892436 | ||
![]() |
90e12ddc51 | ||
![]() |
7a951b4830 | ||
![]() |
f3151c3f84 | ||
![]() |
098ae380c3 | ||
![]() |
1e448e56eb | ||
![]() |
d54dd6429e | ||
![]() |
9dee1784df | ||
![]() |
c779081381 | ||
![]() |
ccddfeb799 | ||
![]() |
30f00d0867 | ||
![]() |
3c417d14eb | ||
![]() |
8d0f013374 | ||
![]() |
a389434fde | ||
![]() |
095d519dd0 | ||
![]() |
087a0821bc | ||
![]() |
cf6000f1e4 | ||
![]() |
b4fcb427a4 | ||
![]() |
849deb9a20 | ||
![]() |
c022e66289 | ||
![]() |
5003abe1e1 | ||
![]() |
4590348bf2 | ||
![]() |
a066774f22 | ||
![]() |
c57988770a | ||
![]() |
9b1a090329 | ||
![]() |
7fbd97ceba | ||
![]() |
7899484942 |
118 changed files with 5189 additions and 9179 deletions
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
|
@ -1 +1,6 @@
|
|||
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
Normal file
42
.github/workflows/release-on-demand.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
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 }}
|
74
.github/workflows/release.yml
vendored
74
.github/workflows/release.yml
vendored
|
@ -1,74 +0,0 @@
|
|||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create release
|
||||
id: create-release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
deploy-releases:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: create-release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-linux
|
||||
os: ubuntu-latest
|
||||
- target: arm64-linux
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-windows
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-macos
|
||||
os: macos-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 archive
|
||||
run: |
|
||||
nix build -L .#${{ matrix.target }}
|
||||
cp result/bin/himalaya* .
|
||||
- name: Upload tgz release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya.${{ matrix.target }}.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya.${{ matrix.target }}.zip
|
||||
asset_content_type: application/zip
|
15
.github/workflows/releases.yml
vendored
Normal file
15
.github/workflows/releases.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: pimalaya/nix/.github/workflows/releases.yml@master
|
||||
secrets: inherit
|
||||
with:
|
||||
project: himalaya
|
175
CHANGELOG.md
175
CHANGELOG.md
|
@ -7,6 +7,148 @@ 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
|
||||
|
@ -771,7 +913,10 @@ Few major concepts changed:
|
|||
- Password from command
|
||||
- Set up README
|
||||
|
||||
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...HEAD
|
||||
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0...HEAD
|
||||
[1.0.0]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.4...v1.0.0
|
||||
[1.0.0-beta.4]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.3...v1.0.0-beta.4
|
||||
[1.0.0-beta.3]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...v1.0.0-beta.3
|
||||
[1.0.0-beta.2]: https://github.com/soywod/himalaya/compare/v1.0.0-beta...v1.0.0-beta.2
|
||||
[1.0.0-beta]: https://github.com/soywod/himalaya/compare/v0.9.0...v1.0.0-beta
|
||||
[0.9.0]: https://github.com/soywod/himalaya/compare/v0.8.4...v0.9.0
|
||||
|
@ -811,17 +956,17 @@ 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
|
||||
|
||||
[#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
|
||||
[#184]: https://todo.sr.ht/~soywod/pimalaya/184
|
||||
[#188]: https://todo.sr.ht/~soywod/pimalaya/188
|
||||
[#194]: https://todo.sr.ht/~soywod/pimalaya/194
|
||||
[#195]: https://todo.sr.ht/~soywod/pimalaya/195
|
||||
[#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
|
||||
|
|
|
@ -4,28 +4,66 @@ 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: `cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`, `notmuch`…
|
||||
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.
|
||||
|
||||
```sh
|
||||
# Start a Nix shell
|
||||
$ nix-shell
|
||||
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
|
||||
|
||||
# then build the CLI
|
||||
$ cargo build
|
||||
|
||||
# run the CLI
|
||||
$ cargo run --feature pgp-gpg -- envelope list
|
||||
```text
|
||||
rustup update
|
||||
```
|
||||
|
||||
## Contributing
|
||||
or install manually the following dependencies:
|
||||
|
||||
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:
|
||||
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
|
||||
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
|
||||
|
||||
The equivalent of **GitHub Discussions** are:
|
||||
## 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.
|
||||
```text
|
||||
cargo build
|
||||
```
|
||||
|
||||
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).
|
||||
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 Pull requests** is the SourceHut mailing list. You can "open a new pull request" by sending an email containing a git patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). The simplest way to send a patch is to use [git send-email](https://git-scm.com/docs/git-send-email), follow [this guide](https://git-send-email.io/) to configure git properly.
|
||||
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).
|
||||
|
|
3227
Cargo.lock
generated
3227
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
102
Cargo.toml
102
Cargo.toml
|
@ -1,84 +1,70 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "CLI to manage emails"
|
||||
version = "1.0.0-beta.4"
|
||||
version = "1.1.0"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "smtp", "sync"]
|
||||
categories = ["command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "maildir", "smtp"]
|
||||
homepage = "https://pimalaya.org/"
|
||||
documentation = "https://pimalaya.org/himalaya/cli/latest/"
|
||||
repository = "https://github.com/soywod/himalaya/"
|
||||
documentation = "https://github.com/pimalaya/himalaya/"
|
||||
repository = "https://github.com/pimalaya/himalaya/"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs", "--document-private-items"]
|
||||
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"imap",
|
||||
"maildir",
|
||||
# "notmuch",
|
||||
"smtp",
|
||||
"sendmail",
|
||||
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"]
|
||||
|
||||
"account-discovery",
|
||||
"account-sync",
|
||||
[build-dependencies]
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
|
||||
|
||||
# "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", "maildir"]
|
||||
|
||||
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"] }
|
||||
|
||||
[dependencies]
|
||||
ariadne = "0.2"
|
||||
async-trait = "0.1"
|
||||
clap = { version = "4.4", features = ["derive", "wrap_help"] }
|
||||
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.4"
|
||||
clap_mangen = "0.2"
|
||||
color-eyre = "0.6.3"
|
||||
console = "0.15.2"
|
||||
dialoguer = "0.10.2"
|
||||
dirs = "4"
|
||||
email-lib = { version = "=0.24.1", default-features = false, features = ["derive"] }
|
||||
email_address = "0.2.4"
|
||||
erased-serde = "0.3"
|
||||
indicatif = "0.17"
|
||||
mail-builder = "0.3"
|
||||
md5 = "0.7"
|
||||
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
|
||||
oauth-lib = "=0.1.1"
|
||||
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"] }
|
||||
once_cell = "1.16"
|
||||
process-lib = { version = "=0.4.2", features = ["derive"] }
|
||||
secret-lib = { version = "=0.4.4", features = ["derive"] }
|
||||
open = "5.3"
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["rustls", "email", "path", "cli", "himalaya", "tracing", "sled"] }
|
||||
secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-toml-merge = "0.3"
|
||||
serde_json = "1"
|
||||
shellexpand-utils = "=0.2.1"
|
||||
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"
|
||||
toml_edit = "0.22"
|
||||
tracing = "0.1.40"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
unicode-width = "0.1"
|
||||
tracing = "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"
|
||||
|
|
714
README.md
714
README.md
|
@ -1,98 +1,694 @@
|
|||
# 📫 Himalaya [](https://github.com/soywod/himalaya/releases/latest) [](https://matrix.to/#/#pimalaya.himalaya:matrix.org)
|
||||
<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>
|
||||
|
||||
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).
|
||||
```
|
||||
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
|
||||
```
|
||||
|
||||

|
||||
|
||||
*Disclaimer: the project is under active development, do not use in production before the final `v1.0.0`.*
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- [Folder (aka mailbox) management](https://pimalaya.org/himalaya/cli/latest/usage/advanced/folder/)
|
||||
- Envelope [listing](https://pimalaya.org/himalaya/cli/latest/usage/basic/envelope/list.html), [filtering and sorting](https://pimalaya.org/himalaya/cli/latest/usage/advanced/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)
|
||||
- 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).*
|
||||
|
||||
## Installation
|
||||
|
||||
<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%">
|
||||
<details>
|
||||
<summary>Pre-built binary</summary>
|
||||
|
||||
```bash
|
||||
# Arch Linux (official)
|
||||
$ pacman -S himalaya
|
||||
Himalaya CLI can be installed with the installer:
|
||||
|
||||
# Arch Linux (from sources)
|
||||
$ yay -S himalaya-git
|
||||
*As root:*
|
||||
|
||||
# Homebrew
|
||||
$ brew install himalaya
|
||||
```
|
||||
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | sudo sh
|
||||
```
|
||||
|
||||
# Scoop
|
||||
$ scoop install himalaya
|
||||
*As a regular user:*
|
||||
|
||||
# Cargo
|
||||
$ cargo install himalaya
|
||||
```
|
||||
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh
|
||||
```
|
||||
|
||||
# Nix
|
||||
$ nix-env -i himalaya
|
||||
These commands install the latest binary from the GitHub [releases](https://github.com/pimalaya/himalaya/releases) section.
|
||||
|
||||
# Fedora/CentOS
|
||||
$ dnf copr enable atim/himalaya
|
||||
$ dnf install 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.
|
||||
|
||||
*See the [documentation](https://pimalaya.org/himalaya/cli/latest/installation.html) for other installation methods.*
|
||||
*Such binaries are built with the default cargo features. If you want to enable or disable a feature, please use another installation method.*
|
||||
</details>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<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>
|
||||
|
||||
## Configuration
|
||||
|
||||
*Please read the [documentation](https://pimalaya.org/himalaya/cli/latest/configuration/).*
|
||||
Just run `himalaya`, the wizard will help you to configure your default account.
|
||||
|
||||
## Contributing
|
||||
Accounts can be (re)configured via the wizard using the command `himalaya account configure <name>`.
|
||||
|
||||
*Please read the [contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) for more detailed information.*
|
||||
You can also manually edit your own configuration, from scratch:
|
||||
|
||||
A **bug tracker** is available on [SourceHut](https://todo.sr.ht/~soywod/pimalaya). <sup>[[send an email](mailto:~soywod/pimalaya@todo.sr.ht)]</sup>
|
||||
- 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 **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>
|
||||
<details>
|
||||
<summary>Proton Mail (Bridge)</summary>
|
||||
|
||||
If you want to **report a bug**, please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
|
||||
|
||||
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.
|
||||
- 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 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.
|
||||
```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>
|
||||
|
||||
## Sponsoring
|
||||
|
||||
[](https://nlnet.nl/project/Himalaya/index.html)
|
||||
[](https://nlnet.nl/)
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
- [NGI Assure](https://nlnet.nl/assure/) in 2022
|
||||
- [NGI Zero Entrust](https://nlnet.nl/entrust/) in 2023
|
||||
- [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)*
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
[Unit]
|
||||
Description=Email client Himalaya CLI envelopes watcher service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=%install_dir%/himalaya envelopes watch --account %i
|
||||
ExecSearchPath=/bin
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
|
@ -3,7 +3,7 @@ Type=Application
|
|||
Name=himalaya
|
||||
DesktopName=Himalaya
|
||||
GenericName=Mail Reader
|
||||
Comment=Command-line interface for email management
|
||||
Comment=CLI to manage emails
|
||||
Terminal=true
|
||||
Exec=himalaya %U
|
||||
Categories=Application;Network
|
||||
|
@ -13,4 +13,4 @@ Actions=Compose
|
|||
|
||||
[Desktop Action Compose]
|
||||
Name=Compose
|
||||
Exec=himalaya write %U
|
||||
Exec=himalaya message write %U
|
||||
|
|
7
build.rs
Normal file
7
build.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use pimalaya_tui::build::{features_env, git_envs, target_envs};
|
||||
|
||||
fn main() {
|
||||
features_env(include_str!("./Cargo.toml"));
|
||||
target_envs();
|
||||
git_envs();
|
||||
}
|
|
@ -1,98 +1,651 @@
|
|||
# The account name.
|
||||
[accounts.example]
|
||||
################################################################################
|
||||
###[ Global configuration ]#####################################################
|
||||
################################################################################
|
||||
|
||||
# The current account will be used by default for all other commands.
|
||||
default = true
|
||||
# Default display name for all accounts. It is used to build the full
|
||||
# email address of an account: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# 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"
|
||||
# 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"
|
||||
|
||||
# The signature can be a string or a path to a file.
|
||||
signature = "Regards,"
|
||||
# Default signature delimiter for all accounts. It delimits the end of
|
||||
# the message body from the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# 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
|
||||
# Default downloads directory path for all accounts. It is mostly used
|
||||
# for downloading attachments. Defaults to the system temporary
|
||||
# directory.
|
||||
#
|
||||
downloads-dir = "~/Downloads"
|
||||
|
||||
# Override the default Maildir path for synchronization.
|
||||
sync.dir = "/tmp/himalaya-sync-example"
|
||||
# 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 = "|| |-||| "
|
||||
|
||||
# Filter folders to sync
|
||||
folder.sync.filter.include = ["INBOX"]
|
||||
# folder.sync.filter.exclude = ["All mails"]
|
||||
# folder.sync.filter = "all"
|
||||
# Customizes the color of the NAME column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.name-color = "green"
|
||||
|
||||
# Define main folder aliases
|
||||
folder.alias.inbox = "INBOX"
|
||||
folder.alias.sent = "Sent"
|
||||
folder.alias.drafts = "Drafts"
|
||||
folder.alias.trash = "Trash"
|
||||
# Customizes the color of the BACKENDS column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.backends-color = "blue"
|
||||
|
||||
# Also define custom folder aliases
|
||||
folder.alias.prev-year = "Archives/2023"
|
||||
# Customizes the color of the DEFAULT column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.default-color = "black"
|
||||
|
||||
# Default backend used for all the features like adding folders,
|
||||
# listing envelopes or copying messages.
|
||||
backend = "imap"
|
||||
################################################################################
|
||||
###[ Account configuration ]####################################################
|
||||
################################################################################
|
||||
|
||||
# The account name should be unique.
|
||||
#
|
||||
[accounts.example]
|
||||
|
||||
# Defaultness of the account. The current account will be used by
|
||||
# default in all commands.
|
||||
#
|
||||
default = true
|
||||
|
||||
# The email address associated to the current 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.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# Downloads directory path. It is mostly used for downloading
|
||||
# attachments. Defaults to the system temporary directory.
|
||||
#
|
||||
downloads-dir = "~/downloads"
|
||||
|
||||
|
||||
|
||||
# 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"
|
||||
|
||||
# Customizes the number of folders to show by page.
|
||||
#
|
||||
folder.list.page-size = 10
|
||||
|
||||
# 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 = "|| |-||| "
|
||||
|
||||
# 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"
|
||||
|
||||
# Date are converted to the user's local timezone.
|
||||
# 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`.
|
||||
#
|
||||
envelope.list.datetime-local-tz = true
|
||||
|
||||
# Override the backend used for listing envelopes.
|
||||
# envelope.list.backend = "imap"
|
||||
# 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 = "|| |-||| "
|
||||
|
||||
# Send notification on receiving new envelopes
|
||||
envelope.watch.received.notify.summary = "📬 New message from {sender}"
|
||||
# Customizes the character of the unseen flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.unseen-char = "*"
|
||||
|
||||
# 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 replied flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.replied-char = "R"
|
||||
|
||||
# Shell commands can also be executed when envelopes change
|
||||
# envelope.watch.any.cmd = "mbsync -a"
|
||||
# Customizes the character of the flagged flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.flagged-char = "!"
|
||||
|
||||
# Override the backend used for sending messages.
|
||||
message.send.backend = "smtp"
|
||||
# Customizes the character of the attachment property of the envelope
|
||||
# listing table.
|
||||
#
|
||||
envelope.list.table.attachment-char = "@"
|
||||
|
||||
# Save a copy of sent messages to the sent folder.
|
||||
message.send.save-copy = false
|
||||
# Customizes the color of the ID column of the envelope listing table.
|
||||
#
|
||||
envelope.list.table.id-color = "red"
|
||||
|
||||
# IMAP config
|
||||
imap.host = "localhost"
|
||||
imap.port = 3143
|
||||
imap.login = "example@localhost"
|
||||
# Customizes the color of the FLAGS column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.flags-color = "black"
|
||||
|
||||
# Encryption can be either "tls" (or true), "start-tls" or "none" (or false).
|
||||
imap.encryption = "none"
|
||||
# Customizes the color of the SUBJECT column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.subject-color = "green"
|
||||
|
||||
# Get password from a raw string (not safe)
|
||||
imap.passwd.raw = "password"
|
||||
# Customizes the color of the SENDER column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.sender-color = "blue"
|
||||
|
||||
# Get password from a shell command
|
||||
# imap.passwd.cmd = "echo password"
|
||||
# Customizes the color of the DATE column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.date-color = "yellow"
|
||||
|
||||
# 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
|
||||
|
||||
# SMTP config
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 3025
|
||||
smtp.login = "example@localhost"
|
||||
smtp.encryption = false
|
||||
smtp.passwd.raw = "password"
|
||||
# Defines headers to show at the top of messages when reading them.
|
||||
#
|
||||
message.read.headers = ["From", "To", "Cc", "Subject"]
|
||||
|
||||
# PGP needs to be enabled with one of those cargo feature:
|
||||
# pgp-commands, pgp-gpg or pgp-native
|
||||
# pgp.backend = "gpg"
|
||||
# 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"
|
||||
|
|
41
default.nix
41
default.nix
|
@ -1,12 +1,29 @@
|
|||
# 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
|
||||
{
|
||||
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" ]
|
||||
)
|
||||
|
|
98
flake.lock
generated
98
flake.lock
generated
|
@ -8,11 +8,11 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713081044,
|
||||
"narHash": "sha256-ZwbJDrizU+nzU7wTgokYuu5yK71wLPmOLukiunm5B6Y=",
|
||||
"lastModified": 1732405626,
|
||||
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"rev": "af99e7e9c87389c0a5aaf953478664d7126c2b14",
|
||||
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -21,95 +21,53 @@
|
|||
"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"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"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": 1713145326,
|
||||
"narHash": "sha256-m7+IWM6mkWOg22EC5kRUFCycXsXLSU7hWmHdmBfmC3s=",
|
||||
"lastModified": 1736437047,
|
||||
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "53a2c32bc66f5ae41a28d7a9a49d321172af621e",
|
||||
"rev": "f17b95775191ea44bc426831235d87affb10faba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-23.11",
|
||||
"ref": "staging-next",
|
||||
"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",
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pimalaya": "pimalaya"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1712818880,
|
||||
"narHash": "sha256-VDxsvgj/bNypHq48tQWtc3VRbWvzlFjzKf9ZZIVO10Y=",
|
||||
"lastModified": 1732050317,
|
||||
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "657b33b0cb9bd49085202e91ad5b4676532c9140",
|
||||
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
217
flake.nix
217
flake.nix
|
@ -2,218 +2,25 @@
|
|||
description = "CLI to manage emails";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
|
||||
gitignore = {
|
||||
url = "github:hercules-ci/gitignore.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
# 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";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk = {
|
||||
url = "github:nix-community/naersk";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
pimalaya = {
|
||||
url = "github:pimalaya/nix";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, gitignore, fenix, naersk, ... }:
|
||||
let
|
||||
inherit (gitignore.lib) gitignoreSource;
|
||||
|
||||
staticRustFlags = [ "-Ctarget-feature=+crt-static" ];
|
||||
|
||||
# Map of map matching supported Nix build systems with Rust
|
||||
# cross target systems.
|
||||
crossBuildTargets = {
|
||||
x86_64-linux = {
|
||||
x86_64-linux = {
|
||||
rustTarget = "x86_64-unknown-linux-musl";
|
||||
override = { ... }: { };
|
||||
};
|
||||
|
||||
arm64-linux = rec {
|
||||
rustTarget = "aarch64-unknown-linux-musl";
|
||||
override = { system, pkgs }:
|
||||
let
|
||||
inherit (mkPkgsCross system rustTarget) stdenv;
|
||||
cc = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc"; in
|
||||
rec {
|
||||
TARGET_CC = cc;
|
||||
CARGO_BUILD_RUSTFLAGS = staticRustFlags ++ [ "-Clinker=${cc}" ];
|
||||
postInstall = mkPostInstall {
|
||||
inherit pkgs;
|
||||
bin = "${pkgs.qemu}/bin/qemu-aarch64 ./himalaya";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
x86_64-windows = {
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
override = { system, pkgs }:
|
||||
let
|
||||
inherit (pkgs) pkgsCross zip;
|
||||
inherit (pkgsCross.mingwW64) stdenv windows;
|
||||
cc = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc";
|
||||
wine = pkgs.wine.override { wineBuild = "wine64"; };
|
||||
postInstall = mkPostInstall {
|
||||
inherit pkgs;
|
||||
bin = "${wine}/bin/wine64 ./himalaya.exe";
|
||||
};
|
||||
in
|
||||
{
|
||||
depsBuildBuild = [ stdenv.cc windows.pthreads ];
|
||||
TARGET_CC = cc;
|
||||
CARGO_BUILD_RUSTFLAGS = staticRustFlags ++ [ "-Clinker=${cc}" ];
|
||||
postInstall = ''
|
||||
export WINEPREFIX="$(mktemp -d)"
|
||||
${postInstall}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
x86_64-darwin = {
|
||||
x86_64-macos = {
|
||||
rustTarget = "x86_64-apple-darwin";
|
||||
override = { pkgs, ... }:
|
||||
let inherit (pkgs.darwin.apple_sdk.frameworks) AppKit Cocoa; in
|
||||
{
|
||||
buildInputs = [ Cocoa ];
|
||||
NIX_LDFLAGS = "-F${AppKit}/Library/Frameworks -framework AppKit";
|
||||
};
|
||||
};
|
||||
|
||||
# FIXME: infinite recursion in stdenv?!
|
||||
arm64-macos = {
|
||||
rustTarget = "aarch64-apple-darwin";
|
||||
override = { system, pkgs }:
|
||||
let
|
||||
# inherit (mkPkgsCross system "aarch64-darwin") stdenv;
|
||||
inherit ((mkPkgsCross system "aarch64-darwin").pkgsStatic) stdenv darwin;
|
||||
inherit (darwin.apple_sdk.frameworks) AppKit Cocoa;
|
||||
cc = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc";
|
||||
in
|
||||
rec {
|
||||
buildInputs = [ Cocoa ];
|
||||
NIX_LDFLAGS = "-F${AppKit}/Library/Frameworks -framework AppKit -F${Cocoa}/Library/Frameworks -framework Cocoa";
|
||||
NIX_CFLAGS_COMPILE = "-F${AppKit}/Library/Frameworks -framework AppKit -F${Cocoa}/Library/Frameworks -framework Cocoa";
|
||||
TARGET_CC = cc;
|
||||
CARGO_BUILD_RUSTFLAGS = staticRustFlags ++ [ "-Clinker=${cc}" "-lframework=${Cocoa}/Library/Frameworks" ];
|
||||
postInstall = mkPostInstall {
|
||||
inherit pkgs;
|
||||
bin = "${pkgs.qemu}/bin/qemu-aarch64 ./himalaya";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkToolchain = import ./rust-toolchain.nix fenix;
|
||||
|
||||
mkPkgsCross = buildSystem: crossSystem: import nixpkgs {
|
||||
system = buildSystem;
|
||||
crossSystem.config = crossSystem;
|
||||
};
|
||||
|
||||
mkPostInstall = { pkgs, bin ? "./himalaya" }: with pkgs; ''
|
||||
cd $out/bin
|
||||
mkdir -p {man,completions}
|
||||
${bin} man ./man
|
||||
${bin} completion bash > ./completions/himalaya.bash
|
||||
${bin} completion elvish > ./completions/himalaya.elvish
|
||||
${bin} completion fish > ./completions/himalaya.fish
|
||||
${bin} completion powershell > ./completions/himalaya.powershell
|
||||
${bin} completion zsh > ./completions/himalaya.zsh
|
||||
tar -czf himalaya.tgz himalaya* man completions
|
||||
${zip}/bin/zip -r himalaya.zip himalaya* man completions
|
||||
'';
|
||||
|
||||
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
|
||||
|
||||
# Email env
|
||||
gnupg
|
||||
gpgme
|
||||
msmtp
|
||||
notmuch
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
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 = false;
|
||||
auditable = false;
|
||||
strictDeps = true;
|
||||
CARGO_BUILD_TARGET = targetPlatform;
|
||||
CARGO_BUILD_RUSTFLAGS = staticRustFlags;
|
||||
postInstall = mkPostInstall { inherit pkgs; };
|
||||
} // package;
|
||||
in
|
||||
naersk'.buildPackage package';
|
||||
|
||||
mkPackages = system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
mkPackage' = target: package: mkPackage pkgs system package.rustTarget (package.override { inherit system pkgs; });
|
||||
in
|
||||
builtins.mapAttrs mkPackage' crossBuildTargets.${system};
|
||||
|
||||
mkApp = drv:
|
||||
let exePath = drv.passthru.exePath or "/bin/himalaya"; in
|
||||
{
|
||||
type = "app";
|
||||
program = "${drv}${exePath}";
|
||||
};
|
||||
|
||||
mkApps = buildPlatform:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
mkApp' = target: package: mkApp self.packages.${buildPlatform}.${target};
|
||||
in
|
||||
builtins.mapAttrs mkApp' crossBuildTargets.${buildPlatform};
|
||||
|
||||
supportedSystems = builtins.attrNames crossBuildTargets;
|
||||
mapSupportedSystem = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
apps = mapSupportedSystem mkApps;
|
||||
packages = mapSupportedSystem mkPackages;
|
||||
devShells = mapSupportedSystem mkDevShells;
|
||||
outputs =
|
||||
inputs:
|
||||
(import inputs.pimalaya).mkFlakeOutputs inputs {
|
||||
shell = ./shell.nix;
|
||||
default = ./default.nix;
|
||||
};
|
||||
}
|
||||
|
|
11
install.sh
11
install.sh
|
@ -9,7 +9,7 @@ die() {
|
|||
|
||||
DESTDIR="${DESTDIR:-}"
|
||||
PREFIX="${PREFIX:-"$DESTDIR/usr/local"}"
|
||||
RELEASES_URL="https://github.com/soywod/himalaya/releases"
|
||||
RELEASES_URL="https://github.com/pimalaya/himalaya/releases"
|
||||
|
||||
binary=himalaya
|
||||
system=$(uname -s | tr [:upper:] [:lower:])
|
||||
|
@ -23,14 +23,17 @@ case $system in
|
|||
linux|freebsd)
|
||||
case $machine in
|
||||
x86_64) target=x86_64-linux;;
|
||||
arm64|aarch64) target=arm64-linux;;
|
||||
x86|i386|i686) target=i686-linux;;
|
||||
arm64|aarch64) target=aarch64-linux;;
|
||||
armv6l) target=armv6l-linux;;
|
||||
armv7l) target=armv7l-linux;;
|
||||
*) die "Unsupported machine $machine for system $system";;
|
||||
esac;;
|
||||
|
||||
darwin)
|
||||
case $machine in
|
||||
x86_64) target=x86_64-macos;;
|
||||
arm64|aarch64) target=arm64-macos;;
|
||||
x86_64) target=x86_64-darwin;;
|
||||
arm64|aarch64) target=aarch64-darwin;;
|
||||
*) die "Unsupported machine $machine for system $system";;
|
||||
esac;;
|
||||
|
||||
|
|
20
logo-small.svg
Normal file
20
logo-small.svg
Normal file
|
@ -0,0 +1,20 @@
|
|||
<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>
|
After Width: | Height: | Size: 2.1 KiB |
81
logo.svg
Normal file
81
logo.svg
Normal file
|
@ -0,0 +1,81 @@
|
|||
<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>
|
After Width: | Height: | Size: 14 KiB |
89
package.nix
Normal file
89
package.nix
Normal file
|
@ -0,0 +1,89 @@
|
|||
# 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
|
||||
];
|
||||
};
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
fenix:
|
||||
|
||||
let
|
||||
file = ./rust-toolchain.toml;
|
||||
sha256 = "+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8=";
|
||||
in
|
||||
{
|
||||
fromFile = { system }: fenix.packages.${system}.fromToolchainFile {
|
||||
inherit file sha256;
|
||||
};
|
||||
|
||||
fromTarget = { pkgs, buildPlatform, targetPlatform }:
|
||||
let
|
||||
name = (pkgs.lib.importTOML file).toolchain.channel;
|
||||
fenixPackage = fenix.packages.${buildPlatform};
|
||||
toolchain = fenixPackage.fromToolchainName { inherit name sha256; };
|
||||
targetToolchain = fenixPackage.targets.${targetPlatform}.fromToolchainName { inherit name sha256; };
|
||||
in
|
||||
fenixPackage.combine [
|
||||
toolchain.rustc
|
||||
toolchain.cargo
|
||||
targetToolchain.rust-std
|
||||
];
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.77.0"
|
||||
channel = "1.82.0"
|
||||
profile = "default"
|
||||
components = [ "rust-src", "rust-analyzer" ]
|
||||
components = ["rust-src", "rust-analyzer"]
|
||||
|
|
BIN
screenshot.jpeg
Normal file
BIN
screenshot.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
18
shell.nix
18
shell.nix
|
@ -1,12 +1,6 @@
|
|||
# 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
|
||||
{
|
||||
pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"),
|
||||
...
|
||||
}@args:
|
||||
|
||||
pimalaya.mkShell ({ rustToolchainFile = ./rust-toolchain.toml; } // removeAttrs args [ "pimalaya" ])
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::context::BackendContextBuilder;
|
||||
use tracing::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,113 +1,52 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
use tracing::info;
|
||||
#[cfg(any(feature = "imap", feature = "smtp"))]
|
||||
use tracing::{debug, warn};
|
||||
|
||||
#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))]
|
||||
use crate::ui::prompt;
|
||||
use crate::{account::arg::name::AccountNameArg, config::TomlConfig, printer::Printer};
|
||||
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
|
||||
|
||||
/// Configure an account.
|
||||
/// Configure the given account.
|
||||
///
|
||||
/// This command is mostly used to define or reset passwords managed
|
||||
/// by your global keyring. If you do not use the keyring system, you
|
||||
/// can skip this command.
|
||||
/// This command allows you to configure an existing account or to
|
||||
/// create a new one, using the wizard. The `wizard` cargo feature is
|
||||
/// required.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountConfigureCommand {
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameArg,
|
||||
|
||||
/// Reset keyring passwords.
|
||||
///
|
||||
/// This argument will force passwords to be prompted again, then
|
||||
/// saved to your global keyring.
|
||||
#[arg(long, short)]
|
||||
pub reset: bool,
|
||||
}
|
||||
|
||||
impl AccountConfigureCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing configure account command");
|
||||
#[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;
|
||||
|
||||
let account = &self.account.name;
|
||||
let (_, account_config) = config.into_toml_account_config(Some(account))?;
|
||||
info!("executing account configure command");
|
||||
|
||||
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 path = match config_path {
|
||||
Some(path) => path.clone(),
|
||||
None => TomlConfig::default_path()?,
|
||||
};
|
||||
|
||||
#[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_name = Some(self.account.name.as_str());
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = account_config.pgp {
|
||||
config.reset().await?;
|
||||
}
|
||||
}
|
||||
let account_config = config
|
||||
.accounts
|
||||
.remove(&self.account.name)
|
||||
.unwrap_or_default();
|
||||
|
||||
#[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
|
||||
}
|
||||
}?;
|
||||
}
|
||||
wizard::edit(path, config, account_name, account_config).await?;
|
||||
|
||||
#[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
|
||||
}
|
||||
}?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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 { "" }
|
||||
))
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
|
233
src/account/command/doctor.rs
Normal file
233
src/account/command/doctor.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
use std::{
|
||||
io::{stdout, Write},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{Result, Section};
|
||||
#[cfg(all(feature = "keyring", feature = "imap"))]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::ImapContextBuilder;
|
||||
#[cfg(feature = "maildir")]
|
||||
use email::maildir::MaildirContextBuilder;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::NotmuchContextBuilder;
|
||||
#[cfg(feature = "sendmail")]
|
||||
use email::sendmail::SendmailContextBuilder;
|
||||
#[cfg(all(feature = "keyring", feature = "smtp"))]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::SmtpContextBuilder;
|
||||
use email::{backend::BackendBuilder, config::Config};
|
||||
#[cfg(feature = "keyring")]
|
||||
use pimalaya_tui::terminal::prompt;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Backend, SendingBackend},
|
||||
terminal::config::TomlConfig as _,
|
||||
};
|
||||
|
||||
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
|
||||
|
||||
/// Diagnose and fix the given account.
|
||||
///
|
||||
/// This command diagnoses the given account and can even try to fix
|
||||
/// it. It mostly checks if the configuration is valid, if backends
|
||||
/// can be instanciated and if sessions work as expected.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountDoctorCommand {
|
||||
#[command(flatten)]
|
||||
pub account: OptionalAccountNameArg,
|
||||
|
||||
/// Try to fix the given account.
|
||||
///
|
||||
/// This argument can be used to (re)configure keyring entries for
|
||||
/// example.
|
||||
#[arg(long, short)]
|
||||
pub fix: bool,
|
||||
}
|
||||
|
||||
impl AccountDoctorCommand {
|
||||
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
|
||||
let mut stdout = stdout();
|
||||
|
||||
if let Some(name) = self.account.name.as_ref() {
|
||||
print!("Checking TOML configuration integrity for account {name}… ");
|
||||
} else {
|
||||
print!("Checking TOML configuration integrity for default account… ");
|
||||
}
|
||||
|
||||
stdout.flush()?;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
println!("OK");
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
if self.fix {
|
||||
if prompt::bool("Would you like to reset existing keyring entries?", false)? {
|
||||
print!("Resetting keyring entries… ");
|
||||
stdout.flush()?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
match toml_account_config.imap_auth_config() {
|
||||
Some(ImapAuthConfig::Password(config)) => config.reset().await?,
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
match toml_account_config.smtp_auth_config() {
|
||||
Some(SmtpAuthConfig::Password(config)) => config.reset().await?,
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
if let Some(config) = &toml_account_config.pgp {
|
||||
config.reset().await?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
match toml_account_config.imap_auth_config() {
|
||||
Some(ImapAuthConfig::Password(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::password("IMAP password")?))
|
||||
.await?;
|
||||
}
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(ImapAuthConfig::OAuth2(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
match toml_account_config.smtp_auth_config() {
|
||||
Some(SmtpAuthConfig::Password(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::password("SMTP password")?))
|
||||
.await?;
|
||||
}
|
||||
#[cfg(feature = "oauth2")]
|
||||
Some(SmtpAuthConfig::OAuth2(config)) => {
|
||||
config
|
||||
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
if let Some(config) = &toml_account_config.pgp {
|
||||
config
|
||||
.configure(&toml_account_config.email, || {
|
||||
Ok(prompt::password("PGP secret key password")?)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
match toml_account_config.backend {
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(Backend::Maildir(mdir_config)) => {
|
||||
print!("Checking Maildir integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(Backend::Imap(imap_config)) => {
|
||||
print!("Checking IMAP integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
|
||||
.with_pool_size(1);
|
||||
let res = BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await;
|
||||
|
||||
if self.fix {
|
||||
res?;
|
||||
} else {
|
||||
res.note("Run with --fix to (re)configure your account.")?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(Backend::Notmuch(notmuch_config)) => {
|
||||
print!("Checking Notmuch integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx =
|
||||
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let sending_backend = toml_account_config
|
||||
.message
|
||||
.and_then(|msg| msg.send)
|
||||
.and_then(|send| send.backend);
|
||||
|
||||
match sending_backend {
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(SendingBackend::Smtp(smtp_config)) => {
|
||||
print!("Checking SMTP integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
|
||||
let res = BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await;
|
||||
|
||||
if self.fix {
|
||||
res?;
|
||||
} else {
|
||||
res.note("Run with --fix to (re)configure your account.")?;
|
||||
}
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
#[cfg(feature = "sendmail")]
|
||||
Some(SendingBackend::Sendmail(sendmail_config)) => {
|
||||
print!("Checking Sendmail integrity… ");
|
||||
stdout.flush()?;
|
||||
|
||||
let ctx =
|
||||
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
|
||||
BackendBuilder::new(account_config.clone(), ctx)
|
||||
.check_up()
|
||||
.await?;
|
||||
|
||||
println!("OK");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,36 +1,41 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Accounts, AccountsTable},
|
||||
terminal::cli::printer::Printer,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::Accounts,
|
||||
config::TomlConfig,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
/// List all accounts.
|
||||
/// List all existing accounts.
|
||||
///
|
||||
/// This command lists all accounts defined in your TOML configuration
|
||||
/// file.
|
||||
/// This command lists all the accounts defined in your TOML
|
||||
/// configuration file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountListCommand {
|
||||
#[command(flatten)]
|
||||
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>,
|
||||
}
|
||||
|
||||
impl AccountListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list accounts command");
|
||||
|
||||
let accounts: Accounts = config.accounts.iter().into();
|
||||
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());
|
||||
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: &Default::default(),
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,41 @@
|
|||
mod check_up;
|
||||
mod configure;
|
||||
mod doctor;
|
||||
mod list;
|
||||
#[cfg(feature = "account-sync")]
|
||||
mod sync;
|
||||
|
||||
use color_eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use self::sync::AccountSyncCommand;
|
||||
use self::{
|
||||
check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand,
|
||||
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
|
||||
};
|
||||
|
||||
/// Manage accounts.
|
||||
/// Configure, list and diagnose your accounts.
|
||||
///
|
||||
/// An account is a set of settings, identified by an account
|
||||
/// name. Settings are directly taken from your TOML configuration
|
||||
/// file. This subcommand allows you to manage them.
|
||||
/// An account is a group of settings, identified by a unique
|
||||
/// name. This subcommand allows you to manage your accounts.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountSubcommand {
|
||||
#[command(alias = "checkup")]
|
||||
CheckUp(AccountCheckUpCommand),
|
||||
|
||||
#[command(alias = "cfg")]
|
||||
Configure(AccountConfigureCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
Doctor(AccountDoctorCommand),
|
||||
List(AccountListCommand),
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(alias = "synchronize", alias = "synchronise")]
|
||||
Sync(AccountSyncCommand),
|
||||
}
|
||||
|
||||
impl AccountSubcommand {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
pub async fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::CheckUp(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Configure(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
#[cfg(feature = "account-sync")]
|
||||
Self::Sync(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Configure(cmd) => cmd.execute(config, config_path).await,
|
||||
Self::Doctor(cmd) => cmd.execute(&config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, &config).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
use clap::{ArgAction, Parser};
|
||||
use color_eyre::{eyre::bail, eyre::eyre, Result};
|
||||
use email::backend::context::BackendContextBuilder;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::ImapContextBuilder;
|
||||
use email::{
|
||||
account::sync::AccountSyncBuilder,
|
||||
backend::BackendBuilder,
|
||||
folder::sync::config::FolderSyncStrategy,
|
||||
sync::{hash::SyncHash, SyncEvent},
|
||||
};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::OptionalAccountNameArg, backend::BackendKind, config::TomlConfig,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
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 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();
|
||||
|
||||
match toml_account_config.sync_kind() {
|
||||
Some(BackendKind::Imap) | Some(BackendKind::ImapCache) => {
|
||||
let imap_config = toml_account_config
|
||||
.imap
|
||||
.as_ref()
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.ok_or_else(|| eyre!("imap config not found"))?;
|
||||
let imap_ctx = ImapContextBuilder::new(account_config.clone(), imap_config)
|
||||
.with_prebuilt_credentials()
|
||||
.await?;
|
||||
let imap = BackendBuilder::new(account_config.clone(), imap_ctx);
|
||||
self.sync(printer, account_name, imap).await
|
||||
}
|
||||
Some(backend) => bail!("backend {backend:?} not supported for synchronization"),
|
||||
None => bail!("no backend configured for synchronization"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn sync(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
account_name: &str,
|
||||
right: BackendBuilder<impl BackendContextBuilder + SyncHash + 'static>,
|
||||
) -> Result<()> {
|
||||
let included_folders = BTreeSet::from_iter(self.include_folder);
|
||||
let excluded_folders = BTreeSet::from_iter(self.exclude_folder);
|
||||
|
||||
let folder_filters = 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 sync_builder =
|
||||
AccountSyncBuilder::try_new(right)?.with_some_folder_filters(folder_filters);
|
||||
|
||||
if self.dry_run {
|
||||
let report = sync_builder.with_dry_run(true).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(())
|
||||
}
|
||||
}
|
|
@ -1,258 +1,3 @@
|
|||
//! Deserialized account config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of an
|
||||
//! account in the accounts section of the user configuration file.
|
||||
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
use email::account::config::pgp::PgpConfig;
|
||||
#[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 email::template::config::TemplateConfig;
|
||||
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,
|
||||
};
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SyncConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
pub enable: Option<bool>,
|
||||
pub dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<SyncConfig> for email::account::sync::config::SyncConfig {
|
||||
fn from(config: SyncConfig) -> Self {
|
||||
Self {
|
||||
enable: config.enable,
|
||||
dir: config.dir,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
pub template: Option<TemplateConfig>,
|
||||
|
||||
#[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 sync_kind(&self) -> Option<&BackendKind> {
|
||||
self.sync
|
||||
.as_ref()
|
||||
.and_then(|sync| sync.backend.as_ref())
|
||||
.or(self.backend.as_ref())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
pub type TomlAccountConfig = HimalayaTomlAccountConfig;
|
||||
|
|
|
@ -1,135 +1,3 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use color_eyre::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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,161 +0,0 @@
|
|||
#[cfg(feature = "account-sync")]
|
||||
use crate::account::config::SyncConfig;
|
||||
use color_eyre::{eyre::bail, Result};
|
||||
#[cfg(feature = "account-sync")]
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::Input;
|
||||
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 offline access for 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)))
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
#[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),
|
||||
}
|
|
@ -1,831 +0,0 @@
|
|||
pub mod config;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use color_eyre::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::{ImapContextBuilder, ImapContextSync};
|
||||
#[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, ListEnvelopesOptions},
|
||||
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, SendMessageThenSaveCopy},
|
||||
Messages,
|
||||
},
|
||||
AnyResult,
|
||||
};
|
||||
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 {
|
||||
None,
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
Imap,
|
||||
#[cfg(all(feature = "imap", feature = "account-sync"))]
|
||||
ImapCache,
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
Maildir,
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch,
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp,
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
Sendmail,
|
||||
}
|
||||
|
||||
impl ToString for BackendKind {
|
||||
fn to_string(&self) -> String {
|
||||
let kind = match self {
|
||||
Self::None => "None",
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
Self::Imap => "IMAP",
|
||||
#[cfg(all(feature = "imap", feature = "account-sync"))]
|
||||
Self::ImapCache => "IMAP cache",
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
Self::Maildir => "Maildir",
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
Self::Notmuch => "Notmuch",
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
Self::Smtp => "SMTP",
|
||||
|
||||
#[cfg(feature = "sendmail")]
|
||||
Self::Sendmail => "Sendmail",
|
||||
};
|
||||
|
||||
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(all(feature = "imap", feature = "account-sync"))]
|
||||
pub imap_cache: Option<MaildirContextBuilder>,
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
pub maildir: 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(all(feature = "imap", feature = "account-sync"))]
|
||||
imap_cache: {
|
||||
let builder = toml_account_config
|
||||
.imap
|
||||
.as_ref()
|
||||
.filter(|_| kinds.contains(&&BackendKind::ImapCache))
|
||||
.map(Clone::clone)
|
||||
.map(Arc::new)
|
||||
.map(|imap_config| {
|
||||
email::backend::context::BackendContextBuilder::try_to_sync_cache_builder(
|
||||
&ImapContextBuilder::new(account_config.clone(), imap_config),
|
||||
&account_config,
|
||||
)
|
||||
});
|
||||
match builder {
|
||||
Some(builder) => Some(builder?),
|
||||
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 = "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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.add_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.add_folder_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.list_folders()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.list_folders_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.expunge_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.expunge_folder_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.purge_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.purge_folder_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.delete_folder()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.delete_folder_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.get_envelope()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.get_envelope_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.list_envelopes()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.list_envelopes_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.watch_envelopes()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.watch_envelopes_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.add_flags()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.add_flags_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.set_flags()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.set_flags_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.remove_flags()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.remove_flags_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.add_message()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.add_message_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.peek_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.peek_messages_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.get_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.get_messages_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.copy_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.copy_messages_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.move_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.move_messages_with_some(&self.maildir),
|
||||
#[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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
let f = self.imap_cache.as_ref()?.delete_messages()?;
|
||||
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Some(BackendKind::Maildir) => self.delete_messages_with_some(&self.maildir),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => self.delete_messages_with_some(&self.notmuch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn build(self) -> AnyResult<Self::Context> {
|
||||
let mut ctx = BackendContext::default();
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap) = self.imap {
|
||||
ctx.imap = Some(imap.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "imap", feature = "account-sync"))]
|
||||
if let Some(maildir) = self.imap_cache {
|
||||
ctx.imap_cache = Some(maildir.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
if let Some(maildir) = self.maildir {
|
||||
ctx.maildir = 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(all(feature = "imap", feature = "account-sync"))]
|
||||
pub imap_cache: Option<MaildirContextSync>,
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
pub maildir: 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(all(feature = "imap", feature = "account-sync"))]
|
||||
Some(BackendKind::ImapCache) => {
|
||||
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,
|
||||
opts: ListEnvelopesOptions,
|
||||
) -> 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, opts).await?;
|
||||
let envelopes =
|
||||
Envelopes::from_backend(&self.backend.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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 add_message_with_flags(
|
||||
&self,
|
||||
folder: &str,
|
||||
email: &[u8],
|
||||
flags: &Flags,
|
||||
) -> 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_with_flags(folder, email, flags)
|
||||
.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)?);
|
||||
let msgs = self.backend.peek_messages(folder, &ids).await?;
|
||||
Ok(msgs)
|
||||
}
|
||||
|
||||
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)?);
|
||||
let msgs = self.backend.get_messages(folder, &ids).await?;
|
||||
Ok(msgs)
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_message_then_save_copy(&self, msg: &[u8]) -> Result<()> {
|
||||
self.backend.send_message_then_save_copy(msg).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn watch_envelopes(&self, folder: &str) -> Result<()> {
|
||||
self.backend.watch_envelopes(folder).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Backend {
|
||||
type Target = email::backend::Backend<BackendContext>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.backend
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
use color_eyre::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
15
src/cache/arg/disable.rs
vendored
|
@ -1,15 +0,0 @@
|
|||
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
1
src/cache/arg/mod.rs
vendored
|
@ -1 +0,0 @@
|
|||
pub mod disable;
|
144
src/cache/mod.rs
vendored
144
src/cache/mod.rs
vendored
|
@ -1,144 +0,0 @@
|
|||
pub mod arg;
|
||||
|
||||
use color_eyre::{eyre::eyre, eyre::Context, Result};
|
||||
use dirs::data_dir;
|
||||
use email::account::config::AccountConfig;
|
||||
use sled::{Config, Db};
|
||||
use std::collections::HashSet;
|
||||
use tracing::debug;
|
||||
|
||||
#[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(eyre!("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(|| eyre!("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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
src/cli.rs
51
src/cli.rs
|
@ -1,11 +1,22 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use color_eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
use pimalaya_tui::{
|
||||
long_version,
|
||||
terminal::{
|
||||
cli::{
|
||||
arg::path_parser,
|
||||
printer::{OutputFmt, Printer},
|
||||
},
|
||||
config::TomlConfig as _,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
account::command::AccountSubcommand,
|
||||
completion::command::CompletionGenerateCommand,
|
||||
config::{self, TomlConfig},
|
||||
config::TomlConfig,
|
||||
envelope::command::EnvelopeSubcommand,
|
||||
flag::command::FlagSubcommand,
|
||||
folder::command::FolderSubcommand,
|
||||
|
@ -14,12 +25,12 @@ use crate::{
|
|||
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
|
||||
template::command::TemplateSubcommand,
|
||||
},
|
||||
output::{ColorFmt, OutputFmt},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "himalaya", author, version, about)]
|
||||
#[command(name = env!("CARGO_PKG_NAME"))]
|
||||
#[command(author, version, about)]
|
||||
#[command(long_version = long_version!())]
|
||||
#[command(propagate_version = true, infer_subcommands = true)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
|
@ -33,8 +44,8 @@ pub struct Cli {
|
|||
/// 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)]
|
||||
#[arg(value_name = "PATH", value_parser = config::path_parser)]
|
||||
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
|
||||
#[arg(value_name = "PATH", value_parser = path_parser)]
|
||||
pub config_paths: Vec<PathBuf>,
|
||||
|
||||
/// Customize the output format.
|
||||
|
@ -52,30 +63,6 @@ pub struct Cli {
|
|||
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
|
||||
pub output: OutputFmt,
|
||||
|
||||
/// Control when to use colors
|
||||
///
|
||||
/// The default setting is 'auto', which means himalaya will try
|
||||
/// to guess when to use colors. For example, if himalaya is
|
||||
/// printing to a terminal, then it will use colors, but if it is
|
||||
/// redirected to a file or a pipe, then it will suppress color
|
||||
/// output. himalaya will suppress color output in some other
|
||||
/// circumstances as well. For example, if the TERM environment
|
||||
/// variable is not set or set to 'dumb', then himalaya will not
|
||||
/// use colors.
|
||||
///
|
||||
/// The possible values 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,
|
||||
|
||||
/// Enable logs with spantrace.
|
||||
///
|
||||
/// This is the same as running the command with `RUST_LOG=debug`
|
||||
|
@ -136,7 +123,7 @@ impl HimalayaCommand {
|
|||
match self {
|
||||
Self::Account(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
cmd.execute(printer, config, config_paths.first()).await
|
||||
}
|
||||
Self::Folder(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::io;
|
||||
|
||||
use clap::{value_parser, CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
use color_eyre::Result;
|
||||
use std::io;
|
||||
use tracing::info;
|
||||
|
||||
use crate::cli::Cli;
|
||||
|
||||
/// Print completion script for a shell to stdout.
|
||||
/// Print completion script for the given shell to stdout.
|
||||
///
|
||||
/// This command allows you to generate completion script for a given
|
||||
/// shell. The script is printed to the standard output. If you want
|
||||
|
|
3
src/config.rs
Normal file
3
src/config.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use pimalaya_tui::himalaya::config::HimalayaTomlConfig;
|
||||
|
||||
pub type TomlConfig = HimalayaTomlConfig;
|
|
@ -1,291 +0,0 @@
|
|||
pub mod wizard;
|
||||
|
||||
use color_eyre::{
|
||||
eyre::{bail, eyre, 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 serde_toml_merge::merge;
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
||||
use toml::{self, Value};
|
||||
use tracing::debug;
|
||||
|
||||
#[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", deny_unknown_fields)]
|
||||
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 paths.
|
||||
///
|
||||
/// Returns an error if a configuration file cannot be read or if
|
||||
/// a content cannot be parsed.
|
||||
fn from_paths(paths: &[PathBuf]) -> Result<Self> {
|
||||
match paths.len() {
|
||||
0 => {
|
||||
// should never happen
|
||||
bail!("cannot read config file from empty paths");
|
||||
}
|
||||
1 => {
|
||||
let path = &paths[0];
|
||||
|
||||
let ref 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:?}"))
|
||||
}
|
||||
_ => {
|
||||
let path = &paths[0];
|
||||
|
||||
let mut merged_content = fs::read_to_string(path)
|
||||
.context(format!("cannot read config file at {path:?}"))?
|
||||
.parse::<Value>()?;
|
||||
|
||||
for path in &paths[1..] {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
merged_content = merge(merged_content, content.parse()?).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("skipping subconfig file at {path:?}: {err}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
merged_content
|
||||
.try_into()
|
||||
.context(format!("cannot parse merged 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_paths(&[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_paths_or_default(paths: &[PathBuf]) -> Result<Self> {
|
||||
match paths.len() {
|
||||
0 => Self::from_default_paths().await,
|
||||
_ if paths[0].exists() => Self::from_paths(paths),
|
||||
_ => Self::from_wizard(&paths[0]).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(eyre!("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(|| eyre!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
.ok_or_else(|| eyre!("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 = match toml_account_config.backend {
|
||||
Some(BackendKind::Imap) => Some(BackendKind::ImapCache),
|
||||
backend => backend,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
delete: c.delete.map(Into::into),
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: c.sync,
|
||||
}),
|
||||
template: config.template,
|
||||
#[cfg(feature = "account-sync")]
|
||||
sync: config.sync.map(Into::into),
|
||||
#[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())
|
||||
}
|
|
@ -1,537 +0,0 @@
|
|||
use color_eyre::Result;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use shellexpand_utils::expand;
|
||||
use std::{fs, path::PathBuf, process};
|
||||
use toml_edit::{DocumentMut, 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: DocumentMut = 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_command(
|
||||
// "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.command = "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_command(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.command = ["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_command(
|
||||
// "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.command = "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_command(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.command = ["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,30 +1,29 @@
|
|||
use std::{process::exit, sync::Arc};
|
||||
|
||||
use ariadne::{Color, Label, Report, ReportKind, Source};
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, email::search_query,
|
||||
backend::feature::BackendFeatureSource, config::Config, email::search_query,
|
||||
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
|
||||
};
|
||||
use std::process::exit;
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
config::TomlConfig,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
};
|
||||
|
||||
/// List all envelopes.
|
||||
/// Search and sort envelopes as a list.
|
||||
///
|
||||
/// This command allows you to list all envelopes included in the
|
||||
/// given folder.
|
||||
/// This command allows you to list envelopes included in the given
|
||||
/// folder, matching the given query.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ListEnvelopesCommand {
|
||||
pub struct EnvelopeListCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
|
@ -41,16 +40,18 @@ pub struct ListEnvelopesCommand {
|
|||
#[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
|
||||
|
@ -122,30 +123,30 @@ pub struct ListEnvelopesCommand {
|
|||
pub query: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for ListEnvelopesCommand {
|
||||
impl Default for EnvelopeListCommand {
|
||||
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 ListEnvelopesCommand {
|
||||
impl EnvelopeListCommand {
|
||||
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(),
|
||||
#[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 toml_account_config = Arc::new(toml_account_config);
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let page = 1.max(self.page) - 1;
|
||||
|
@ -153,14 +154,17 @@ impl ListEnvelopesCommand {
|
|||
.page_size
|
||||
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
|
||||
|
||||
let list_envelopes_kind = toml_account_config.list_envelopes_kind();
|
||||
|
||||
let backend = Backend::new(
|
||||
let backend = BackendBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
list_envelopes_kind,
|
||||
|builder| builder.set_list_envelopes(BackendFeatureSource::Context),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_list_envelopes(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let query = self
|
||||
|
@ -196,15 +200,19 @@ impl ListEnvelopesCommand {
|
|||
};
|
||||
|
||||
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.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &account_config.get_message_read_format(),
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
pub mod list;
|
||||
pub mod watch;
|
||||
pub mod thread;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
|
||||
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
|
||||
|
||||
/// Manage envelopes.
|
||||
/// List, search and sort your envelopes.
|
||||
///
|
||||
/// An envelope is a small representation of a message. It contains an
|
||||
/// identifier (given by the backend), some flags as well as few
|
||||
|
@ -17,10 +18,10 @@ use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
|
|||
#[derive(Debug, Subcommand)]
|
||||
pub enum EnvelopeSubcommand {
|
||||
#[command(alias = "lst")]
|
||||
List(ListEnvelopesCommand),
|
||||
List(EnvelopeListCommand),
|
||||
|
||||
#[command()]
|
||||
Watch(WatchEnvelopesCommand),
|
||||
Thread(EnvelopeThreadCommand),
|
||||
}
|
||||
|
||||
impl EnvelopeSubcommand {
|
||||
|
@ -28,7 +29,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::Watch(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
201
src/email/envelope/command/thread.rs
Normal file
201
src/email/envelope/command/thread.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
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)
|
||||
// }
|
||||
// }
|
|
@ -1,57 +0,0 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use tracing::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
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
#[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,20 +1,22 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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 an envelope.
|
||||
/// Add flag(s) to the given envelope.
|
||||
///
|
||||
/// This command allows you to attach the given flag(s) to the given
|
||||
/// envelope(s).
|
||||
|
@ -26,10 +28,6 @@ pub struct FlagAddCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,24 +38,27 @@ 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(),
|
||||
#[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_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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.add_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.print(format!("Flag(s) {flags} successfully added!"))
|
||||
printer.out(format!("Flag(s) {flags} successfully added!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,19 @@ mod add;
|
|||
mod remove;
|
||||
mod set;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
|
||||
|
||||
/// Manage flags.
|
||||
/// Add, change and remove your envelopes flags.
|
||||
///
|
||||
/// A flag is a tag associated to an envelope. Existing flags are
|
||||
/// seen, answered, flagged, deleted, draft. Other flags are
|
||||
/// considered custom, which are not always supported (the
|
||||
/// synchronization does not take care of them yet).
|
||||
/// considered custom, which are not always supported.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum FlagSubcommand {
|
||||
#[command(arg_required_else_help = true)]
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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 an envelope.
|
||||
/// Remove flag(s) from a given envelope.
|
||||
///
|
||||
/// This command allows you to remove the given flag(s) from the given
|
||||
/// envelope(s).
|
||||
|
@ -26,10 +28,6 @@ pub struct FlagRemoveCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,24 +38,27 @@ 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(),
|
||||
#[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 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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_remove_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.remove_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.print(format!("Flag(s) {flags} successfully removed!"))
|
||||
printer.out(format!("Flag(s) {flags} successfully removed!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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 an envelope.
|
||||
/// Replace flag(s) of a given envelope.
|
||||
///
|
||||
/// This command allows you to replace existing flags of the given
|
||||
/// envelope(s) with the given flag(s).
|
||||
|
@ -26,10 +28,6 @@ pub struct FlagSetCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,24 +38,27 @@ 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(),
|
||||
#[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 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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_set_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.set_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.print(format!("Flag(s) {flags} successfully replaced!"))
|
||||
printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
#[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,48 +1,2 @@
|
|||
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,128 +1,3 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod flag;
|
||||
|
||||
use color_eyre::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 to: 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(),
|
||||
},
|
||||
to: Mailbox {
|
||||
name: envelope.to.name.clone(),
|
||||
addr: envelope.to.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,19 +1,20 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use std::{fs, path::PathBuf};
|
||||
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 uuid::Uuid;
|
||||
|
||||
#[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::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Download all attachments for the given message.
|
||||
/// Download all attachments found in the given message.
|
||||
///
|
||||
/// This command allows you to download all attachments found for the
|
||||
/// given message to your downloads directory.
|
||||
|
@ -25,10 +26,6 @@ pub struct AttachmentDownloadCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,20 +37,25 @@ 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(),
|
||||
#[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 get_messages_kind = toml_account_config.get_messages_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let emails = backend.get_messages(folder, ids).await?;
|
||||
|
@ -67,14 +69,14 @@ impl AttachmentDownloadCommand {
|
|||
let attachments = email.attachments()?;
|
||||
|
||||
if attachments.is_empty() {
|
||||
printer.print_log(format!("No attachment found for message {id}!"))?;
|
||||
printer.log(format!("No attachment found for message {id}!\n"))?;
|
||||
continue;
|
||||
} else {
|
||||
emails_count += 1;
|
||||
}
|
||||
|
||||
printer.print_log(format!(
|
||||
"{} attachment(s) found for message {id}!",
|
||||
printer.log(format!(
|
||||
"{} attachment(s) found for message {id}!\n",
|
||||
attachments.len()
|
||||
))?;
|
||||
|
||||
|
@ -84,7 +86,7 @@ impl AttachmentDownloadCommand {
|
|||
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||
.into();
|
||||
let filepath = account_config.get_download_file_path(&filename)?;
|
||||
printer.print_log(format!("Downloading {:?}…", filepath))?;
|
||||
printer.log(format!("Downloading {:?}…\n", filepath))?;
|
||||
fs::write(&filepath, &attachment.body)
|
||||
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
|
||||
attachments_count += 1;
|
||||
|
@ -92,10 +94,10 @@ impl AttachmentDownloadCommand {
|
|||
}
|
||||
|
||||
match attachments_count {
|
||||
0 => printer.print("No attachment found!"),
|
||||
1 => printer.print("Downloaded 1 attachment!"),
|
||||
n => printer.print(format!(
|
||||
"Downloaded {} attachment(s) from {} messages(s)!",
|
||||
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",
|
||||
n, emails_count,
|
||||
)),
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
mod download;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::download::AttachmentDownloadCommand;
|
||||
|
||||
/// Manage attachments.
|
||||
/// Download your message attachments.
|
||||
///
|
||||
/// A message body can be composed of multiple MIME parts. An
|
||||
/// attachment is the representation of a binary part of a message
|
||||
/// body.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AttachmentSubcommand {
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(arg_required_else_help = true, alias = "dl")]
|
||||
Download(AttachmentDownloadCommand),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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 a message from a source folder to a target folder.
|
||||
/// Copy the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageCopyCommand {
|
||||
#[command(flatten)]
|
||||
|
@ -26,10 +29,6 @@ pub struct MessageCopyCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -42,26 +41,29 @@ 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(),
|
||||
#[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 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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_copy_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.copy_messages(source, target, ids).await?;
|
||||
|
||||
printer.print(format!(
|
||||
"Message(s) successfully copied from {source} to {target}!"
|
||||
printer.out(format!(
|
||||
"Message(s) successfully copied from {source} to {target}!\n"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Mark as deleted a message from a folder.
|
||||
/// Mark as deleted the message associated to the given envelope id(s).
|
||||
///
|
||||
/// This command does not really delete the message: if the given
|
||||
/// folder points to the trash folder, it adds the "deleted" flag to
|
||||
|
@ -25,10 +28,6 @@ pub struct MessageDeleteCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,24 +39,27 @@ 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(),
|
||||
#[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_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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_delete_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.delete_messages(folder, ids).await?;
|
||||
|
||||
printer.print(format!("Message(s) successfully removed from {folder}!"))
|
||||
printer.out(format!("Message(s) successfully removed from {folder}!\n"))
|
||||
}
|
||||
}
|
||||
|
|
103
src/email/message/command/edit.rs
Normal file
103
src/email/message/command/edit.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Edit the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to edit the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
/// edition process finishes, you can choose between saving or sending
|
||||
/// the final message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageEditCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// List of headers that should be visible at the top of the
|
||||
/// message.
|
||||
///
|
||||
/// If a given header is not found in the message, it will not be
|
||||
/// visible. If no header is given, defaults to the one set up in
|
||||
/// your TOML configuration file.
|
||||
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||
pub headers: Vec<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(())
|
||||
}
|
||||
}
|
155
src/email/message/command/export.rs
Normal file
155
src/email/message/command/export.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use std::{
|
||||
env::temp_dir,
|
||||
fs,
|
||||
io::{stdout, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Export the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to export a message. A message can be
|
||||
/// fully exported in one single file, or exported in multiple files
|
||||
/// (one per MIME part found in the message). This is useful, for
|
||||
/// example, to read a HTML message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageExportCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// Export the full raw message as one unique .eml file.
|
||||
///
|
||||
/// The raw message represents the headers and the body as it is
|
||||
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||
/// useful for debugging faulty messages, but also for
|
||||
/// saving/sending/transfering messages.
|
||||
#[arg(long, short = 'F')]
|
||||
pub full: bool,
|
||||
|
||||
/// Try to open the exported message, when applicable.
|
||||
///
|
||||
/// This argument only works with full message export, or when
|
||||
/// HTML or plain text is present in the export.
|
||||
#[arg(long, short = 'O')]
|
||||
pub open: bool,
|
||||
|
||||
/// Where the message should be exported to.
|
||||
///
|
||||
/// The destination should point to a valid directory. If `--full`
|
||||
/// is given, it can also point to a .eml file.
|
||||
#[arg(long, short, alias = "dest")]
|
||||
pub destination: Option<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,22 +1,23 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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 a message.
|
||||
/// Forward the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to forward the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
|
@ -36,10 +37,6 @@ pub struct MessageForwardCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -50,24 +47,25 @@ impl MessageForwardCommand {
|
|||
|
||||
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_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use mail_builder::MessageBuilder;
|
||||
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 url::Url;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, printer::Printer,
|
||||
ui::editor,
|
||||
};
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
||||
|
||||
/// Parse and edit a message from a mailto URL string.
|
||||
/// Parse and edit a message from the given mailto URL string.
|
||||
///
|
||||
/// This command allows you to edit a message from the mailto format
|
||||
/// using the editor defined in your environment variable
|
||||
|
@ -24,10 +24,6 @@ pub struct MessageMailtoCommand {
|
|||
#[arg()]
|
||||
pub url: Url,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -36,8 +32,6 @@ 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(),
|
||||
})
|
||||
}
|
||||
|
@ -45,60 +39,57 @@ 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(),
|
||||
#[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_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut builder = MessageBuilder::new().to(self.url.path());
|
||||
let mut body = String::new();
|
||||
let mut msg = Vec::<u8>::new();
|
||||
let mut body = Vec::<u8>::new();
|
||||
|
||||
msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n");
|
||||
|
||||
for (key, val) in self.url.query_pairs() {
|
||||
match key {
|
||||
key if key.eq_ignore_ascii_case("in-reply-to") => {
|
||||
builder = builder.in_reply_to(val.to_string());
|
||||
}
|
||||
key if key.eq_ignore_ascii_case("cc") => {
|
||||
builder = builder.cc(val.to_string());
|
||||
}
|
||||
key if key.eq_ignore_ascii_case("bcc") => {
|
||||
builder = builder.bcc(val.to_string());
|
||||
}
|
||||
key if key.eq_ignore_ascii_case("subject") => {
|
||||
builder = builder.subject(val.to_string());
|
||||
}
|
||||
key if key.eq_ignore_ascii_case("body") => {
|
||||
body += &val;
|
||||
}
|
||||
_ => (),
|
||||
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 account_config.find_full_signature() {
|
||||
Some(ref sig) => builder = builder.text_body(body + "\n\n" + sig),
|
||||
None => builder = builder.text_body(body),
|
||||
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());
|
||||
}
|
||||
|
||||
let tpl = account_config
|
||||
.generate_tpl_interpreter()
|
||||
.with_show_only_headers(account_config.get_message_write_headers())
|
||||
.build()
|
||||
.from_msg_builder(builder)
|
||||
.from_bytes(msg)
|
||||
.await?
|
||||
.into();
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod edit;
|
||||
pub mod export;
|
||||
pub mod forward;
|
||||
pub mod mailto;
|
||||
pub mod r#move;
|
||||
|
@ -7,21 +9,24 @@ pub mod read;
|
|||
pub mod reply;
|
||||
pub mod save;
|
||||
pub mod send;
|
||||
pub mod thread;
|
||||
pub mod write;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
|
||||
mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand,
|
||||
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
|
||||
copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand,
|
||||
export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand,
|
||||
r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand,
|
||||
save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand,
|
||||
write::MessageWriteCommand,
|
||||
};
|
||||
|
||||
/// Manage messages.
|
||||
/// Read, write, send, copy, move and delete your messages.
|
||||
///
|
||||
/// A message is the content of an email. It is composed of headers
|
||||
/// (located at the top of the message) and a body (located at the
|
||||
|
@ -32,16 +37,22 @@ pub enum MessageSubcommand {
|
|||
#[command(arg_required_else_help = true)]
|
||||
Read(MessageReadCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Export(MessageExportCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Thread(MessageThreadCommand),
|
||||
|
||||
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||
Write(MessageWriteCommand),
|
||||
|
||||
#[command()]
|
||||
Reply(MessageReplyCommand),
|
||||
|
||||
#[command(aliases = ["fwd", "fd"])]
|
||||
Forward(MessageForwardCommand),
|
||||
|
||||
#[command()]
|
||||
Edit(MessageEditCommand),
|
||||
|
||||
Mailto(MessageMailtoCommand),
|
||||
|
||||
Save(MessageSaveCommand),
|
||||
|
@ -66,9 +77,12 @@ impl MessageSubcommand {
|
|||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Read(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Export(cmd) => cmd.execute(config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Write(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Reply(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Forward(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Edit(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Mailto(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Save(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Send(cmd) => cmd.execute(printer, config).await,
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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 a message from a source folder to a target folder.
|
||||
/// Move the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageMoveCommand {
|
||||
#[command(flatten)]
|
||||
|
@ -27,10 +30,6 @@ pub struct MessageMoveCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -43,26 +42,29 @@ 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(),
|
||||
#[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 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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_move_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.move_messages(source, target, ids).await?;
|
||||
|
||||
printer.print(format!(
|
||||
"Message(s) successfully moved from {source} to {target}!"
|
||||
printer.out(format!(
|
||||
"Message(s) successfully moved from {source} to {target}!\n"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use mml::message::FilterParts;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Read a message.
|
||||
/// Read a human-friendly version of the message associated to the
|
||||
/// given envelope id(s).
|
||||
///
|
||||
/// This command allows you to read a message. When reading a message,
|
||||
/// the "seen" flag is automatically applied to the corresponding
|
||||
/// envelope. To prevent this behaviour, use the --preview flag.
|
||||
/// envelope. To prevent this behaviour, use the "--preview" flag.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageReadCommand {
|
||||
#[command(flatten)]
|
||||
|
@ -31,31 +34,10 @@ pub struct MessageReadCommand {
|
|||
#[arg(long, short)]
|
||||
pub preview: bool,
|
||||
|
||||
/// Read the raw version of the given message.
|
||||
///
|
||||
/// The raw message represents the headers and the body as it is
|
||||
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||
/// useful for debugging faulty messages, but also for
|
||||
/// saving/sending/transfering messages.
|
||||
#[arg(long, short)]
|
||||
#[arg(conflicts_with = "no_headers")]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub raw: bool,
|
||||
|
||||
/// Read only body of text/html parts.
|
||||
///
|
||||
/// This argument is useful when you need to read the HTML version
|
||||
/// of a message. Combined with --no-headers, you can write it to
|
||||
/// a .html file and open it with your favourite browser.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
pub html: bool,
|
||||
|
||||
/// Read only the body of the message.
|
||||
///
|
||||
/// All headers will be removed from the message.
|
||||
#[arg(long)]
|
||||
#[arg(conflicts_with = "raw")]
|
||||
#[arg(conflicts_with = "headers")]
|
||||
pub no_headers: bool,
|
||||
|
||||
|
@ -66,14 +48,9 @@ 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,
|
||||
}
|
||||
|
@ -85,20 +62,26 @@ 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(),
|
||||
#[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 get_messages_kind = toml_account_config.get_messages_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
.with_peek_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let emails = if self.preview {
|
||||
|
@ -113,32 +96,22 @@ impl MessageReadCommand {
|
|||
for email in emails.to_vec() {
|
||||
bodies.push_str(glue);
|
||||
|
||||
if self.raw {
|
||||
// emails do not always have valid utf8, uses "lossy" to
|
||||
// display what can be displayed
|
||||
bodies.push_str(&String::from_utf8_lossy(email.raw()?));
|
||||
} else {
|
||||
let tpl = email
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if self.no_headers {
|
||||
tpl = tpl.with_hide_all_headers();
|
||||
} else if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
let tpl = email
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if self.no_headers {
|
||||
tpl = tpl.with_hide_all_headers();
|
||||
} else if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
|
||||
if self.html {
|
||||
tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into()));
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
bodies.push_str(&tpl);
|
||||
}
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
bodies.push_str(&tpl);
|
||||
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.print(bodies)
|
||||
printer.out(bodies)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
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;
|
||||
|
||||
#[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 a message.
|
||||
/// Reply to the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to reply to the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
|
@ -39,10 +40,6 @@ pub struct MessageReplyCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -52,24 +49,25 @@ 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(),
|
||||
#[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_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
|
@ -84,6 +82,11 @@ impl MessageReplyCommand {
|
|||
.with_reply_all(self.reply.all)
|
||||
.build()
|
||||
.await?;
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
|
||||
|
||||
backend.add_flag(folder, &[id], Flag::Answered).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
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;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
|
||||
};
|
||||
|
||||
/// Save a message to a folder.
|
||||
/// Save the given raw message to the given folder.
|
||||
///
|
||||
/// This command allows you to add a raw message to the given folder.
|
||||
#[derive(Debug, Parser)]
|
||||
|
@ -23,10 +27,6 @@ pub struct MessageSaveCommand {
|
|||
#[command(flatten)]
|
||||
pub message: MessageRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -37,20 +37,23 @@ impl MessageSaveCommand {
|
|||
|
||||
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_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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let is_tty = io::stdin().is_terminal();
|
||||
|
@ -68,6 +71,6 @@ impl MessageSaveCommand {
|
|||
|
||||
backend.add_message(folder, msg.as_bytes()).await?;
|
||||
|
||||
printer.print(format!("Message successfully saved to {folder}!"))
|
||||
printer.out(format!("Message successfully saved to {folder}!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
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;
|
||||
|
||||
#[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,
|
||||
};
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
|
||||
|
||||
/// Send a message.
|
||||
/// Send the given raw message.
|
||||
///
|
||||
/// This command allows you to send a raw message and to save a copy
|
||||
/// to your send folder.
|
||||
|
@ -20,10 +22,6 @@ pub struct MessageSendCommand {
|
|||
#[command(flatten)]
|
||||
pub message: MessageRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -32,27 +30,23 @@ 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(),
|
||||
#[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 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,
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let msg = if io::stdin().is_terminal() {
|
||||
|
@ -68,6 +62,6 @@ impl MessageSendCommand {
|
|||
|
||||
backend.send_message_then_save_copy(msg.as_bytes()).await?;
|
||||
|
||||
printer.print("Message successfully sent!")
|
||||
printer.out("Message successfully sent!")
|
||||
}
|
||||
}
|
||||
|
|
130
src/email/message/command/thread.rs
Normal file
130
src/email/message/command/thread.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
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,20 +1,24 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, message::Message};
|
||||
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;
|
||||
|
||||
#[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,
|
||||
};
|
||||
|
||||
/// Write a new message.
|
||||
/// Compose a new message, from scratch.
|
||||
///
|
||||
/// This command allows you to write a new message using the editor
|
||||
/// defined in your environment variable $EDITOR. When the edition
|
||||
|
@ -28,10 +32,6 @@ pub struct MessageWriteCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -40,24 +40,25 @@ 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(),
|
||||
#[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_message_kind = toml_account_config.add_message_kind();
|
||||
let send_message_kind = toml_account_config.send_message_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
add_message_kind.into_iter().chain(send_message_kind),
|
||||
|builder| {
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = Message::new_tpl_builder(account_config.clone())
|
||||
|
|
|
@ -1,189 +0,0 @@
|
|||
use email::message::delete::config::DeleteMessageStyle;
|
||||
#[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<DeleteMessageConfig>,
|
||||
#[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 DeleteMessageConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
pub style: Option<DeleteMessageStyle>,
|
||||
}
|
||||
|
||||
impl From<DeleteMessageConfig> for email::message::delete::config::DeleteMessageConfig {
|
||||
fn from(config: DeleteMessageConfig) -> Self {
|
||||
Self {
|
||||
style: config.style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DeleteMessageConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
pub mod arg;
|
||||
pub mod attachment;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod template;
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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.
|
||||
|
@ -34,10 +36,6 @@ pub struct TemplateForwardCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -48,20 +46,25 @@ impl TemplateForwardCommand {
|
|||
|
||||
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 get_messages_kind = toml_account_config.get_messages_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
|
@ -76,6 +79,6 @@ impl TemplateForwardCommand {
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,25 +4,25 @@ mod save;
|
|||
mod send;
|
||||
mod write;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand,
|
||||
send::TemplateSendCommand, write::TemplateWriteCommand,
|
||||
};
|
||||
|
||||
/// Manage templates.
|
||||
/// Generate, save and send message templates.
|
||||
///
|
||||
/// A template is an editable version of a message (headers +
|
||||
/// body). It uses a specific language called MML that allows you to
|
||||
/// attach file or encrypt content. This subcommand allows you manage
|
||||
/// them.
|
||||
///
|
||||
/// You can learn more about MML at
|
||||
/// <https://crates.io/crates/mml-lib>.
|
||||
/// Learn more about MML at: <https://crates.io/crates/mml-lib>.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum TemplateSubcommand {
|
||||
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::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.
|
||||
|
@ -38,10 +40,6 @@ pub struct TemplateReplyCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -53,20 +51,25 @@ 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(),
|
||||
#[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 get_messages_kind = toml_account_config.get_messages_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
get_messages_kind,
|
||||
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = backend
|
||||
|
@ -81,6 +84,6 @@ impl TemplateReplyCommand {
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use mml::MmlCompilerBuilder;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
email::template::arg::TemplateRawArg, folder::arg::name::FolderNameOptionalFlag,
|
||||
printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Save a template to a folder.
|
||||
|
@ -27,10 +31,6 @@ pub struct TemplateSaveCommand {
|
|||
#[command(flatten)]
|
||||
pub template: TemplateRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -41,20 +41,25 @@ impl TemplateSaveCommand {
|
|||
|
||||
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_message_kind = toml_account_config.add_message_kind();
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
add_message_kind,
|
||||
|builder| builder.set_add_message(BackendFeatureSource::Context),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let is_tty = io::stdin().is_terminal();
|
||||
|
@ -73,13 +78,13 @@ impl TemplateSaveCommand {
|
|||
#[allow(unused_mut)]
|
||||
let mut compiler = MmlCompilerBuilder::new();
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
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.print(format!("Template successfully saved to {folder}!"))
|
||||
printer.out(format!("Template successfully saved to {folder}!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::backend::feature::BackendFeatureSource;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use mml::MmlCompilerBuilder;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
email::template::arg::TemplateRawArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
|
||||
};
|
||||
|
||||
/// Send a template.
|
||||
|
@ -23,10 +28,6 @@ pub struct TemplateSendCommand {
|
|||
#[command(flatten)]
|
||||
pub template: TemplateRawArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -35,27 +36,25 @@ 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(),
|
||||
#[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 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 account_config = Arc::new(account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
send_message_kind,
|
||||
|builder| {
|
||||
builder.set_send_message(BackendFeatureSource::Context);
|
||||
builder.set_add_message(BackendFeatureSource::Context);
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = if io::stdin().is_terminal() {
|
||||
|
@ -72,13 +71,13 @@ impl TemplateSendCommand {
|
|||
#[allow(unused_mut)]
|
||||
let mut compiler = MmlCompilerBuilder::new();
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
|
||||
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.print("Message successfully sent!")
|
||||
printer.out("Message successfully sent!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::message::Message;
|
||||
use email::{config::Config, message::Message};
|
||||
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
|
||||
use tracing::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,10 +23,6 @@ pub struct TemplateWriteCommand {
|
|||
#[command(flatten)]
|
||||
pub body: TemplateRawBodyArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -35,18 +31,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(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
let (_, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let tpl = Message::new_tpl_builder(account_config)
|
||||
let tpl = Message::new_tpl_builder(Arc::new(account_config))
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,2 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
|
||||
use color_eyre::Result;
|
||||
use email::template::Template;
|
||||
|
||||
use crate::printer::{Print, WriteColor};
|
||||
|
||||
impl Print for Template {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
self.as_str().print(writer)?;
|
||||
Ok(writer.reset()?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,61 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::add::AddFolder};
|
||||
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;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
};
|
||||
|
||||
/// Create a new folder.
|
||||
/// Create the given folder.
|
||||
///
|
||||
/// This command allows you to create a new folder using the given
|
||||
/// name.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddFolderCommand {
|
||||
pub struct FolderAddCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl AddFolderCommand {
|
||||
impl FolderAddCommand {
|
||||
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 add_folder_kind = toml_account_config.add_folder_kind();
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
add_folder_kind,
|
||||
|builder| builder.set_add_folder(BackendFeatureSource::Context),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.add_folder(folder).await?;
|
||||
|
||||
printer.print(format!("Folder {folder} successfully created!"))
|
||||
printer.out(format!("Folder {folder} successfully created!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
use std::{process, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use dialoguer::Confirm;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder};
|
||||
use std::process;
|
||||
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;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
};
|
||||
|
||||
/// Delete a folder.
|
||||
/// Delete the given folder.
|
||||
///
|
||||
/// All emails from the given folder are definitely deleted. The
|
||||
/// folder is also deleted after execution of the command.
|
||||
|
@ -21,12 +25,11 @@ 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 {
|
||||
|
@ -35,34 +38,36 @@ impl FolderDeleteCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
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 !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 (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||
self.account.name.as_deref(),
|
||||
#[cfg(feature = "account-sync")]
|
||||
self.cache.disable,
|
||||
)?;
|
||||
if !prompt::bool(confirm, false)? {
|
||||
process::exit(0);
|
||||
};
|
||||
}
|
||||
|
||||
let delete_folder_kind = toml_account_config.delete_folder_kind();
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let backend = Backend::new(
|
||||
toml_account_config.clone(),
|
||||
account_config,
|
||||
delete_folder_kind,
|
||||
|builder| builder.set_delete_folder(BackendFeatureSource::Context),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_delete_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.delete_folder(folder).await?;
|
||||
|
||||
printer.print(format!("Folder {folder} successfully deleted!"))
|
||||
printer.out(format!("Folder {folder} successfully deleted!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::expunge::ExpungeFolder};
|
||||
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;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
};
|
||||
|
||||
/// Expunge a folder.
|
||||
/// Expunge the given folder.
|
||||
///
|
||||
/// The concept of expunging is similar to the IMAP one: it definitely
|
||||
/// deletes emails from the given folder that contain the "deleted"
|
||||
|
@ -20,10 +25,6 @@ pub struct FolderExpungeCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -33,24 +34,27 @@ 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(),
|
||||
#[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 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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_expunge_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.expunge_folder(folder).await?;
|
||||
|
||||
printer.print(format!("Folder {folder} successfully expunged!"))
|
||||
printer.out(format!("Folder {folder} successfully expunged!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,73 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::list::ListFolders};
|
||||
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;
|
||||
|
||||
#[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,
|
||||
};
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
||||
|
||||
/// List all folders.
|
||||
///
|
||||
/// This command allows you to list all exsting folders.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderListCommand {
|
||||
#[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>,
|
||||
}
|
||||
|
||||
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(),
|
||||
#[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 list_folders_kind = toml_account_config.list_folders_kind();
|
||||
let toml_account_config = Arc::new(toml_account_config);
|
||||
|
||||
let backend = Backend::new(
|
||||
let backend = BackendBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
account_config.clone(),
|
||||
list_folders_kind,
|
||||
|builder| builder.set_list_folders(BackendFeatureSource::Context),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let folders: Folders = backend.list_folders().await?.into();
|
||||
|
||||
printer.print_table(
|
||||
Box::new(folders),
|
||||
PrintTableOpts {
|
||||
format: &account_config.get_message_read_format(),
|
||||
max_width: self.table.max_width,
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_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());
|
||||
|
||||
printer.out(table)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,24 +4,25 @@ mod expunge;
|
|||
mod list;
|
||||
mod purge;
|
||||
|
||||
use color_eyre::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
|
||||
add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
|
||||
list::FolderListCommand, purge::FolderPurgeCommand,
|
||||
};
|
||||
|
||||
/// Manage folders.
|
||||
/// Create, list and purge your folders (as known as mailboxes).
|
||||
///
|
||||
/// A folder (as known as mailbox, or directory) contains one or more
|
||||
/// emails. This subcommand allows you to manage them.
|
||||
/// A folder (as known as mailbox, or directory) is a messages
|
||||
/// container. This subcommand allows you to manage them.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum FolderSubcommand {
|
||||
#[command(visible_alias = "create", alias = "new")]
|
||||
Add(AddFolderCommand),
|
||||
Add(FolderAddCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
List(FolderListCommand),
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use std::{process, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use dialoguer::Confirm;
|
||||
use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
|
||||
use std::process;
|
||||
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;
|
||||
|
||||
#[cfg(feature = "account-sync")]
|
||||
use crate::cache::arg::disable::CacheDisableFlag;
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
|
||||
folder::arg::name::FolderNameArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
};
|
||||
|
||||
/// Purge a folder.
|
||||
/// Purge the given folder.
|
||||
///
|
||||
/// All emails from the given folder are definitely deleted. The
|
||||
/// purged folder will remain empty after execution of the command.
|
||||
|
@ -21,12 +22,11 @@ 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,34 +35,36 @@ impl FolderPurgeCommand {
|
|||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
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);
|
||||
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 (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 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),
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_purge_folder(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.purge_folder(folder).await?;
|
||||
|
||||
printer.print(format!("Folder {folder} successfully purged!"))
|
||||
printer.out(format!("Folder {folder} successfully purged!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
#[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,67 +1,2 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
|
||||
use color_eyre::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 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,545 +0,0 @@
|
|||
use color_eyre::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::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
|
||||
config
|
||||
.client_secret
|
||||
.set_only_keyring(&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::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
|
||||
config.access_token.set_only_keyring(access_token).await?;
|
||||
|
||||
if let Some(refresh_token) = &refresh_token {
|
||||
config.refresh_token =
|
||||
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))?;
|
||||
config.refresh_token.set_only_keyring(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::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
|
||||
secret
|
||||
.set_only_keyring(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_command(
|
||||
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::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
|
||||
config
|
||||
.client_secret
|
||||
.set_only_keyring(&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::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
|
||||
config.access_token.set_only_keyring(access_token).await?;
|
||||
|
||||
if let Some(refresh_token) = &refresh_token {
|
||||
config.refresh_token =
|
||||
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))?;
|
||||
config.refresh_token.set_only_keyring(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::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
|
||||
secret
|
||||
.set_only_keyring(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_command(
|
||||
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))
|
||||
}
|
16
src/lib.rs
16
src/lib.rs
|
@ -1,26 +1,10 @@
|
|||
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 tracing;
|
||||
pub mod ui;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::email::{envelope, flag, message};
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,23 +0,0 @@
|
|||
use color_eyre::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))
|
||||
}
|
43
src/main.rs
43
src/main.rs
|
@ -1,27 +1,20 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::{Result, Section};
|
||||
use color_eyre::Result;
|
||||
use himalaya::{
|
||||
cli::Cli, config::TomlConfig, envelope::command::list::ListEnvelopesCommand,
|
||||
message::command::mailto::MessageMailtoCommand, printer::StdoutPrinter,
|
||||
cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand,
|
||||
message::command::mailto::MessageMailtoCommand,
|
||||
};
|
||||
use pimalaya_tui::terminal::{
|
||||
cli::{printer::StdoutPrinter, tracing},
|
||||
config::TomlConfig as _,
|
||||
};
|
||||
use std::env;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
if std::env::args().any(|arg| arg == "--debug") {
|
||||
env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
if std::env::args().any(|arg| arg == "--trace") {
|
||||
env::set_var("RUST_LOG", "trace");
|
||||
}
|
||||
}
|
||||
let cli = Cli::parse();
|
||||
let tracing = tracing::install()?;
|
||||
|
||||
let filter = himalaya::tracing::install()?;
|
||||
|
||||
let mut printer = StdoutPrinter::new(cli.output, cli.color);
|
||||
#[cfg(feature = "keyring")]
|
||||
secret::keyring::set_global_service_name("himalaya-cli");
|
||||
|
||||
// if the first argument starts by "mailto:", execute straight the
|
||||
// mailto message command
|
||||
|
@ -38,23 +31,17 @@ async fn main() -> Result<()> {
|
|||
.await;
|
||||
}
|
||||
|
||||
let mut res = match cli.command {
|
||||
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,
|
||||
None => {
|
||||
let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
|
||||
ListEnvelopesCommand::default()
|
||||
EnvelopeListCommand::default()
|
||||
.execute(&mut printer, &config)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
if filter < LevelFilter::DEBUG {
|
||||
res = res.note("Run with --debug to enable logs with spantrace.");
|
||||
};
|
||||
|
||||
if filter < LevelFilter::TRACE {
|
||||
res = res.note("Run with --trace to enable verbose logs with backtrace.")
|
||||
};
|
||||
|
||||
res
|
||||
tracing.with_debug_and_trace_notes(res)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use std::{fs, path::PathBuf};
|
||||
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_mangen::Man;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use std::{fs, path::PathBuf};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{cli::Cli, printer::Printer};
|
||||
use crate::cli::Cli;
|
||||
|
||||
/// Generate manual pages to a directory.
|
||||
/// Generate manual pages to the given directory.
|
||||
///
|
||||
/// This command allows you to generate manual pages (following the
|
||||
/// man page format) to the given directory. If the directory does not
|
||||
|
@ -33,7 +35,7 @@ impl ManualGenerateCommand {
|
|||
Man::new(cmd).render(&mut buffer)?;
|
||||
|
||||
fs::create_dir_all(&self.dir)?;
|
||||
printer.print_log(format!("Generating man page for command {cmd_name}…"))?;
|
||||
printer.log(format!("Generating man page for command {cmd_name}…\n"))?;
|
||||
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
|
||||
|
||||
for subcmd in subcmds {
|
||||
|
@ -42,16 +44,18 @@ impl ManualGenerateCommand {
|
|||
let mut buffer = Vec::new();
|
||||
Man::new(subcmd).render(&mut buffer)?;
|
||||
|
||||
printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?;
|
||||
printer.log(format!(
|
||||
"Generating man page for subcommand {subcmd_name}…\n"
|
||||
))?;
|
||||
fs::write(
|
||||
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
|
||||
buffer,
|
||||
)?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"{subcmds_len} man page(s) successfully generated in {:?}!",
|
||||
self.dir
|
||||
printer.log(format!(
|
||||
"{subcmds_len} man page(s) successfully generated in {}!\n",
|
||||
self.dir.display()
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,24 +0,0 @@
|
|||
use color_eyre::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))
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
//! Module related to output CLI.
|
||||
//!
|
||||
//! This module provides arguments related to output.
|
||||
|
||||
use clap::Arg;
|
||||
|
||||
pub(crate) const ARG_COLOR: &str = "color";
|
||||
pub(crate) const ARG_OUTPUT: &str = "output";
|
||||
|
||||
/// Output arguments.
|
||||
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||
[
|
||||
Arg::new(ARG_OUTPUT)
|
||||
.help("Define the output format")
|
||||
.long("output")
|
||||
.short('o')
|
||||
.global(true)
|
||||
.value_name("format")
|
||||
.value_parser(["plain", "json"])
|
||||
.default_value("plain"),
|
||||
Arg::new(ARG_COLOR)
|
||||
.help("Control when to use colors")
|
||||
.long_help(
|
||||
"Control when to use colors.
|
||||
|
||||
The default setting is 'auto', which means himalaya will try to guess
|
||||
when to use colors. For example, if himalaya is printing to a
|
||||
terminal, then it will use colors, but if it is redirected to a file
|
||||
or a pipe, then it will suppress color output. himalaya will suppress
|
||||
color output in some other circumstances as well. For example, if the
|
||||
TERM environment variable is not set or set to 'dumb', then himalaya
|
||||
will not use colors.
|
||||
|
||||
The possible values for this flag are:
|
||||
|
||||
never Colors will never be used.
|
||||
auto The default. himalaya tries to be smart.
|
||||
always Colors will always be used regardless of where output is sent.
|
||||
ansi Like 'always', but emits ANSI escapes (even in a Windows console).",
|
||||
)
|
||||
.long("color")
|
||||
.short('C')
|
||||
.global(true)
|
||||
.value_parser(["never", "auto", "always", "ansi"])
|
||||
.default_value("auto")
|
||||
.value_name("mode"),
|
||||
]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod args;
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod output;
|
||||
|
||||
pub use output::*;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue