mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-20 00:03:40 +00:00
Compare commits
397 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 | ||
![]() |
10de8e9fb4 | ||
![]() |
23ae40e728 | ||
![]() |
220008d0b4 | ||
![]() |
a9e177b77b | ||
![]() |
7f8b08bd81 | ||
![]() |
5a0ff83a5e | ||
![]() |
cc79f5cc38 | ||
![]() |
58df66b5fa | ||
![]() |
d95f277bab | ||
![]() |
ee9718a482 | ||
![]() |
a5ef14da9f | ||
![]() |
2cf30e2fda | ||
![]() |
799ee8b25b | ||
![]() |
1c23adc8a2 | ||
![]() |
7ee710634b | ||
![]() |
3868c62511 | ||
![]() |
362a5ca647 | ||
![]() |
2566d45a96 | ||
![]() |
3b53bcc529 | ||
![]() |
c56a5f285b | ||
![]() |
c1ffc40bd3 | ||
![]() |
ed5407a5c7 | ||
![]() |
da49352d4e | ||
![]() |
8867c99b91 | ||
![]() |
a8e6dea162 | ||
![]() |
46bf3eebfc | ||
![]() |
1e7adc5e0c | ||
![]() |
c28b4c6bb3 | ||
![]() |
1f6f2fcc11 | ||
![]() |
8e8040e036 | ||
![]() |
1699a581ce | ||
![]() |
04982a4644 | ||
![]() |
556949a684 | ||
![]() |
e945c4b8e2 | ||
![]() |
0e35a0cd64 | ||
![]() |
79da9404f3 | ||
![]() |
5cb247169a | ||
![]() |
faeda95978 | ||
![]() |
123224963d | ||
![]() |
1907817392 | ||
![]() |
3e0cf0cfda | ||
![]() |
76ab833a62 | ||
![]() |
8741508413 | ||
![]() |
dd7e1a02be | ||
![]() |
35c1453863 | ||
![]() |
a945e1bf2f | ||
![]() |
83306d5f6a | ||
![]() |
e5cf39b351 | ||
![]() |
34a0978588 | ||
![]() |
7cdfecd7dd | ||
![]() |
b1cc03d2c7 | ||
![]() |
72c3e55bba | ||
![]() |
4f9705952a | ||
![]() |
16266dbc0b | ||
![]() |
39d2dec9e8 | ||
![]() |
4d288b9d51 | ||
![]() |
8cebdf9e90 | ||
![]() |
3137e1e851 | ||
![]() |
a700f358fb | ||
![]() |
7d4ad9c1d9 | ||
![]() |
2342a83d0d | ||
![]() |
7eba3a5186 | ||
![]() |
1246be8a5b | ||
![]() |
a15e2c0442 | ||
![]() |
fc59757a9d | ||
![]() |
87eac50eb7 | ||
![]() |
0b066b7529 | ||
![]() |
a6440aaa27 | ||
![]() |
2af1936ef8 | ||
![]() |
b417ad11a0 | ||
![]() |
0f097fe293 | ||
![]() |
945c567f35 | ||
![]() |
2ef477c225 | ||
![]() |
54287d40b8 | ||
![]() |
bd1ac45a58 | ||
![]() |
0ff940871b | ||
![]() |
f7a7937cb1 | ||
![]() |
59fefd7c78 | ||
![]() |
8016ecb5a0 | ||
![]() |
6fcdf7ea10 | ||
![]() |
6f9f75cfd2 | ||
![]() |
b0d7e773dc | ||
![]() |
921194da5c | ||
![]() |
95eed65193 | ||
![]() |
3cca9ac9e8 | ||
![]() |
d2ad386eaa | ||
![]() |
6173495cb6 | ||
![]() |
42226abc9c | ||
![]() |
161f35d20e | ||
![]() |
819bdc84b3 | ||
![]() |
a6b863759c | ||
![]() |
9ffac16e05 | ||
![]() |
95c078c327 | ||
![]() |
45ce05ec4d | ||
![]() |
38c8a67ddd | ||
![]() |
89fbb8a9db | ||
![]() |
70fad9b1fd | ||
![]() |
0352e91e36 | ||
![]() |
a8c6756f56 | ||
![]() |
37c352ea7f | ||
![]() |
6af2342316 | ||
![]() |
6b6e5cb1fa | ||
![]() |
77206b2326 |
129 changed files with 6291 additions and 9231 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
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
name: Do not open issues on GitHub
|
||||
about: Instead send an email at ~soywod/pimalaya@todo.sr.ht
|
||||
title: ''
|
||||
labels: invalid
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Himalaya is slowly migrating away from GitHub. The new bug tracker is
|
||||
now on [sourcehut](https://sr.ht/). You can submit an issue either by:
|
||||
|
||||
* Sending an email at
|
||||
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht)
|
||||
(it is the simplest since you do not need to create any account)
|
||||
* Submitting [this form](https://todo.sr.ht/~soywod/pimalaya) (you
|
||||
need a free sourcehut account)
|
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 }}
|
160
.github/workflows/release.yml
vendored
160
.github/workflows/release.yml
vendored
|
@ -1,160 +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-unix-releases:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: create-release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: linux
|
||||
os: ubuntu-latest
|
||||
- target: linux-musl
|
||||
os: ubuntu-latest
|
||||
- target: macos
|
||||
os: macos-latest
|
||||
# TODO: uncomment once nix build .#windows works
|
||||
# - target: windows
|
||||
# os: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v24
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-23.11
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: soywod
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Build release
|
||||
run: nix build -L .#${{ matrix.target }}
|
||||
- name: Copy binary
|
||||
run: |
|
||||
cp result/bin/himalaya* .
|
||||
- name: Patch binary interpreter
|
||||
if: ${{ matrix.target == 'linux' }}
|
||||
run: |
|
||||
nix-shell -p patchelf --command "sudo patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 himalaya"
|
||||
- name: Prepare release archives
|
||||
run: |
|
||||
mkdir -p {man,completions}
|
||||
nix run .#${{ matrix.target }} man ./man
|
||||
nix run .#${{ matrix.target }} completion bash > ./completions/himalaya.bash
|
||||
nix run .#${{ matrix.target }} completion elvish > ./completions/himalaya.elvish
|
||||
nix run .#${{ matrix.target }} completion fish > ./completions/himalaya.fish
|
||||
nix run .#${{ matrix.target }} completion powershell > ./completions/himalaya.powershell
|
||||
nix run .#${{ matrix.target }} completion zsh > ./completions/himalaya.zsh
|
||||
tar -czf himalaya.tgz himalaya* man completions
|
||||
zip -r himalaya.zip himalaya* man completions
|
||||
- name: Upload tarball release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya-${{ matrix.target }}.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya-${{ matrix.target }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
# TODO: remove me once nix build .#windows works
|
||||
deploy-windows-release:
|
||||
runs-on: windows-latest
|
||||
needs: create-release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
- name: Build release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release
|
||||
- name: Copy binary
|
||||
run: |
|
||||
copy target/release/himalaya.exe .
|
||||
- name: Prepare release archives
|
||||
run: |
|
||||
mkdir man
|
||||
mkdir completions
|
||||
./himalaya.exe man ./man
|
||||
./himalaya.exe completion bash > ./completions/himalaya.bash
|
||||
./himalaya.exe completion elvish > ./completions/himalaya.elvish
|
||||
./himalaya.exe completion fish > ./completions/himalaya.fish
|
||||
./himalaya.exe completion powershell > ./completions/himalaya.powershell
|
||||
./himalaya.exe completion zsh > ./completions/himalaya.zsh
|
||||
tar -czf himalaya.tgz himalaya.exe man completions
|
||||
Compress-Archive -Path himalaya.exe,man,completions -DestinationPath himalaya.zip
|
||||
- name: Upload tarball release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya-windows.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya-windows.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
publish-crates-io:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v24
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-23.11
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- name: Publish library to crates.io
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
nix develop -c cargo publish --no-verify --token ${CARGO_REGISTRY_TOKEN}
|
15
.github/workflows/releases.yml
vendored
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
|
24
.github/workflows/tests.yml
vendored
24
.github/workflows/tests.yml
vendored
|
@ -1,24 +0,0 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v20
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-22.11
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: soywod
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Build then test
|
||||
run: nix build
|
694
CHANGELOG.md
694
CHANGELOG.md
|
@ -7,6 +7,236 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.0] - 2025-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added `-y|--yes` flag for `folder purge` and `folder delete` commands. [#469]
|
||||
|
||||
### Changed
|
||||
|
||||
- Put back `warn` the default log level. [#522]
|
||||
|
||||
Since logs are sent to `stderr`, warnings can be easily discarded by prepending commands with `RUST_LOG=off` or by appending commands with `2>/dev/null`.
|
||||
|
||||
- Changed `message.send.save-copy` default to `true`. [#536]
|
||||
|
||||
- Changed default downloads directory. [core#1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed permissions issues when using `install.sh`. [#515]
|
||||
- Fixed de/serialization issues of backends' `none` variant. [#523]
|
||||
- Fixed list envelopes out of bound error when empty result. [#535]
|
||||
- Fixed macOS x86_64 builds. [#538]
|
||||
|
||||
## [1.0.0] - 2024-12-09
|
||||
|
||||
The Himalaya CLI scope has changed. It does not include anymore the synchronization, nor the envelope watching. These scopes have moved to dedicated projects:
|
||||
|
||||
- [Neverest CLI](https://github.com/pimalaya/neverest), CLI to synchronize, backup and restore emails
|
||||
- [Mirador CLI](https://github.com/pimalaya/mirador), CLI to watch mailbox changes
|
||||
|
||||
Due to the long time difference with the previous `v1.0.0-beta.4` release, this changelog may be incomplete. The simplest way to upgrade is to reconfigure Himalaya CLI from scratch, using the wizard or the [`config.sample.toml`](./config.sample.toml).
|
||||
|
||||
Himalaya CLI will now try to adopt the [conventional commits specification](https://github.com/conventional-commits/conventionalcommits.org). Tools like [`git-cliff`](https://git-cliff.org/) may help us generating more accurate changelogs in the future.
|
||||
|
||||
### Added
|
||||
|
||||
- Added `message edit` command to edit a message. To edit on place (replace a message), use `--on-place`.
|
||||
- Added `account.list.table.preset` global config option, `accounts.<name>.folder.list.table.preset` and `accounts.<name>.envelope.list.table.preset` account config options.
|
||||
|
||||
These options customize the shape of tables, see examples at [`comfy_table::presets`](https://docs.rs/comfy-table/latest/comfy_table/presets/index.html). Defaults to `"|| |-||| "`, which corresponds to [`comfy_table::presets::ASCII_MARKDOWN`](https://docs.rs/comfy-table/latest/comfy_table/presets/constant.ASCII_MARKDOWN.html).
|
||||
|
||||
- Added `account.list.table.name-color` config option to customize the color used for the accounts' `NAME` column (defaults to `green`).
|
||||
- Added `account.list.table.backends-color` config option to customize the color used for the folders' `BACKENDS` column (defaults to `blue`).
|
||||
- Added `account.list.table.default-color` config option to customize the color used for the folders' `DEFAULT` column (defaults to `reset`).
|
||||
- Added `accounts.<name>.folder.list.table.name-color` account config option to customize the color used for the folders' `NAME` column (defaults to `blue`).
|
||||
- Added `accounts.<name>.folder.list.table.desc-color` account config option to customize the color used for the folders' `DESC` column (defaults to `green`).
|
||||
- Added `accounts.<name>.envelope.list.table.id-color` account config option to customize the color used for the envelopes' `ID` column (defaults to `red`).
|
||||
- Added `accounts.<name>.envelope.list.table.flags-color` account config option to customize the color used for the envelopes' `FLAGS` column (defaults to `reset`).
|
||||
- Added `accounts.<name>.envelope.list.table.subject-color` account config option to customize the color used for the envelopes' `SUBJECT` column (defaults to `green`).
|
||||
- Added `accounts.<name>.envelope.list.table.sender-color` account config option to customize the color used for the envelopes' `FROM` column (defaults to `blue`).
|
||||
- Added `accounts.<name>.envelope.list.table.date-color` account config option to customize the color used for the envelopes' `DATE` column (defaults to `dark_yellow`).
|
||||
- Added `accounts.<name>.envelope.list.table.unseen-char` account config option to customize the char used for unseen envelopes (defaults to `*`).
|
||||
- Added `accounts.<name>.envelope.list.table.replied-char` account config option to customize the char used for replied envelopes (defaults to `R`).
|
||||
- Added `accounts.<name>.envelope.list.table.flagged-char` account config option to customize the char used for flagged envelopes (defaults to `!`).
|
||||
- Added `accounts.<name>.envelope.list.table.attachment-char` account config option to customize the char used for envelopes with at least one attachment (defaults to `@`).
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored the `account configure` command: this command stands now for creating or editing account configurations from the wizard. The command requires the `wizard` cargo feature.
|
||||
- Improved the `account doctor` command: it now checks the state of the config, and the new `--fix` argument allows you to configure keyring, OAuth 2.0 etc.
|
||||
- Improved long version `--version`. [#496]
|
||||
- Improved error messages when missing cargo features. For example, if a TOML configuration uses the IMAP backend without the `imap` cargo features, the error `missing "imap" feature` is displayed. [#20](https://github.com/pimalaya/core/issues/20)
|
||||
- Normalized enum-based configurations, using the [internally tagged representation](https://serde.rs/enum-representations.html#internally-tagged) `type =`. It should reduce issues due to misconfiguration, and improve othe error messages. Yet it is not perfect, see [#802](https://github.com/toml-rs/toml/issues/802):
|
||||
|
||||
- `imap.*`, `maildir.*` and `notmuch.*` moved to `backend.*`:
|
||||
|
||||
```toml
|
||||
# before
|
||||
imap.host = "localhost"
|
||||
imap.port = 143
|
||||
|
||||
# after
|
||||
backend.type = "imap"
|
||||
backend.host = "localhost"
|
||||
backend.port = 143
|
||||
```
|
||||
|
||||
- `smtp.*` and `sendmail.*` moved to `message.send.backend.*`:
|
||||
|
||||
```toml
|
||||
# before
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 25
|
||||
|
||||
# after
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "localhost"
|
||||
message.send.backend.port = 25
|
||||
```
|
||||
|
||||
- `pgp.backend` renamed `pgp.type`:
|
||||
|
||||
```toml
|
||||
# before
|
||||
pgp.backend = "commands"
|
||||
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
|
||||
|
||||
# after
|
||||
pgp.type = "commands"
|
||||
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
|
||||
```
|
||||
|
||||
- `{imap,smtp}.auth` moved as well:
|
||||
|
||||
```toml
|
||||
# before
|
||||
imap.password.cmd = "pass show example"
|
||||
smtp.oauth2.method = "xoauth2"
|
||||
|
||||
# after
|
||||
backend.auth.type = "password"
|
||||
backend.auth.cmd = "pass show example"
|
||||
message.send.backend.auth.type = "oauth2"
|
||||
message.send.backend.auth.method = "xoauth2"
|
||||
```
|
||||
|
||||
- Moved IMAP and SMTP `encryption` to `encryption.type`.
|
||||
|
||||
This change prepares the config to accept different TLS providers with their options. The `true` and `false` variant have been removed as well:
|
||||
|
||||
```toml
|
||||
# before
|
||||
backend.encryption = "none" # or false
|
||||
backend.encryption = "start-tls"
|
||||
message.send.backend.encryption = "tls" # or true
|
||||
|
||||
# after
|
||||
backend.encryption.type = "none"
|
||||
backend.encryption.type = "start-tls"
|
||||
message.send.backend.encryption.type = "tls"
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed pre-release archives issue. [#492]
|
||||
- Fixed mailto parsing issue. [core#10]
|
||||
- Fixed `Answered` flag not set when replying to a message. [#508]
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed systemd service from `assets/` folder, as Himalaya CLI scope does not include synchronization nor watching anymore.
|
||||
|
||||
## [1.0.0-beta.4] - 2024-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added systemd service in `assets/` folder.
|
||||
- Added configuration option `message.delete.style` that can be either `folder` (deleted messages are moved to the Trash folder, default style) or `flag` (deleted messages receive the Deleted flag).
|
||||
- Added `--debug` as an alias for `RUST_LOG=debug`.
|
||||
- Added `--trace` as an alias for `RUST_LOG=trace` and `RUST_BACKTRACE=1`.
|
||||
- Added notes about `--debug` and `--trace` when error occurs.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Added back the search feature**: you can now give an optional filter and sort query at the end of the `envelope list` command. See `envelope list --help` or [pimalaya.org](https://pimalaya.org/himalaya/cli/master/usage/advanced/envelope/list.html#query) for more detail on the search API.
|
||||
- Changed the `envelope list` folder argument due to the search query: it became a flag `--folder|-f`.
|
||||
- Made the global `--config|-c` option repeatable: the first option is considered the path to the main config, and successive options are considered partial overrides [#184].
|
||||
- Refactored error management: error should be more clear, colored and can now contain spantrace and backtrace.
|
||||
- Made `--help` content wrapping properly thanks to the `clap` cargo feature `wrap_help`.
|
||||
- Improved `template {new,reply,forward}` command JSON output: they return now a JSON object with 3 properties:
|
||||
- `content`: the content of the template
|
||||
- `cursor.row`: the row at which the cursor should be placed by the interface using the template
|
||||
- `cursor.col`: the column at which the cursor should be placed by the interface using the template
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed watch IMAP envelopes when folder was empty [#179].
|
||||
- Prevented parsing of undefined config options [#188].
|
||||
- Fixed `In-Reply-To` header being skipped from mailto URLs [#194].
|
||||
- Fixed error page out of bounds when filtering envelopes returned an empty result [#195].
|
||||
|
||||
## [1.0.0-beta.3] - 2024-02-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added `account check-up` command.
|
||||
- Added wizard warning about google passwords [#41].
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed account configurations flatten level in order to improve diagnostic errors, due to a [bug](https://github.com/toml-rs/toml/issues/589#issuecomment-1872345017) in clap. **This means that accounts need to be prefixed by `accounts`: `[my-account]` becomes `[accounts.my-account]`**. It also opens doors for interface-specific configurations.
|
||||
- Rolled back cargo feature additions from the previous release. It was a mistake: the amount of features was too big, the code (both CLI and lib) was too hard to maintain. Cargo features kept: `imap`, `maildir`, `notmuch`, `smtp`, `sendmail`, `account-sync`, `account-discovery`, `pgp-gpg`, `pgp-commands` and `pgp-native`.
|
||||
- Moved `sync.strategy` to `folder.sync.filter`.
|
||||
- Changed location of the synchronization data from `$XDG_DATA_HOME/himalaya/<account-name>` to `$XDG_DATA_HOME/pimalaya/email/sync/<account-name>-cache`.
|
||||
- Changed location of the synchronization cache from `sync.dir` to `$XDG_CACHE_HOME/pimalaya/email/sync/<hash>/`.
|
||||
- Replaced id mapping database `SQLite` by `sled`, a pure key-val store written in Rust to improve portability of the tool. **Therefore, id aliases are reset**.
|
||||
- Improved pre and post edit choices interaction [#58].
|
||||
- Improved account synchronization performances, making it 50% faster than `mbsync` and 370% faster than `OfflineIMAP`.
|
||||
- Changed `envelope.watch.{event}.{hook}`: hooks can now be cumulated. For example it is possible to send a system notification and execute a shell command when receiving a new envelope:
|
||||
|
||||
```toml
|
||||
envelope.watch.received.notify.summary = "New message from {sender}"
|
||||
envelope.watch.received.notify.body = "{subject}"
|
||||
envelope.watch.received.cmd = "echo {id} >> /tmp/new-email-counter"
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed bug that was preventing watch placeholders to be replaced when using shell command hook.
|
||||
- Fixed watch IMAP envelopes issue preventing events to be triggered.
|
||||
- Fixed DNS account discovery priority issues.
|
||||
- Fixed SMTP messages not properly sent to all recipients [#172].
|
||||
- Fixed backend feature badly linked, leading to reply and forward message errors [#173].
|
||||
|
||||
## [1.0.0-beta.2] - 2024-01-27
|
||||
|
||||
### Added
|
||||
|
||||
- Added cargo feature `wizard`, enabled by default.
|
||||
- Added one cargo feature per backend feature:
|
||||
- `account` including `account-configure`, `account-list`, `account-sync` and the `account-subcmd`
|
||||
- `folder` including `folder-add`, `folder-list`, `folder-expunge`, `folder-purge`, `folder-delete` and the `folder-subcmd`
|
||||
- `envelope` including `envelope-list`, `envelope-watch`, `envelope-get` and the `envelope-subcmd`
|
||||
- `flag` including `flag-add`, `flag-set`, `flag-remove` and the `flag-subcmd`
|
||||
- `message` including `message-read`, `message-write`, `message-mailto`, `message-reply`, `message-forward`, `message-copy`, `message-move`, `message-delete`, `message-save`, `message-send` and the `message-subcmd`
|
||||
- `attachment` including `attachment-download` and the `attachment-subcmd`
|
||||
- `template` including `template-write`, `template-reply`, `template-forward`, `template-save`, `template-send` and the `template-subcmd`
|
||||
- Added wizard capability to autodetect IMAP and SMTP configurations, based on the [Thunderbird Autoconfiguration](https://wiki.mozilla.org/Thunderbird:Autoconfiguration) standard.
|
||||
- Added back Notmuch backend features.
|
||||
|
||||
### Changed
|
||||
|
||||
- Renamed `folder create` to `folder add` in order to better match types. An alias has been set up, so both `create` and `add` still work.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed default command: running `himalaya` without argument lists envelopes, as it used to be in previous versions.
|
||||
- Fixed bug when listing envelopes with `backend = "imap"`, `sync.enable = true` and `envelope.watch.backend = "imap"` led to unwanted IMAP connection creation (which slowed down the listing).
|
||||
- Fixed builds related to enabled cargo features.
|
||||
|
||||
## [1.0.0-beta] - 2024-01-01
|
||||
|
||||
Few major concepts changed:
|
||||
|
@ -81,7 +311,7 @@ Few major concepts changed:
|
|||
### Removed
|
||||
|
||||
- Disabled temporarily the `notmuch` backend because it needs to be refactored using the backend features system (it should be reimplemented soon).
|
||||
- Disabled temporarily the `search` and `sort` command because they need to be refactored, see [#39](https://todo.sr.ht/~soywod/pimalaya/39).
|
||||
- Disabled temporarily the `search` and `sort` command because they need to be refactored, see [#39].
|
||||
- Removed the `notify` command (replaced by the new `watch` command).
|
||||
- Removed all global options except for `display-name`, `signature`, `signature-delim` and `downloads-dir`.
|
||||
|
||||
|
@ -124,7 +354,7 @@ Few major concepts changed:
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fixed the way folder aliases are resolved. In some case, aliases were resolved CLI side and lib side, which led to alias errors [sourcehut#95].
|
||||
- Fixed the way folder aliases are resolved. In some case, aliases were resolved CLI side and lib side, which led to alias errors [#95].
|
||||
|
||||
## [0.8.1] - 2023-06-15
|
||||
|
||||
|
@ -196,9 +426,9 @@ Few major concepts changed:
|
|||
|
||||
### Added
|
||||
|
||||
- Added `create` and `delete` folder commands [sourcehut#54].
|
||||
- Added generated completions and man pages to releases [sourcehut#43].
|
||||
- Added new account config option `sync-folders-strategy` which allows to choose a folders synchronization strategy [sourcehut#59]:
|
||||
- Added `create` and `delete` folder commands [#54].
|
||||
- Added generated completions and man pages to releases [#43].
|
||||
- Added new account config option `sync-folders-strategy` which allows to choose a folders synchronization strategy [#59]:
|
||||
|
||||
- `sync-folders-strategy = "all"`: synchronize all existing folders for the current account
|
||||
- `sync-folders-strategy.include = ["folder1", "folder2", …]`: synchronize only the given folders for the current account
|
||||
|
@ -214,7 +444,7 @@ Few major concepts changed:
|
|||
|
||||
### Changed
|
||||
|
||||
- Made global options truly global, which means they can be used everywhere (not only *before* commands but also *after*) [sourcehut#60].
|
||||
- Made global options truly global, which means they can be used everywhere (not only *before* commands but also *after*) [#60].
|
||||
- Replaced reply all `-a` argument with `-A` because it conflicted with the global option `-a|--account`.
|
||||
- Replaced `himalaya-lib` by `pimalaya-email`.
|
||||
- Renamed feature `vendored` to `native-tls-vendored`.
|
||||
|
@ -248,11 +478,11 @@ Few major concepts changed:
|
|||
|
||||
### Added
|
||||
|
||||
- Added offline support with the `account sync` command to synchronize a backend to a local Maildir backend [#342].
|
||||
- Added offline support with the `account sync` command to synchronize a backend to a local Maildir backend.
|
||||
- Added the flag `--disable-cache` to not use the local Maildir backend.
|
||||
- Added the email composer (from its own [repository](https://git.sr.ht/~soywod/mime-msg-builder)) [#341].
|
||||
- Added Musl builds to releases [#356].
|
||||
- Added `himalaya man` command to generate man page [#419].
|
||||
- Added the email composer (from its own [repository](https://git.sr.ht/~soywod/mime-msg-builder)).
|
||||
- Added Musl builds to releases.
|
||||
- Added `himalaya man` command to generate man page.
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -261,7 +491,7 @@ Few major concepts changed:
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fixed missing folder aliases [#430].
|
||||
- Fixed missing folder aliases.
|
||||
|
||||
### Removed
|
||||
|
||||
|
@ -282,23 +512,23 @@ Few major concepts changed:
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fixed empty text bodies when reading html part on plain text email [#352].
|
||||
- Fixed empty text bodies when reading html part on plain text email.
|
||||
|
||||
## [0.6.0] - 2022-10-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Separated the CLI from the lib module [#340].
|
||||
- Separated the CLI from the lib module.
|
||||
|
||||
The source code has been split into subrepositories:
|
||||
|
||||
- The email logic has been extracted from the CLI and placed in a lib on [sourcehut](https://git.sr.ht/~soywod/himalaya-lib)
|
||||
- The vim plugin is now in a dedicated repository on [sourcehut](https://git.sr.ht/~soywod/himalaya-vim) as well
|
||||
- This repository only contains the CLI source code (it was not possible to move it to sourcehut because of cross platform builds)
|
||||
- The email logic has been extracted from the CLI and placed in a lib on [SourceHut](https://git.sr.ht/~soywod/himalaya-lib)
|
||||
- The vim plugin is now in a dedicated repository on [SourceHut](https://git.sr.ht/~soywod/himalaya-vim) as well
|
||||
- This repository only contains the CLI source code (it was not possible to move it to SourceHut because of cross platform builds)
|
||||
|
||||
- [**BREAKING**] Renamed `-m|--mailbox` to `-f|--folder`
|
||||
|
||||
- [**BREAKING**] Refactored config system [#344].
|
||||
- [**BREAKING**] Refactored config system.
|
||||
|
||||
The configuration has been rethought in order to be more intuitive and structured. Here are the breaking changes for the global config:
|
||||
|
||||
|
@ -329,92 +559,92 @@ Few major concepts changed:
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fixed flag commands [#334].
|
||||
- Fixed Windows build [#346].
|
||||
- Fixed flag commands.
|
||||
- Fixed Windows build.
|
||||
|
||||
## [0.5.9] - 2022-03-12
|
||||
|
||||
### Added
|
||||
|
||||
- SMTP pre-send hook [#178]
|
||||
- Customize headers to show at the top of a read message [#338]
|
||||
- SMTP pre-send hook
|
||||
- Customize headers to show at the top of a read message
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve `attachments` command [#281]
|
||||
- Improve `attachments` command
|
||||
|
||||
### Fixed
|
||||
|
||||
- `In-Reply-To` not set properly when replying to a message [#323]
|
||||
- `Cc` missing or invalid when replying to a message [#324]
|
||||
- Notmuch backend hangs [#329]
|
||||
- Maildir e2e tests [#335]
|
||||
- JSON API for listings [#331]
|
||||
- `In-Reply-To` not set properly when replying to a message
|
||||
- `Cc` missing or invalid when replying to a message
|
||||
- Notmuch backend hangs
|
||||
- Maildir e2e tests
|
||||
- JSON API for listings
|
||||
|
||||
## [0.5.8] - 2022-03-04
|
||||
|
||||
### Added
|
||||
|
||||
- Flowed format support [#206]
|
||||
- List accounts command [#244]
|
||||
- One cargo feature per backend [#318]
|
||||
- Flowed format support
|
||||
- List accounts command
|
||||
- One cargo feature per backend
|
||||
|
||||
### Changed
|
||||
|
||||
- Vim doc about mailbox pickers [#298]
|
||||
- Vim doc about mailbox pickers
|
||||
|
||||
### Fixed
|
||||
|
||||
- Some emojis break the table layout [#300]
|
||||
- Bad sender and date in reply and forward template [#321]
|
||||
- Some emojis break the table layout
|
||||
- Bad sender and date in reply and forward template
|
||||
|
||||
## [0.5.7] - 2022-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Notmuch support [#57]
|
||||
- Notmuch support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Build failure due to `imap` version [#303]
|
||||
- No tilde expansion in `maildir-dir` [#305]
|
||||
- Unknown command SORT [#308]
|
||||
- Build failure due to `imap` version
|
||||
- No tilde expansion in `maildir-dir`
|
||||
- Unknown command SORT
|
||||
|
||||
### Changed
|
||||
|
||||
- [**BREAKING**] Replace `inbox-folder`, `sent-folder` and `draft-folder` by a generic hashmap `mailboxes`
|
||||
- Display short envelopes id for `maildir` and `notmuch` backends [#309]
|
||||
- Display short envelopes id for `maildir` and `notmuch` backends
|
||||
|
||||
## [0.5.6] - 2022-02-22
|
||||
|
||||
### Added
|
||||
|
||||
- Sort command [#34]
|
||||
- Maildir support [#43]
|
||||
- Sort command
|
||||
- Maildir support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Suffix to downloaded attachments with same name [#204]
|
||||
- Suffix to downloaded attachments with same name
|
||||
|
||||
## [0.5.5] - 2022-02-08
|
||||
|
||||
### Added
|
||||
|
||||
- [Contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) [#256]
|
||||
- Notify query config option [#289]
|
||||
- End-to-end encryption [#54]
|
||||
- [Contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md)
|
||||
- Notify query config option
|
||||
- End-to-end encryption
|
||||
|
||||
### Fixed
|
||||
|
||||
- Multiple recipients issue [#288]
|
||||
- Cannot parse address [#227]
|
||||
- Multiple recipients issue
|
||||
- Cannot parse address
|
||||
|
||||
## [0.5.4] - 2022-02-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add attachments with save and send commands [#47] [#259]
|
||||
- Invalid sequence set [#276]
|
||||
- Add attachments with save and send commands
|
||||
- Invalid sequence set
|
||||
|
||||
## [0.5.3] - 2022-02-03
|
||||
|
||||
|
@ -427,194 +657,194 @@ Few major concepts changed:
|
|||
|
||||
### Fixed
|
||||
|
||||
- Blur in list msg screenshot [#181]
|
||||
- Make inbox, sent and drafts folders customizable [#172]
|
||||
- Vim plugin get focused msg id [#268]
|
||||
- Nix run issue [#272]
|
||||
- Range not displayed when fetch fails [#276]
|
||||
- Blank lines and spaces in `text/plain` parts [#280]
|
||||
- Watch command [#271]
|
||||
- Mailbox telescope.nvim preview [#249]
|
||||
- Blur in list msg screenshot
|
||||
- Make inbox, sent and drafts folders customizable
|
||||
- Vim plugin get focused msg id
|
||||
- Nix run issue
|
||||
- Range not displayed when fetch fails
|
||||
- Blank lines and spaces in `text/plain` parts
|
||||
- Watch command
|
||||
- Mailbox telescope.nvim preview
|
||||
|
||||
### Removed
|
||||
|
||||
- The wiki git submodule [#273]
|
||||
- The wiki git submodule
|
||||
|
||||
## [0.5.1] - 2021-10-24
|
||||
|
||||
### Added
|
||||
|
||||
- Disable color feature [#185]
|
||||
- `--max-width|-w` argument to restrict listing table width [#220]
|
||||
- Disable color feature
|
||||
- `--max-width|-w` argument to restrict listing table width
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error when receiving notification from `notify` command [#228]
|
||||
- Error when receiving notification from `notify` command
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove error when empty subject [#229]
|
||||
- Vim plugin does not render anymore the msg by itself, it uses the one available from the CLI [#220]
|
||||
- Remove error when empty subject
|
||||
- Vim plugin does not render anymore the msg by itself, it uses the one available from the CLI
|
||||
|
||||
## [0.5.0] - 2021-10-10
|
||||
|
||||
### Added
|
||||
|
||||
- Mailto support [#162]
|
||||
- Remove previous signature when replying/forwarding a message [#193]
|
||||
- Config option `signature-delimiter` to customize the signature delimiter (default to `-- \n`) [[#114](https://github.com/soywod/himalaya/pull/114)]
|
||||
- Expand tilde and env vars for `downloads-dir` and `signature` [#102]
|
||||
- Mailto support
|
||||
- Remove previous signature when replying/forwarding a message
|
||||
- Config option `signature-delimiter` to customize the signature delimiter (default to `-- \n`)
|
||||
- Expand tilde and env vars for `downloads-dir` and `signature`
|
||||
|
||||
### Changed
|
||||
|
||||
- [**BREAKING**] Folder structure, message management, JSON API and Vim plugin [#199]
|
||||
- Pagination for list and search cmd starts from 1 instead of 0 [#186]
|
||||
- Errors management with `anyhow` [#152]
|
||||
- [**BREAKING**] Folder structure, message management, JSON API and Vim plugin
|
||||
- Pagination for list and search cmd starts from 1 instead of 0
|
||||
- Errors management with `anyhow`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Panic on flags command [#190]
|
||||
- Make more use of serde [#153]
|
||||
- Write message vim plugin [#196]
|
||||
- Invalid encoding when sending message [#205]
|
||||
- Pagination reset current account [#215]
|
||||
- New/reply/forward from Vim plugin since Tpl refactor [#176]
|
||||
- Panic on flags command
|
||||
- Make more use of serde
|
||||
- Write message vim plugin
|
||||
- Invalid encoding when sending message
|
||||
- Pagination reset current account
|
||||
- New/reply/forward from Vim plugin since Tpl refactor
|
||||
|
||||
## [0.4.0] - 2021-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- Add ability to change account in with the Vim plugin [#91]
|
||||
- Add possibility to make Himalaya default email app [#160] [[#161](https://github.com/soywod/himalaya/pull/161)]
|
||||
- Add ability to change account in with the Vim plugin
|
||||
- Add possibility to make Himalaya default email app
|
||||
|
||||
### Changed
|
||||
|
||||
- [**BREAKING**] Short version of reply `--all` arg is now `-A` to
|
||||
avoid conflicts with `--attachment|-a`
|
||||
- Template management [#80]
|
||||
- Template management
|
||||
|
||||
### Fixed
|
||||
|
||||
- `\Seen` flag when moving a message
|
||||
- Attachments arg for reply and forward commands [#109]
|
||||
- Vim doc [#117]
|
||||
- Attachments arg for reply and forward commands
|
||||
- Vim doc
|
||||
|
||||
### Removed
|
||||
|
||||
- `Content-Type` from templates [#146]
|
||||
- `Content-Type` from templates
|
||||
|
||||
## [0.3.2] - 2021-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- Mailbox attributes [#134]
|
||||
- Wiki entry about new messages counter [#121]
|
||||
- Copy/move/delete a message in vim [#95]
|
||||
- Mailbox attributes
|
||||
- Wiki entry about new messages counter
|
||||
- Copy/move/delete a message in vim
|
||||
|
||||
### Changed
|
||||
|
||||
- Get signature from file [#135]
|
||||
- Get signature from file
|
||||
- [**BREAKING**] Split `idle` command into two commands:
|
||||
- `notify`: Runs `notify-cmd` when a new message arrives to the server
|
||||
- `watch`: Runs `watch-cmds` when any change occurs on the server
|
||||
|
||||
### Removed
|
||||
|
||||
- `.exe` extension from release binaries [#144]
|
||||
- `.exe` extension from release binaries
|
||||
|
||||
## [0.3.1] - 2021-05-04
|
||||
|
||||
### Added
|
||||
|
||||
- Send message via stdin [#78]
|
||||
- Send message via stdin
|
||||
|
||||
### Fixed
|
||||
|
||||
- Table with subject containing `\r`, `\n` or `\t` [#141]
|
||||
- Overflow panic when shrink column [#138]
|
||||
- Vim plugin empty mailbox message [#136]
|
||||
- Table with subject containing `\r`, `\n` or `\t`
|
||||
- Overflow panic when shrink column
|
||||
- Vim plugin empty mailbox message
|
||||
|
||||
## [0.3.0] - 2021-04-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- IDLE mode after network interruption [#123]
|
||||
- Output redirected to `stderr` [#130]
|
||||
- Refactor table system [#132]
|
||||
- Editon file format on Linux [#133]
|
||||
- Show email address when name not available [#131]
|
||||
- IDLE mode after network interruption
|
||||
- Output redirected to `stderr`
|
||||
- Refactor table system
|
||||
- Editon file format on Linux
|
||||
- Show email address when name not available
|
||||
|
||||
### Removed
|
||||
|
||||
- `--log-level|-l` arg (replaced by default `RUST_LOG` env var from `env_logger`) [#130]
|
||||
- `--log-level|-l` arg (replaced by default `RUST_LOG` env var from `env_logger`)
|
||||
|
||||
## [0.2.7] - 2021-04-24
|
||||
|
||||
### Added
|
||||
|
||||
- Default page size to config [#96]
|
||||
- Custom config path [#86]
|
||||
- Default page size to config
|
||||
- Custom config path
|
||||
- Setting idle-hook-cmds
|
||||
|
||||
### Changed
|
||||
|
||||
- Plain logger with `env_logger` [#126]
|
||||
- Refresh email list on load buffer [#125]
|
||||
- Plain logger with `env_logger`
|
||||
- Refresh email list on load buffer
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve config compatibility on Windows [[#111](https://github.com/soywod/himalaya/pull/111)]
|
||||
- Vim table containing emoji [#122]
|
||||
- Improve config compatibility on Windows
|
||||
- Vim table containing emoji
|
||||
|
||||
## [0.2.6] - 2021-04-17
|
||||
|
||||
### Added
|
||||
|
||||
- Insecure TLS option [#84] [#103](https://github.com/soywod/himalaya/pull/103) [[#105](https://github.com/soywod/himalaya/pull/105)]
|
||||
- Completion subcommands [[#99](https://github.com/soywod/himalaya/pull/99)]
|
||||
- Vim flags to enable telescope preview and to choose picker [[#97](https://github.com/soywod/himalaya/pull/97)]
|
||||
- Insecure TLS option
|
||||
- Completion subcommands
|
||||
- Vim flags to enable telescope preview and to choose picker
|
||||
|
||||
### Changed
|
||||
|
||||
- Make `install.sh` POSIX compliant [[#53](https://github.com/soywod/himalaya/pull/53)]
|
||||
- Make `install.sh` POSIX compliant
|
||||
|
||||
### Fixed
|
||||
|
||||
- SMTP port [#87]
|
||||
- Save msg upon error [#59]
|
||||
- Answered flag not set [#50]
|
||||
- Panic when downloads-dir does not exist [#100]
|
||||
- Idle mode incorrect new message notification [#48]
|
||||
- SMTP port
|
||||
- Save msg upon error
|
||||
- Answered flag not set
|
||||
- Panic when downloads-dir does not exist
|
||||
- Idle mode incorrect new message notification
|
||||
|
||||
## [0.2.5] - 2021-04-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Expunge mbox after `move` and `delete` cmd [#83]
|
||||
- JSON output [#89]
|
||||
- Expunge mbox after `move` and `delete` cmd
|
||||
- JSON output
|
||||
|
||||
## [0.2.4] - 2021-04-09
|
||||
|
||||
### Added
|
||||
|
||||
- Wiki entry for Gmail users [#58]
|
||||
- Info logs for copy/move/delete cmd + silent mode [#74]
|
||||
- `--raw` arg for `read` cmd [#79]
|
||||
- Wiki entry for Gmail users
|
||||
- Info logs for copy/move/delete cmd + silent mode
|
||||
- `--raw` arg for `read` cmd
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor output system + log levels [#74]
|
||||
- Refactor output system + log levels
|
||||
|
||||
## [0.2.3] - 2021-04-08
|
||||
|
||||
### Added
|
||||
|
||||
- Telescope support [#61]
|
||||
- Telescope support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unicode chars breaks the view [#71]
|
||||
- Copy/move incomplete (missing parts) [#75]
|
||||
- Unicode chars breaks the view
|
||||
- Copy/move incomplete (missing parts)
|
||||
|
||||
## [0.2.2] - 2021-04-04
|
||||
|
||||
|
@ -631,59 +861,63 @@ Few major concepts changed:
|
|||
|
||||
### Added
|
||||
|
||||
- IDLE support [#29]
|
||||
- Improve choice after editing msg [#30]
|
||||
- Flags management [#41]
|
||||
- Copy feature [#35]
|
||||
- Move feature [#31]
|
||||
- Delete feature [#36]
|
||||
- Signature support [#33]
|
||||
- Add attachment(s) to a message (CLI) [#37]
|
||||
- IDLE support
|
||||
- Improve choice after editing msg
|
||||
- Flags management
|
||||
- Copy feature
|
||||
- Move feature
|
||||
- Delete feature
|
||||
- Signature support
|
||||
- Add attachment(s) to a message (CLI)
|
||||
|
||||
### Changed
|
||||
|
||||
- Errors management with `error_chain` [#39]
|
||||
- Errors management with `error_chain`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Missing `FLAGS` column in messages table [#40]
|
||||
- Subtract with overflow if next page empty [#38]
|
||||
- Missing `FLAGS` column in messages table
|
||||
- Subtract with overflow if next page empty
|
||||
|
||||
## [0.2.0] - 2021-03-10
|
||||
|
||||
### Added
|
||||
|
||||
- STARTTLS support [#32]
|
||||
- Flags [#25]
|
||||
- STARTTLS support
|
||||
- Flags
|
||||
|
||||
### Changed
|
||||
|
||||
- JSON support [#18]
|
||||
- JSON support
|
||||
|
||||
## [0.1.0] - 2021-01-17
|
||||
|
||||
### Added
|
||||
|
||||
- Parse TOML config [#1]
|
||||
- Populate Config struct from TOML [#2]
|
||||
- Set up IMAP connection [#3]
|
||||
- List new emails [#6]
|
||||
- Set up CLI arg parser [#15]
|
||||
- List mailboxes command [#5]
|
||||
- Text and HTML previews [#12] [#13]
|
||||
- Set up SMTP connection [#4]
|
||||
- Write new email [#8]
|
||||
- Write new email [#8]
|
||||
- Reply, reply all and forward [#9] [#10] [#11]
|
||||
- Download attachments [#14]
|
||||
- Merge `Email` with `Msg` [#21]
|
||||
- List command with pagination [#19]
|
||||
- Icon in table when attachment is present [#16]
|
||||
- Multi-account [#17]
|
||||
- Password from command [#22]
|
||||
- Set up README [#20]
|
||||
- Parse TOML config
|
||||
- Populate Config struct from TOML
|
||||
- Set up IMAP connection
|
||||
- List new emails
|
||||
- Set up CLI arg parser
|
||||
- List mailboxes command
|
||||
- Text and HTML previews
|
||||
- Set up SMTP connection
|
||||
- Write new email
|
||||
- Write new email
|
||||
- Reply, reply all and forward
|
||||
- Download attachments
|
||||
- Merge `Email` with `Msg`
|
||||
- List command with pagination
|
||||
- Icon in table when attachment is present
|
||||
- Multi-account
|
||||
- Password from command
|
||||
- Set up README
|
||||
|
||||
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0-beta...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
|
||||
[0.8.4]: https://github.com/soywod/himalaya/compare/v0.8.3...v0.8.4
|
||||
|
@ -722,145 +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
|
||||
|
||||
[#1]: https://github.com/soywod/himalaya/issues/1
|
||||
[#2]: https://github.com/soywod/himalaya/issues/2
|
||||
[#3]: https://github.com/soywod/himalaya/issues/3
|
||||
[#4]: https://github.com/soywod/himalaya/issues/4
|
||||
[#5]: https://github.com/soywod/himalaya/issues/5
|
||||
[#8]: https://github.com/soywod/himalaya/issues/8
|
||||
[#9]: https://github.com/soywod/himalaya/issues/9
|
||||
[#10]: https://github.com/soywod/himalaya/issues/10
|
||||
[#11]: https://github.com/soywod/himalaya/issues/11
|
||||
[#12]: https://github.com/soywod/himalaya/issues/12
|
||||
[#13]: https://github.com/soywod/himalaya/issues/13
|
||||
[#14]: https://github.com/soywod/himalaya/issues/14
|
||||
[#15]: https://github.com/soywod/himalaya/issues/15
|
||||
[#16]: https://github.com/soywod/himalaya/issues/16
|
||||
[#17]: https://github.com/soywod/himalaya/issues/17
|
||||
[#18]: https://github.com/soywod/himalaya/issues/18
|
||||
[#19]: https://github.com/soywod/himalaya/issues/19
|
||||
[#20]: https://github.com/soywod/himalaya/issues/20
|
||||
[#21]: https://github.com/soywod/himalaya/issues/21
|
||||
[#22]: https://github.com/soywod/himalaya/issues/22
|
||||
[#25]: https://github.com/soywod/himalaya/issues/25
|
||||
[#29]: https://github.com/soywod/himalaya/issues/29
|
||||
[#30]: https://github.com/soywod/himalaya/issues/30
|
||||
[#31]: https://github.com/soywod/himalaya/issues/31
|
||||
[#32]: https://github.com/soywod/himalaya/issues/32
|
||||
[#33]: https://github.com/soywod/himalaya/issues/33
|
||||
[#34]: https://github.com/soywod/himalaya/issues/34
|
||||
[#35]: https://github.com/soywod/himalaya/issues/35
|
||||
[#37]: https://github.com/soywod/himalaya/issues/37
|
||||
[#38]: https://github.com/soywod/himalaya/issues/38
|
||||
[#39]: https://github.com/soywod/himalaya/issues/39
|
||||
[#40]: https://github.com/soywod/himalaya/issues/40
|
||||
[#41]: https://github.com/soywod/himalaya/issues/41
|
||||
[#43]: https://github.com/soywod/himalaya/issues/43
|
||||
[#47]: https://github.com/soywod/himalaya/issues/47
|
||||
[#48]: https://github.com/soywod/himalaya/issues/48
|
||||
[#50]: https://github.com/soywod/himalaya/issues/50
|
||||
[#54]: https://github.com/soywod/himalaya/issues/54
|
||||
[#57]: https://github.com/soywod/himalaya/issues/57
|
||||
[#58]: https://github.com/soywod/himalaya/issues/58
|
||||
[#59]: https://github.com/soywod/himalaya/issues/59
|
||||
[#61]: https://github.com/soywod/himalaya/issues/61
|
||||
[#71]: https://github.com/soywod/himalaya/issues/71
|
||||
[#74]: https://github.com/soywod/himalaya/issues/74
|
||||
[#75]: https://github.com/soywod/himalaya/issues/75
|
||||
[#78]: https://github.com/soywod/himalaya/issues/78
|
||||
[#79]: https://github.com/soywod/himalaya/issues/79
|
||||
[#80]: https://github.com/soywod/himalaya/issues/80
|
||||
[#83]: https://github.com/soywod/himalaya/issues/83
|
||||
[#84]: https://github.com/soywod/himalaya/issues/84
|
||||
[#86]: https://github.com/soywod/himalaya/issues/86
|
||||
[#87]: https://github.com/soywod/himalaya/issues/87
|
||||
[#89]: https://github.com/soywod/himalaya/issues/89
|
||||
[#91]: https://github.com/soywod/himalaya/issues/91
|
||||
[#95]: https://github.com/soywod/himalaya/issues/95
|
||||
[#96]: https://github.com/soywod/himalaya/issues/96
|
||||
[#100]: https://github.com/soywod/himalaya/issues/100
|
||||
[#102]: https://github.com/soywod/himalaya/issues/102
|
||||
[#109]: https://github.com/soywod/himalaya/issues/109
|
||||
[#117]: https://github.com/soywod/himalaya/issues/117
|
||||
[#121]: https://github.com/soywod/himalaya/issues/121
|
||||
[#122]: https://github.com/soywod/himalaya/issues/122
|
||||
[#123]: https://github.com/soywod/himalaya/issues/123
|
||||
[#125]: https://github.com/soywod/himalaya/issues/125
|
||||
[#126]: https://github.com/soywod/himalaya/issues/126
|
||||
[#130]: https://github.com/soywod/himalaya/issues/130
|
||||
[#131]: https://github.com/soywod/himalaya/issues/131
|
||||
[#132]: https://github.com/soywod/himalaya/issues/132
|
||||
[#133]: https://github.com/soywod/himalaya/issues/133
|
||||
[#134]: https://github.com/soywod/himalaya/issues/134
|
||||
[#135]: https://github.com/soywod/himalaya/issues/135
|
||||
[#136]: https://github.com/soywod/himalaya/issues/136
|
||||
[#138]: https://github.com/soywod/himalaya/issues/138
|
||||
[#141]: https://github.com/soywod/himalaya/issues/141
|
||||
[#144]: https://github.com/soywod/himalaya/issues/144
|
||||
[#146]: https://github.com/soywod/himalaya/issues/146
|
||||
[#152]: https://github.com/soywod/himalaya/issues/152
|
||||
[#153]: https://github.com/soywod/himalaya/issues/153
|
||||
[#160]: https://github.com/soywod/himalaya/issues/160
|
||||
[#162]: https://github.com/soywod/himalaya/issues/162
|
||||
[#176]: https://github.com/soywod/himalaya/issues/176
|
||||
[#172]: https://github.com/soywod/himalaya/issues/172
|
||||
[#178]: https://github.com/soywod/himalaya/issues/178
|
||||
[#181]: https://github.com/soywod/himalaya/issues/181
|
||||
[#185]: https://github.com/soywod/himalaya/issues/185
|
||||
[#186]: https://github.com/soywod/himalaya/issues/186
|
||||
[#190]: https://github.com/soywod/himalaya/issues/190
|
||||
[#193]: https://github.com/soywod/himalaya/issues/193
|
||||
[#196]: https://github.com/soywod/himalaya/issues/196
|
||||
[#199]: https://github.com/soywod/himalaya/issues/199
|
||||
[#204]: https://github.com/soywod/himalaya/issues/204
|
||||
[#205]: https://github.com/soywod/himalaya/issues/205
|
||||
[#206]: https://github.com/soywod/himalaya/issues/206
|
||||
[#215]: https://github.com/soywod/himalaya/issues/215
|
||||
[#220]: https://github.com/soywod/himalaya/issues/220
|
||||
[#227]: https://github.com/soywod/himalaya/issues/227
|
||||
[#228]: https://github.com/soywod/himalaya/issues/228
|
||||
[#229]: https://github.com/soywod/himalaya/issues/229
|
||||
[#244]: https://github.com/soywod/himalaya/issues/244
|
||||
[#249]: https://github.com/soywod/himalaya/issues/249
|
||||
[#256]: https://github.com/soywod/himalaya/issues/256
|
||||
[#259]: https://github.com/soywod/himalaya/issues/259
|
||||
[#268]: https://github.com/soywod/himalaya/issues/268
|
||||
[#272]: https://github.com/soywod/himalaya/issues/272
|
||||
[#273]: https://github.com/soywod/himalaya/issues/273
|
||||
[#276]: https://github.com/soywod/himalaya/issues/276
|
||||
[#271]: https://github.com/soywod/himalaya/issues/271
|
||||
[#276]: https://github.com/soywod/himalaya/issues/276
|
||||
[#280]: https://github.com/soywod/himalaya/issues/280
|
||||
[#281]: https://github.com/soywod/himalaya/issues/281
|
||||
[#288]: https://github.com/soywod/himalaya/issues/288
|
||||
[#289]: https://github.com/soywod/himalaya/issues/289
|
||||
[#298]: https://github.com/soywod/himalaya/issues/298
|
||||
[#300]: https://github.com/soywod/himalaya/issues/300
|
||||
[#303]: https://github.com/soywod/himalaya/issues/303
|
||||
[#305]: https://github.com/soywod/himalaya/issues/305
|
||||
[#308]: https://github.com/soywod/himalaya/issues/308
|
||||
[#309]: https://github.com/soywod/himalaya/issues/309
|
||||
[#318]: https://github.com/soywod/himalaya/issues/318
|
||||
[#321]: https://github.com/soywod/himalaya/issues/321
|
||||
[#323]: https://github.com/soywod/himalaya/issues/323
|
||||
[#324]: https://github.com/soywod/himalaya/issues/324
|
||||
[#329]: https://github.com/soywod/himalaya/issues/329
|
||||
[#331]: https://github.com/soywod/himalaya/issues/331
|
||||
[#334]: https://github.com/soywod/himalaya/issues/334
|
||||
[#335]: https://github.com/soywod/himalaya/issues/335
|
||||
[#338]: https://github.com/soywod/himalaya/issues/338
|
||||
[#340]: https://github.com/soywod/himalaya/issues/340
|
||||
[#341]: https://github.com/soywod/himalaya/issues/341
|
||||
[#342]: https://github.com/soywod/himalaya/issues/342
|
||||
[#344]: https://github.com/soywod/himalaya/issues/344
|
||||
[#346]: https://github.com/soywod/himalaya/issues/346
|
||||
[#352]: https://github.com/soywod/himalaya/issues/352
|
||||
[#356]: https://github.com/soywod/himalaya/issues/356
|
||||
[#419]: https://github.com/soywod/himalaya/issues/419
|
||||
[#430]: https://github.com/soywod/himalaya/issues/430
|
||||
[#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
|
||||
|
||||
[sourcehut#43]: https://todo.sr.ht/~soywod/pimalaya/43
|
||||
[sourcehut#54]: https://todo.sr.ht/~soywod/pimalaya/54
|
||||
[sourcehut#59]: https://todo.sr.ht/~soywod/pimalaya/59
|
||||
[sourcehut#60]: https://todo.sr.ht/~soywod/pimalaya/60
|
||||
[sourcehut#95]: https://todo.sr.ht/~soywod/pimalaya/95
|
||||
[core#1]: https://github.com/pimalaya/core/issues/1
|
||||
[core#10]: https://github.com/pimalaya/core/issues/10
|
||||
|
|
|
@ -1,49 +1,69 @@
|
|||
# Himalaya contributing guide
|
||||
# Contributing guide
|
||||
|
||||
Thank you for investing your time in contributing to Himalaya!
|
||||
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 tool:
|
||||
`cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`…
|
||||
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
|
||||
# starts a nix shell (the first launch may take a while)
|
||||
$ nix-shell
|
||||
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
|
||||
|
||||
# builds the CLI
|
||||
$ cargo build
|
||||
|
||||
# runs the CLI
|
||||
$ cargo run -- list
|
||||
```text
|
||||
rustup update
|
||||
```
|
||||
|
||||
## Contributing
|
||||
or install manually the following dependencies:
|
||||
|
||||
If you find a **bug**, please send an email at
|
||||
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
|
||||
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
|
||||
|
||||
If you have a **question**, please send an email at
|
||||
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
|
||||
## Build
|
||||
|
||||
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)
|
||||
using [git send-email](https://git-scm.com/docs/git-send-email) (see
|
||||
[this guide](https://git-send-email.io/) on how to configure it).
|
||||
```text
|
||||
cargo build
|
||||
```
|
||||
|
||||
If you want to **subscribe** to the mailing list, please send an email
|
||||
at
|
||||
[~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.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`.
|
||||
|
||||
If you want to **unsubscribe** to the mailing list, please send an
|
||||
email at
|
||||
[~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht).
|
||||
Finally, you can build a release with `--release`:
|
||||
|
||||
If you 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).
|
||||
```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).
|
||||
|
|
3660
Cargo.lock
generated
3660
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
112
Cargo.toml
112
Cargo.toml
|
@ -1,88 +1,70 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "CLI to manage emails"
|
||||
version = "1.0.0-beta"
|
||||
version = "1.1.0"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||
keywords = ["cli", "mail", "email", "client", "imap"]
|
||||
homepage = "https://pimalaya.org/himalaya/cli/latest/"
|
||||
documentation = "https://pimalaya.org/himalaya/cli/latest/"
|
||||
repository = "https://github.com/soywod/himalaya/"
|
||||
categories = ["command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "maildir", "smtp"]
|
||||
homepage = "https://pimalaya.org/"
|
||||
documentation = "https://github.com/pimalaya/himalaya/"
|
||||
repository = "https://github.com/pimalaya/himalaya/"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"maildir",
|
||||
"imap",
|
||||
# "notmuch",
|
||||
"smtp",
|
||||
"sendmail",
|
||||
# "pgp-commands",
|
||||
# "pgp-gpg",
|
||||
# "pgp-native",
|
||||
]
|
||||
maildir = ["email-lib/maildir"]
|
||||
imap = ["email-lib/imap"]
|
||||
notmuch = ["email-lib/notmuch"]
|
||||
smtp = ["email-lib/smtp"]
|
||||
sendmail = ["email-lib/sendmail"]
|
||||
pgp = []
|
||||
pgp-commands = ["pgp", "mml-lib/pgp-commands", "email-lib/pgp-commands"]
|
||||
pgp-gpg = ["pgp", "mml-lib/pgp-gpg", "email-lib/pgp-gpg"]
|
||||
pgp-native = ["pgp", "mml-lib/pgp-native", "email-lib/pgp-native"]
|
||||
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"]
|
||||
|
||||
[build-dependencies]
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait = "0.1"
|
||||
tempfile = "3.3"
|
||||
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
chrono = "0.4.24"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
ariadne = "0.2"
|
||||
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.4"
|
||||
clap_mangen = "0.2"
|
||||
console = "0.15.2"
|
||||
dialoguer = "0.10.2"
|
||||
dirs = "4.0"
|
||||
email-lib = { version = "=0.19.5", default-features = false }
|
||||
email_address = "0.2.4"
|
||||
env_logger = "0.8"
|
||||
erased-serde = "0.3"
|
||||
indicatif = "0.17"
|
||||
keyring-lib = "=0.3.2"
|
||||
log = "0.4"
|
||||
mail-builder = "0.3"
|
||||
md5 = "0.7.0"
|
||||
mml-lib = { version = "=1.0.6", default-features = false }
|
||||
oauth-lib = "=0.1.0"
|
||||
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 = "=0.3.0"
|
||||
secret-lib = "=0.3.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shellexpand-utils = "=0.2.0"
|
||||
termcolor = "1.1"
|
||||
terminal_size = "0.1"
|
||||
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_json = "1"
|
||||
shellexpand-utils = "=0.2.1"
|
||||
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.7.4"
|
||||
toml_edit = "0.19.8"
|
||||
unicode-width = "0.1"
|
||||
toml = "0.8"
|
||||
tracing = "0.1"
|
||||
url = "2.2"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies.rusqlite]
|
||||
version = "0.29"
|
||||
features = []
|
||||
[patch.crates-io]
|
||||
imap-codec.git = "https://github.com/duesee/imap-codec"
|
||||
|
||||
[target.'cfg(not(target_env = "musl"))'.dependencies.rusqlite]
|
||||
version = "0.29"
|
||||
features = ["bundled"]
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies.coredump]
|
||||
version = "=0.1.2"
|
||||
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"
|
||||
|
|
717
README.md
717
README.md
|
@ -1,95 +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>
|
||||
|
||||
https://pimalaya.org/himalaya/cli/latest/
|
||||
```
|
||||
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
|
||||
```
|
||||
|
||||
CLI to manage emails, based on [email-lib](https://crates.io/crates/email-lib).
|
||||
|
||||

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

|
||||
|
||||
## Features
|
||||
|
||||
- [Mailbox management](https://pimalaya.org/himalaya/cli/latest/usage/folder/)
|
||||
- [Envelopes listing](https://pimalaya.org/himalaya/cli/latest/usage/envelope/list.md)
|
||||
- [Message composition](https://pimalaya.org/himalaya/cli/latest/usage/message/write.md) based on `$EDITOR`
|
||||
- Message manipulation ([copy](https://pimalaya.org/himalaya/cli/latest/usage/message/copy.md)/[move](https://pimalaya.org/himalaya/cli/latest/usage/message/move.md)/[delete](https://pimalaya.org/himalaya/cli/latest/usage/message/delete.md))
|
||||
- [Multi-accounting](https://pimalaya.org/himalaya/cli/latest/configuration/)
|
||||
- [Account synchronization](https://pimalaya.org/himalaya/cli/latest/usage/account/sync.md) for offline usage
|
||||
- Support for [IMAP](https://pimalaya.org/himalaya/cli/latest/configuration/imap.md), [Maildir](https://pimalaya.org/himalaya/cli/latest/configuration/maildir.md), [notmuch](https://pimalaya.org/himalaya/cli/latest/configuration/notmuch.md)
|
||||
- Sending via [SMTP](https://pimalaya.org/himalaya/cli/latest/configuration/smtp.md) or [sendmail](https://pimalaya.org/himalaya/cli/latest/configuration/sendmail.md)
|
||||
- [PGP](https://pimalaya.org/himalaya/cli/latest/configuration/pgp/) end-to-end encryption
|
||||
- Generate [completion scripts](https://pimalaya.org/himalaya/cli/latest/tips/completion.md) for various shells
|
||||
- Generate [man pages](https://pimalaya.org/himalaya/cli/latest/tips/man.md)
|
||||
- JSON output
|
||||
- …and more!
|
||||
- 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/) 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>`.
|
||||
|
||||
If you want to **report a bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
You can also manually edit your own configuration, from scratch:
|
||||
|
||||
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) using [git send-email](https://git-scm.com/docs/git-send-email). Follow [this guide](https://git-send-email.io/) to configure git properly.
|
||||
- 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
|
||||
|
||||
If you just want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.general](https://matrix.to/#/#pimalaya.general:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org). You can also use the mailing list [[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)].
|
||||
<details>
|
||||
<summary>Proton Mail (Bridge)</summary>
|
||||
|
||||
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
|
||||
|
||||
- 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.
|
||||
|
||||
```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 Untrust](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)
|
||||
|
|
|
@ -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,95 +1,651 @@
|
|||
[example]
|
||||
# The current account will be used by default for all other commands.
|
||||
default = true
|
||||
################################################################################
|
||||
###[ Global configuration ]#####################################################
|
||||
################################################################################
|
||||
|
||||
# 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 display name for all accounts. It is used to build the full
|
||||
# email address of an account: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# The signature can be a string or a path to a file.
|
||||
signature = "Regards,"
|
||||
# Default signature for all accounts. The signature is put at the
|
||||
# bottom of all messages. It can be a path or a string. Supports TOML
|
||||
# multilines.
|
||||
#
|
||||
#signature = "/path/to/signature/file"
|
||||
#signature = """
|
||||
# Thanks you,
|
||||
# Regards
|
||||
#"""
|
||||
signature = "Regards,\n"
|
||||
|
||||
# Default signature delimiter for all accounts. It delimits the end of
|
||||
# the message body from the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# 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 = true
|
||||
# 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 = "|| |-||| "
|
||||
|
||||
# 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 NAME column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.name-color = "green"
|
||||
|
||||
# Also define custom folder aliases
|
||||
folder.alias.prev-year = "Archives/2023"
|
||||
# Customizes the color of the BACKENDS column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.backends-color = "blue"
|
||||
|
||||
# Default backend used for all the features like adding folders,
|
||||
# listing envelopes or copying messages.
|
||||
backend = "imap"
|
||||
# Customizes the color of the DEFAULT column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.default-color = "black"
|
||||
|
||||
################################################################################
|
||||
###[ Account configuration ]####################################################
|
||||
################################################################################
|
||||
|
||||
# The account name should be unique.
|
||||
#
|
||||
[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.
|
||||
# Customizes the color of the ID column of the envelope listing table.
|
||||
#
|
||||
envelope.list.table.id-color = "red"
|
||||
|
||||
# Customizes the color of the FLAGS column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.flags-color = "black"
|
||||
|
||||
# Customizes the color of the SUBJECT column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.subject-color = "green"
|
||||
|
||||
# Customizes the color of the SENDER column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.sender-color = "blue"
|
||||
|
||||
# Customizes the color of the DATE column of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.date-color = "yellow"
|
||||
|
||||
|
||||
|
||||
# Defines headers to show at the top of messages when reading them.
|
||||
#
|
||||
message.read.headers = ["From", "To", "Cc", "Subject"]
|
||||
|
||||
# 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
|
||||
|
||||
# IMAP config
|
||||
imap.host = "localhost"
|
||||
imap.port = 3143
|
||||
imap.login = "example@localhost"
|
||||
# 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"
|
||||
|
||||
# Encryption can be either "tls" (or true), "start-tls" or "none" (or false).
|
||||
imap.encryption = "none"
|
||||
# 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"
|
||||
|
||||
# Authentication can be either "passwd" or "oauth2"
|
||||
imap.auth = "passwd"
|
||||
|
||||
# Get password from a raw string (not safe)
|
||||
imap.passwd.raw = "password"
|
||||
|
||||
# Get password from a shell command
|
||||
# imap.passwd.cmd = "echo password"
|
||||
# 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"
|
||||
|
||||
# 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"
|
||||
# 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"
|
||||
|
||||
# Customize at which period, in seconds, the IMAP IDLE mode should refresh.
|
||||
# Defaults to 1740 (29 min), as defined in the RFC.
|
||||
# imap.watch.timeout = 25
|
||||
# Defines 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"
|
||||
|
||||
# SMTP config
|
||||
smtp.host = "localhost"
|
||||
smtp.port = 3025
|
||||
smtp.login = "example@localhost"
|
||||
smtp.encryption = false
|
||||
smtp.auth = "passwd"
|
||||
smtp.passwd.raw = "password"
|
||||
# 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"
|
||||
|
||||
# PGP needs to be enabled with one of those cargo feature:
|
||||
# pgp-commands, pgp-gpg or pgp-native
|
||||
# pgp.backend = "gpg"
|
||||
# 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" ]
|
||||
)
|
||||
|
|
136
flake.lock
generated
136
flake.lock
generated
|
@ -8,127 +8,66 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1704090261,
|
||||
"narHash": "sha256-Vti1mv4WhmXHPNcFgUiJyt4OKLvsvLzM2eKS4bEegf0=",
|
||||
"owner": "nix-community",
|
||||
"lastModified": 1732405626,
|
||||
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"rev": "66fc1883c34c42df188b83272445aedb26bb64b5",
|
||||
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703887061,
|
||||
"narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698420672,
|
||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703992652,
|
||||
"narHash": "sha256-C0o8AUyu8xYgJ36kOxJfXIroy9if/G6aJbNOpA5W0+M=",
|
||||
"lastModified": 1736437047,
|
||||
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "32f63574c85fbc80e4ba1fbb932cde9619bad25e",
|
||||
"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",
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pimalaya": "pimalaya"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1704034202,
|
||||
"narHash": "sha256-OFBXLWm+aIqG+jNAz8BmB+QpepI11SGLtSY6qEs6EmY=",
|
||||
"lastModified": 1732050317,
|
||||
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "cf52c4b2b3367ae7355ef23393e2eae1d37de723",
|
||||
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -137,21 +76,6 @@
|
|||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
151
flake.nix
151
flake.nix
|
@ -2,148 +2,25 @@
|
|||
description = "CLI to manage emails";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
gitignore = {
|
||||
url = "github:hercules-ci/gitignore.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
# FIXME: when #358989 lands on nixos-unstable
|
||||
# https://nixpk.gs/pr-tracker.html?pr=358989
|
||||
nixpkgs.url = "github:nixos/nixpkgs/staging-next";
|
||||
fenix = {
|
||||
url = "github:nix-community/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, flake-utils, gitignore, fenix, naersk, ... }:
|
||||
let
|
||||
inherit (gitignore.lib) gitignoreSource;
|
||||
|
||||
mkToolchain = import ./rust-toolchain.nix fenix;
|
||||
|
||||
mkDevShells = buildPlatform:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
rust-toolchain = mkToolchain.fromFile { system = buildPlatform; };
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
# Nix
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
|
||||
# Rust
|
||||
rust-toolchain
|
||||
|
||||
# Notmuch
|
||||
notmuch
|
||||
|
||||
# GPG
|
||||
gnupg
|
||||
gpgme
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
mkPackage = pkgs: buildPlatform: targetPlatform: package:
|
||||
let
|
||||
toolchain = mkToolchain.fromTarget {
|
||||
inherit pkgs buildPlatform targetPlatform;
|
||||
};
|
||||
naersk' = naersk.lib.${buildPlatform}.override {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
package' = {
|
||||
name = "himalaya";
|
||||
src = gitignoreSource ./.;
|
||||
overrideMain = _: {
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/applications/
|
||||
cp assets/himalaya.desktop $out/share/applications/
|
||||
'';
|
||||
};
|
||||
doCheck = true;
|
||||
cargoTestOptions = opts: opts ++ [ "--lib" ];
|
||||
} // pkgs.lib.optionalAttrs (!isNull targetPlatform) {
|
||||
CARGO_BUILD_TARGET = targetPlatform;
|
||||
} // package;
|
||||
in
|
||||
naersk'.buildPackage package';
|
||||
|
||||
mkPackages = buildPlatform:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
mkPackage' = mkPackage pkgs buildPlatform;
|
||||
in
|
||||
rec {
|
||||
default = if pkgs.stdenv.isDarwin then macos else linux;
|
||||
linux = mkPackage' null { };
|
||||
linux-musl = mkPackage' "x86_64-unknown-linux-musl" (with pkgs.pkgsStatic; {
|
||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||
SQLITE3_STATIC = 1;
|
||||
SQLITE3_LIB_DIR = "${sqlite.out}/lib";
|
||||
hardeningDisable = [ "all" ];
|
||||
});
|
||||
macos = mkPackage' null (with pkgs.darwin.apple_sdk.frameworks; {
|
||||
# NOTE: needed to prevent error Undefined symbols
|
||||
# "_OBJC_CLASS_$_NSImage" and
|
||||
# "_LSCopyApplicationURLsForBundleIdentifier"
|
||||
NIX_LDFLAGS = "-F${AppKit}/Library/Frameworks -framework AppKit";
|
||||
buildInputs = [ Cocoa ];
|
||||
});
|
||||
# FIXME: bzlip: fatal error: windows.h: No such file or directory
|
||||
# May be related to SQLite.
|
||||
windows = mkPackage' "x86_64-pc-windows-gnu" {
|
||||
strictDeps = true;
|
||||
depsBuildBuild = with pkgs.pkgsCross.mingwW64; [
|
||||
stdenv.cc
|
||||
windows.pthreads
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
mkApp = drv: flake-utils.lib.mkApp {
|
||||
inherit drv;
|
||||
name = "himalaya";
|
||||
};
|
||||
|
||||
mkApps = buildPlatform:
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
in
|
||||
rec {
|
||||
default = if pkgs.stdenv.isDarwin then macos else linux;
|
||||
linux = mkApp self.packages.${buildPlatform}.linux;
|
||||
linux-musl = mkApp self.packages.${buildPlatform}.linux-musl;
|
||||
macos = mkApp self.packages.${buildPlatform}.macos;
|
||||
windows =
|
||||
let
|
||||
wine = pkgs.wine.override { wineBuild = "wine64"; };
|
||||
himalaya = self.packages.${buildPlatform}.windows;
|
||||
app = pkgs.writeShellScriptBin "himalaya" ''
|
||||
export WINEPREFIX="$(mktemp -d)"
|
||||
${wine}/bin/wine64 ${himalaya}/bin/himalaya.exe $@
|
||||
'';
|
||||
in
|
||||
mkApp app;
|
||||
};
|
||||
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: {
|
||||
devShells = mkDevShells system;
|
||||
packages = mkPackages system;
|
||||
apps = mkApps system;
|
||||
});
|
||||
outputs =
|
||||
inputs:
|
||||
(import inputs.pimalaya).mkFlakeOutputs inputs {
|
||||
shell = ./shell.nix;
|
||||
default = ./default.nix;
|
||||
};
|
||||
}
|
||||
|
|
40
install.sh
40
install.sh
|
@ -9,25 +9,47 @@ 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:])
|
||||
machine=$(uname -m | tr [:upper:] [:lower:])
|
||||
|
||||
case $system in
|
||||
msys*|mingw*|cygwin*|win*) system=windows; binary=himalaya.exe;;
|
||||
linux|freebsd) system=linux; binary=himalaya;;
|
||||
darwin) system=macos; binary=himalaya;;
|
||||
*) die "Unsupported system: $system" ;;
|
||||
msys*|mingw*|cygwin*|win*)
|
||||
target=x86_64-windows
|
||||
binary=himalaya.exe;;
|
||||
|
||||
linux|freebsd)
|
||||
case $machine in
|
||||
x86_64) target=x86_64-linux;;
|
||||
x86|i386|i686) target=i686-linux;;
|
||||
arm64|aarch64) target=aarch64-linux;;
|
||||
armv6l) target=armv6l-linux;;
|
||||
armv7l) target=armv7l-linux;;
|
||||
*) die "Unsupported machine $machine for system $system";;
|
||||
esac;;
|
||||
|
||||
darwin)
|
||||
case $machine in
|
||||
x86_64) target=x86_64-darwin;;
|
||||
arm64|aarch64) target=aarch64-darwin;;
|
||||
*) die "Unsupported machine $machine for system $system";;
|
||||
esac;;
|
||||
|
||||
*)
|
||||
die "Unsupported system $system";;
|
||||
esac
|
||||
|
||||
tmpdir=$(mktemp -d) || die "Failed to create tmpdir"
|
||||
tmpdir=$(mktemp -d) || die "Cannot create temporary directory"
|
||||
trap "rm -rf $tmpdir" EXIT
|
||||
|
||||
echo "Downloading latest $system release…"
|
||||
curl -sLo "$tmpdir/himalaya.tar.gz" \
|
||||
"$RELEASES_URL/latest/download/himalaya-$system.tar.gz"
|
||||
curl -sLo "$tmpdir/himalaya.tgz" \
|
||||
"$RELEASES_URL/latest/download/himalaya.$target.tgz"
|
||||
|
||||
echo "Installing binary…"
|
||||
tar -xzf "$tmpdir/himalaya.tar.gz" -C "$tmpdir"
|
||||
tar -xzf "$tmpdir/himalaya.tgz" -C "$tmpdir"
|
||||
|
||||
mkdir -p "$PREFIX/bin"
|
||||
cp -f -- "$tmpdir/$binary" "$PREFIX/bin/$binary"
|
||||
|
|
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,27 +0,0 @@
|
|||
fenix:
|
||||
|
||||
let
|
||||
file = ./rust-toolchain.toml;
|
||||
sha256 = "SXRtAuO4IqNOQq+nLbrsDFbVk+3aVA8NNpSZsKlVH/8=";
|
||||
in
|
||||
{
|
||||
fromFile = { system }: fenix.packages.${system}.fromToolchainFile {
|
||||
inherit file sha256;
|
||||
};
|
||||
|
||||
fromTarget = { pkgs, buildPlatform, targetPlatform ? null }:
|
||||
let
|
||||
inherit ((pkgs.lib.importTOML file).toolchain) channel;
|
||||
toolchain = fenix.packages.${buildPlatform};
|
||||
in
|
||||
if
|
||||
isNull targetPlatform
|
||||
then
|
||||
fenix.packages.${buildPlatform}.${channel}.toolchain
|
||||
else
|
||||
toolchain.combine [
|
||||
toolchain.${channel}.rustc
|
||||
toolchain.${channel}.cargo
|
||||
toolchain.targets.${targetPlatform}.${channel}.rust-std
|
||||
];
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
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,116 +1,52 @@
|
|||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpAuthConfig;
|
||||
use log::{debug, info, warn};
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameArg,
|
||||
config::{
|
||||
wizard::{prompt_passwd, prompt_secret},
|
||||
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<()> {
|
||||
#[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;
|
||||
|
||||
info!("executing account configure command");
|
||||
|
||||
let account = &self.account.name;
|
||||
let (_, account_config) = config.into_toml_account_config(Some(account))?;
|
||||
let path = match config_path {
|
||||
Some(path) => path.clone(),
|
||||
None => TomlConfig::default_path()?,
|
||||
};
|
||||
|
||||
if self.reset {
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = account_config.imap {
|
||||
let reset = match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => config.reset().await,
|
||||
ImapAuthConfig::OAuth2(config) => config.reset().await,
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting imap secrets: {err}");
|
||||
debug!("error while resetting imap secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
let account_name = Some(self.account.name.as_str());
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = account_config.smtp {
|
||||
let reset = match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => config.reset().await,
|
||||
SmtpAuthConfig::OAuth2(config) => config.reset().await,
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting smtp secrets: {err}");
|
||||
debug!("error while resetting smtp secrets: {err:?}");
|
||||
}
|
||||
}
|
||||
let account_config = config
|
||||
.accounts
|
||||
.remove(&self.account.name)
|
||||
.unwrap_or_default();
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
if let Some(ref config) = account_config.pgp {
|
||||
config.reset().await?;
|
||||
}
|
||||
}
|
||||
wizard::edit(path, config, account_name, account_config).await?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(ref config) = account_config.imap {
|
||||
match &config.auth {
|
||||
ImapAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt_passwd("IMAP password")).await
|
||||
}
|
||||
ImapAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(ref config) = account_config.smtp {
|
||||
match &config.auth {
|
||||
SmtpAuthConfig::Passwd(config) => {
|
||||
config.configure(|| prompt_passwd("SMTP password")).await
|
||||
}
|
||||
SmtpAuthConfig::OAuth2(config) => {
|
||||
config
|
||||
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(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 anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::Accounts,
|
||||
config::TomlConfig,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Accounts, AccountsTable},
|
||||
terminal::cli::printer::Printer,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
/// List all accounts.
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
/// 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 account list command");
|
||||
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,39 +1,41 @@
|
|||
mod configure;
|
||||
mod doctor;
|
||||
mod list;
|
||||
mod sync;
|
||||
|
||||
use anyhow::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;
|
||||
|
||||
use self::{
|
||||
configure::AccountConfigureCommand, list::AccountListCommand, sync::AccountSyncCommand,
|
||||
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 = "cfg")]
|
||||
Configure(AccountConfigureCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
Doctor(AccountDoctorCommand),
|
||||
List(AccountListCommand),
|
||||
|
||||
#[command(alias = "synchronize", alias = "synchronise")]
|
||||
Sync(AccountSyncCommand),
|
||||
}
|
||||
|
||||
impl AccountSubcommand {
|
||||
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::Configure(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
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,252 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::{ArgAction, Parser};
|
||||
use email::{
|
||||
account::sync::{AccountSyncBuilder, AccountSyncProgressEvent},
|
||||
folder::sync::FolderSyncStrategy,
|
||||
};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
account::arg::name::OptionalAccountNameArg, backend::BackendBuilder, config::TomlConfig,
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
|
||||
});
|
||||
|
||||
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||
ProgressStyle::with_template(
|
||||
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
const 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 account sync command");
|
||||
|
||||
let included_folders = HashSet::from_iter(self.include_folder);
|
||||
let excluded_folders = HashSet::from_iter(self.exclude_folder);
|
||||
|
||||
let strategy = if !included_folders.is_empty() {
|
||||
Some(FolderSyncStrategy::Include(included_folders))
|
||||
} else if !excluded_folders.is_empty() {
|
||||
Some(FolderSyncStrategy::Exclude(excluded_folders))
|
||||
} else if self.all_folders {
|
||||
Some(FolderSyncStrategy::All)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, true)?;
|
||||
let account_name = account_config.name.as_str();
|
||||
|
||||
let backend_builder =
|
||||
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
|
||||
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
|
||||
.await?
|
||||
.with_some_folders_strategy(strategy)
|
||||
.with_dry_run(self.dry_run);
|
||||
|
||||
if self.dry_run {
|
||||
let report = sync_builder.sync().await?;
|
||||
let mut hunks_count = report.folders_patch.len();
|
||||
|
||||
if !report.folders_patch.is_empty() {
|
||||
printer.print_log("Folders patch:")?;
|
||||
for (hunk, _) in report.folders_patch {
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
if !report.emails_patch.is_empty() {
|
||||
printer.print_log("Envelopes patch:")?;
|
||||
for (hunk, _) in report.emails_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("Synchronizing folders…"),
|
||||
);
|
||||
|
||||
// Force the progress bar to show
|
||||
main_progress.set_position(0);
|
||||
|
||||
let report = sync_builder
|
||||
.with_on_progress(move |evt| {
|
||||
use AccountSyncProgressEvent::*;
|
||||
Ok(match evt {
|
||||
ApplyFolderPatches(..) => {
|
||||
main_progress.inc(3);
|
||||
}
|
||||
ApplyEnvelopePatches(patches) => {
|
||||
let mut envelopes_progresses = sub_progresses.lock().unwrap();
|
||||
let patches_len =
|
||||
patches.values().fold(0, |sum, patch| sum + patch.len());
|
||||
main_progress.set_length((110 * patches_len / 100) as u64);
|
||||
main_progress.set_position((5 * patches_len / 100) as u64);
|
||||
main_progress.set_message("Synchronizing envelopes…");
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
ApplyEnvelopeHunk(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}…"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplyEnvelopeCachePatch(_patch) => {
|
||||
main_progress.set_length(100);
|
||||
main_progress.set_position(95);
|
||||
main_progress.set_message("Saving cache database…");
|
||||
}
|
||||
ExpungeFolders(folders) => {
|
||||
let mut progresses = sub_progresses.lock().unwrap();
|
||||
for progress in progresses.values() {
|
||||
progress.finish_and_clear()
|
||||
}
|
||||
progresses.clear();
|
||||
|
||||
main_progress.set_position(100);
|
||||
main_progress
|
||||
.set_message(format!("Expunging {} folders…", folders.len()));
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
})
|
||||
.sync()
|
||||
.await?;
|
||||
|
||||
let folders_patch_err = report
|
||||
.folders_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}")))?;
|
||||
}
|
||||
|
||||
if let Some(err) = report.folders_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occurred while applying the folder cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
let envelopes_patch_err = report
|
||||
.emails_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 folders_patch_err {
|
||||
printer.print_log(format!(" - {hunk}: {err}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = report.emails_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occurred while applying the envelopes cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
printer.print(format!("Account {account_name} successfully synchronized!"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,227 +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 = "smtp")]
|
||||
use email::smtp::config::SmtpConfig;
|
||||
use email::{
|
||||
account::sync::config::SyncConfig, maildir::config::MaildirConfig,
|
||||
sendmail::config::SendmailConfig,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig,
|
||||
folder::config::FolderConfig, message::config::MessageConfig,
|
||||
};
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
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>,
|
||||
|
||||
pub sync: Option<SyncConfig>,
|
||||
#[cfg(feature = "pgp")]
|
||||
pub pgp: Option<PgpConfig>,
|
||||
|
||||
pub folder: Option<FolderConfig>,
|
||||
pub envelope: Option<EnvelopeConfig>,
|
||||
pub flag: Option<FlagConfig>,
|
||||
pub message: Option<MessageConfig>,
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
pub maildir: Option<MaildirConfig>,
|
||||
#[cfg(feature = "imap")]
|
||||
pub imap: Option<ImapConfig>,
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub notmuch: Option<NotmuchConfig>,
|
||||
#[cfg(feature = "smtp")]
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub sendmail: Option<SendmailConfig>,
|
||||
}
|
||||
|
||||
impl TomlAccountConfig {
|
||||
pub fn add_folder_kind(&self) -> Option<&BackendKind> {
|
||||
self.folder
|
||||
.as_ref()
|
||||
.and_then(|folder| folder.add.as_ref())
|
||||
.and_then(|add| add.backend.as_ref())
|
||||
.or_else(|| 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_else(|| 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_else(|| 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_else(|| 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_else(|| 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_else(|| 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_else(|| 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_else(|| 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_else(|| 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_else(|| self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn add_raw_message_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|msg| msg.write.as_ref())
|
||||
.and_then(|add| add.backend.as_ref())
|
||||
.or_else(|| 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_else(|| 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_else(|| 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_else(|| self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn move_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|message| message.move_.as_ref())
|
||||
.and_then(|move_| move_.backend.as_ref())
|
||||
.or_else(|| self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn delete_messages_kind(&self) -> Option<&BackendKind> {
|
||||
self.flag
|
||||
.as_ref()
|
||||
.and_then(|flag| flag.remove.as_ref())
|
||||
.and_then(|remove| remove.backend.as_ref())
|
||||
.or_else(|| self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn send_raw_message_kind(&self) -> Option<&BackendKind> {
|
||||
self.message
|
||||
.as_ref()
|
||||
.and_then(|msg| msg.send.as_ref())
|
||||
.and_then(|send| send.backend.as_ref())
|
||||
.or_else(|| self.backend.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_watch_message_kind(&self) -> Option<&BackendKind> {
|
||||
self.envelope
|
||||
.as_ref()
|
||||
.and_then(|envelope| envelope.watch.as_ref())
|
||||
.and_then(|watch| watch.backend.as_ref())
|
||||
.or_else(|| 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,132 +1,3 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::{collections::hash_map::Iter, fmt, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::table::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use self::config::TomlAccountConfig;
|
||||
|
||||
/// Represents the printable account.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Account {
|
||||
/// Represents the account name.
|
||||
pub name: String,
|
||||
/// Represents the backend name of the account.
|
||||
pub backend: String,
|
||||
/// Represents the default state of the account.
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
backend: backend.into(),
|
||||
default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Account {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Account {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("BACKENDS").bold().underline().white())
|
||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let default = if self.default { "yes" } else { "" };
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).shrinkable().green())
|
||||
.cell(Cell::new(&self.backend).blue())
|
||||
.cell(Cell::new(default).white())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of printable accounts.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Accounts(pub Vec<Account>);
|
||||
|
||||
impl Deref for Accounts {
|
||||
type Target = Vec<Account>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Accounts {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
|
||||
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
|
||||
let mut accounts: Vec<_> = map
|
||||
.map(|(name, account)| {
|
||||
let mut backends = String::new();
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if account.imap.is_some() {
|
||||
backends.push_str("imap");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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,107 +0,0 @@
|
|||
use anyhow::{bail, Result};
|
||||
use dialoguer::{Confirm, Input};
|
||||
use email::account::sync::config::SyncConfig;
|
||||
use email_address::EmailAddress;
|
||||
|
||||
use crate::{
|
||||
backend::{self, config::BackendConfig, BackendKind},
|
||||
config::wizard::THEME,
|
||||
message::config::{MessageConfig, MessageSendConfig},
|
||||
wizard_prompt,
|
||||
};
|
||||
|
||||
use super::TomlAccountConfig;
|
||||
|
||||
pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||
let mut config = TomlAccountConfig::default();
|
||||
|
||||
let account_name = Input::with_theme(&*THEME)
|
||||
.with_prompt("Account name")
|
||||
.default(String::from("Personal"))
|
||||
.interact()?;
|
||||
|
||||
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()?;
|
||||
|
||||
config.display_name = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Full display name")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
config.downloads_dir = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Downloads directory")
|
||||
.default(String::from("~/Downloads"))
|
||||
.interact()?
|
||||
.into(),
|
||||
);
|
||||
|
||||
match backend::wizard::configure(&account_name, &config.email).await? {
|
||||
Some(BackendConfig::Maildir(mdir_config)) => {
|
||||
config.maildir = Some(mdir_config);
|
||||
config.backend = Some(BackendKind::Maildir);
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendConfig::Imap(imap_config)) => {
|
||||
config.imap = Some(imap_config);
|
||||
config.backend = Some(BackendKind::Imap);
|
||||
}
|
||||
#[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, &config.email).await? {
|
||||
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 = "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()
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let should_configure_sync = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Do you need an offline access to your account?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if should_configure_sync {
|
||||
config.sync = Some(SyncConfig {
|
||||
enable: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some((account_name, config)))
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::ImapConfig;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::notmuch::config::NotmuchConfig;
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::SmtpConfig;
|
||||
use email::{maildir::config::MaildirConfig, sendmail::config::SendmailConfig};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum BackendConfig {
|
||||
Maildir(MaildirConfig),
|
||||
#[cfg(feature = "imap")]
|
||||
Imap(ImapConfig),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch(NotmuchConfig),
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp(SmtpConfig),
|
||||
Sendmail(SendmailConfig),
|
||||
}
|
|
@ -1,861 +0,0 @@
|
|||
pub mod config;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::{ImapSessionBuilder, ImapSessionSync};
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::{SmtpClientBuilder, SmtpClientSync};
|
||||
use email::{
|
||||
account::config::AccountConfig,
|
||||
envelope::{
|
||||
get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir},
|
||||
list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir},
|
||||
watch::{imap::WatchImapEnvelopes, maildir::WatchMaildirEnvelopes},
|
||||
Id, SingleId,
|
||||
},
|
||||
flag::{
|
||||
add::{imap::AddFlagsImap, maildir::AddFlagsMaildir},
|
||||
remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir},
|
||||
set::{imap::SetFlagsImap, maildir::SetFlagsMaildir},
|
||||
Flag, Flags,
|
||||
},
|
||||
folder::{
|
||||
add::{imap::AddFolderImap, maildir::AddFolderMaildir},
|
||||
delete::{imap::DeleteFolderImap, maildir::DeleteFolderMaildir},
|
||||
expunge::{imap::ExpungeFolderImap, maildir::ExpungeFolderMaildir},
|
||||
list::{imap::ListFoldersImap, maildir::ListFoldersMaildir},
|
||||
purge::imap::PurgeFolderImap,
|
||||
},
|
||||
maildir::{config::MaildirConfig, MaildirSessionBuilder, MaildirSessionSync},
|
||||
message::{
|
||||
add_raw::imap::AddRawMessageImap,
|
||||
add_raw_with_flags::{
|
||||
imap::AddRawMessageWithFlagsImap, maildir::AddRawMessageWithFlagsMaildir,
|
||||
},
|
||||
copy::{imap::CopyMessagesImap, maildir::CopyMessagesMaildir},
|
||||
get::imap::GetMessagesImap,
|
||||
move_::{imap::MoveMessagesImap, maildir::MoveMessagesMaildir},
|
||||
peek::{imap::PeekMessagesImap, maildir::PeekMessagesMaildir},
|
||||
send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp},
|
||||
Messages,
|
||||
},
|
||||
sendmail::SendmailContext,
|
||||
};
|
||||
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 {
|
||||
Maildir,
|
||||
#[serde(skip_deserializing)]
|
||||
MaildirForSync,
|
||||
#[cfg(feature = "imap")]
|
||||
Imap,
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch,
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp,
|
||||
Sendmail,
|
||||
}
|
||||
|
||||
impl ToString for BackendKind {
|
||||
fn to_string(&self) -> String {
|
||||
let kind = match self {
|
||||
Self::Maildir => "Maildir",
|
||||
Self::MaildirForSync => "Maildir",
|
||||
#[cfg(feature = "imap")]
|
||||
Self::Imap => "IMAP",
|
||||
#[cfg(feature = "notmuch")]
|
||||
Self::Notmuch => "Notmuch",
|
||||
#[cfg(feature = "smtp")]
|
||||
Self::Smtp => "SMTP",
|
||||
Self::Sendmail => "Sendmail",
|
||||
};
|
||||
|
||||
kind.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BackendContextBuilder {
|
||||
maildir: Option<MaildirSessionBuilder>,
|
||||
maildir_for_sync: Option<MaildirSessionBuilder>,
|
||||
#[cfg(feature = "imap")]
|
||||
imap: Option<ImapSessionBuilder>,
|
||||
#[cfg(feature = "smtp")]
|
||||
smtp: Option<SmtpClientBuilder>,
|
||||
sendmail: Option<SendmailContext>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl email::backend::BackendContextBuilder for BackendContextBuilder {
|
||||
type Context = BackendContext;
|
||||
|
||||
async fn build(self) -> Result<Self::Context> {
|
||||
let mut ctx = BackendContext::default();
|
||||
|
||||
if let Some(maildir) = self.maildir {
|
||||
ctx.maildir = Some(maildir.build().await?);
|
||||
}
|
||||
|
||||
if let Some(maildir) = self.maildir_for_sync {
|
||||
ctx.maildir_for_sync = Some(maildir.build().await?);
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap) = self.imap {
|
||||
ctx.imap = Some(imap.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?);
|
||||
}
|
||||
|
||||
if let Some(sendmail) = self.sendmail {
|
||||
ctx.sendmail = Some(sendmail.build().await?);
|
||||
}
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BackendContext {
|
||||
pub maildir: Option<MaildirSessionSync>,
|
||||
pub maildir_for_sync: Option<MaildirSessionSync>,
|
||||
#[cfg(feature = "imap")]
|
||||
pub imap: Option<ImapSessionSync>,
|
||||
#[cfg(feature = "smtp")]
|
||||
pub smtp: Option<SmtpClientSync>,
|
||||
pub sendmail: Option<SendmailContext>,
|
||||
}
|
||||
|
||||
pub struct BackendBuilder {
|
||||
toml_account_config: TomlAccountConfig,
|
||||
builder: email::backend::BackendBuilder<BackendContextBuilder>,
|
||||
}
|
||||
|
||||
impl BackendBuilder {
|
||||
pub async fn new(
|
||||
toml_account_config: TomlAccountConfig,
|
||||
account_config: AccountConfig,
|
||||
with_sending: bool,
|
||||
) -> Result<Self> {
|
||||
let used_backends = toml_account_config.get_used_backends();
|
||||
|
||||
let is_maildir_used = used_backends.contains(&BackendKind::Maildir);
|
||||
let is_maildir_for_sync_used = used_backends.contains(&BackendKind::MaildirForSync);
|
||||
#[cfg(feature = "imap")]
|
||||
let is_imap_used = used_backends.contains(&BackendKind::Imap);
|
||||
#[cfg(feature = "notmuch")]
|
||||
let is_notmuch_used = used_backends.contains(&BackendKind::Notmuch);
|
||||
#[cfg(feature = "smtp")]
|
||||
let is_smtp_used = used_backends.contains(&BackendKind::Smtp);
|
||||
let is_sendmail_used = used_backends.contains(&BackendKind::Sendmail);
|
||||
|
||||
let backend_ctx_builder = BackendContextBuilder {
|
||||
maildir: toml_account_config
|
||||
.maildir
|
||||
.as_ref()
|
||||
.filter(|_| is_maildir_used)
|
||||
.map(|mdir_config| {
|
||||
MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone())
|
||||
}),
|
||||
maildir_for_sync: Some(MaildirConfig {
|
||||
root_dir: account_config.get_sync_dir()?,
|
||||
})
|
||||
.filter(|_| is_maildir_for_sync_used)
|
||||
.map(|mdir_config| MaildirSessionBuilder::new(account_config.clone(), mdir_config)),
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
imap: {
|
||||
let ctx_builder = toml_account_config
|
||||
.imap
|
||||
.as_ref()
|
||||
.filter(|_| is_imap_used)
|
||||
.map(|imap_config| {
|
||||
ImapSessionBuilder::new(account_config.clone(), imap_config.clone())
|
||||
.with_prebuilt_credentials()
|
||||
});
|
||||
|
||||
match ctx_builder {
|
||||
Some(ctx_builder) => Some(ctx_builder.await?),
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "notmuch")]
|
||||
notmuch: toml_account_config
|
||||
.notmuch
|
||||
.as_ref()
|
||||
.filter(|_| is_notmuch_used)
|
||||
.map(|notmuch_config| {
|
||||
NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone())
|
||||
}),
|
||||
#[cfg(feature = "smtp")]
|
||||
smtp: toml_account_config
|
||||
.smtp
|
||||
.as_ref()
|
||||
.filter(|_| with_sending)
|
||||
.filter(|_| is_smtp_used)
|
||||
.map(|smtp_config| {
|
||||
SmtpClientBuilder::new(account_config.clone(), smtp_config.clone())
|
||||
}),
|
||||
sendmail: toml_account_config
|
||||
.sendmail
|
||||
.as_ref()
|
||||
.filter(|_| with_sending)
|
||||
.filter(|_| is_sendmail_used)
|
||||
.map(|sendmail_config| {
|
||||
SendmailContext::new(account_config.clone(), sendmail_config.clone())
|
||||
}),
|
||||
};
|
||||
|
||||
let mut backend_builder =
|
||||
email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder);
|
||||
|
||||
match toml_account_config.add_folder_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_folder(|ctx| ctx.maildir.as_ref().and_then(AddFolderMaildir::new));
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_add_folder(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(AddFolderMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_folder(|ctx| ctx.imap.as_ref().and_then(AddFolderImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_folder(|ctx| ctx.notmuch.as_ref().and_then(AddFolderNotmuch::new));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.list_folders_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_list_folders(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(ListFoldersMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_list_folders(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(ListFoldersMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_list_folders(|ctx| ctx.imap.as_ref().and_then(ListFoldersImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_list_folders(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(ListFoldersNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.expunge_folder_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_expunge_folder(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(ExpungeFolderMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_expunge_folder(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(ExpungeFolderMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_expunge_folder(|ctx| ctx.imap.as_ref().and_then(ExpungeFolderImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_expunge_folder(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(ExpungeFolderNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.purge_folder_kind() {
|
||||
// TODO
|
||||
// Some(BackendKind::Maildir) => {
|
||||
// backend_builder = backend_builder
|
||||
// .with_purge_folder(|ctx| ctx.maildir.as_ref().and_then(PurgeFolderMaildir::new));
|
||||
// }
|
||||
// TODO
|
||||
// Some(BackendKind::MaildirForSync) => {
|
||||
// backend_builder = backend_builder
|
||||
// .with_purge_folder(|ctx| ctx.maildir_for_sync.as_ref().and_then(PurgeFolderMaildir::new));
|
||||
// }
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_purge_folder(|ctx| ctx.imap.as_ref().and_then(PurgeFolderImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_purge_folder(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(PurgeFolderNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.delete_folder_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_delete_folder(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(DeleteFolderMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_delete_folder(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(DeleteFolderMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_delete_folder(|ctx| ctx.imap.as_ref().and_then(DeleteFolderImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_delete_folder(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(DeleteFolderNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.backend {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_watch_envelopes(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(WatchMaildirEnvelopes::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_watch_envelopes(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(WatchMaildirEnvelopes::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder.with_watch_envelopes(|ctx| {
|
||||
ctx.imap.as_ref().and_then(WatchImapEnvelopes::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_watch_envelopes(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(WatchNotmuchEnvelopes::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.get_envelope_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_get_envelope(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(GetEnvelopeMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_get_envelope(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(GetEnvelopeMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_get_envelope(|ctx| ctx.imap.as_ref().and_then(GetEnvelopeImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_get_envelope(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(GetEnvelopeNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.list_envelopes_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_list_envelopes(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(ListEnvelopesMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_list_envelopes(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(ListEnvelopesMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_list_envelopes(|ctx| ctx.imap.as_ref().and_then(ListEnvelopesImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_list_envelopes(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(ListEnvelopesNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.add_flags_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_flags(|ctx| ctx.maildir.as_ref().and_then(AddFlagsMaildir::new));
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_add_flags(|ctx| {
|
||||
ctx.maildir_for_sync.as_ref().and_then(AddFlagsMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_flags(|ctx| ctx.imap.as_ref().and_then(AddFlagsImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_flags(|ctx| ctx.notmuch.as_ref().and_then(AddFlagsNotmuch::new));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.set_flags_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder
|
||||
.with_set_flags(|ctx| ctx.maildir.as_ref().and_then(SetFlagsMaildir::new));
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_set_flags(|ctx| {
|
||||
ctx.maildir_for_sync.as_ref().and_then(SetFlagsMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_set_flags(|ctx| ctx.imap.as_ref().and_then(SetFlagsImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder
|
||||
.with_set_flags(|ctx| ctx.notmuch.as_ref().and_then(SetFlagsNotmuch::new));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.remove_flags_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_remove_flags(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(RemoveFlagsMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_remove_flags(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(RemoveFlagsMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_remove_flags(|ctx| ctx.imap.as_ref().and_then(RemoveFlagsImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_remove_flags(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(RemoveFlagsNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.send_raw_message_kind() {
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(BackendKind::Smtp) => {
|
||||
backend_builder = backend_builder.with_send_raw_message(|ctx| {
|
||||
ctx.smtp.as_ref().and_then(SendRawMessageSmtp::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::Sendmail) => {
|
||||
backend_builder = backend_builder.with_send_raw_message(|ctx| {
|
||||
ctx.sendmail.as_ref().and_then(SendRawMessageSendmail::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.add_raw_message_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| {
|
||||
ctx.maildir
|
||||
.as_ref()
|
||||
.and_then(AddRawMessageWithFlagsMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(AddRawMessageWithFlagsMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_add_raw_message(|ctx| ctx.imap.as_ref().and_then(AddRawMessageImap::new))
|
||||
.with_add_raw_message_with_flags(|ctx| {
|
||||
ctx.imap.as_ref().and_then(AddRawMessageWithFlagsImap::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_add_raw_message(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(AddRawMessageNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.peek_messages_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_peek_messages(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(PeekMessagesMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_peek_messages(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(PeekMessagesMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_peek_messages(|ctx| ctx.imap.as_ref().and_then(PeekMessagesImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_peek_messages(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(PeekMessagesNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.get_messages_kind() {
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_get_messages(|ctx| ctx.imap.as_ref().and_then(GetMessagesImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_get_messages(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(GetMessagesNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.copy_messages_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_copy_messages(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(CopyMessagesMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_copy_messages(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(CopyMessagesMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_copy_messages(|ctx| ctx.imap.as_ref().and_then(CopyMessagesImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_copy_messages(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(CopyMessagesNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match toml_account_config.move_messages_kind() {
|
||||
Some(BackendKind::Maildir) => {
|
||||
backend_builder = backend_builder.with_move_messages(|ctx| {
|
||||
ctx.maildir.as_ref().and_then(MoveMessagesMaildir::new)
|
||||
});
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
backend_builder = backend_builder.with_move_messages(|ctx| {
|
||||
ctx.maildir_for_sync
|
||||
.as_ref()
|
||||
.and_then(MoveMessagesMaildir::new)
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
Some(BackendKind::Imap) => {
|
||||
backend_builder = backend_builder
|
||||
.with_move_messages(|ctx| ctx.imap.as_ref().and_then(MoveMessagesImap::new));
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
backend_builder = backend_builder.with_move_messages(|ctx| {
|
||||
ctx.notmuch.as_ref().and_then(MoveMessagesNotmuch::new)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
toml_account_config,
|
||||
builder: backend_builder,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn build(self) -> Result<Backend> {
|
||||
Ok(Backend {
|
||||
toml_account_config: self.toml_account_config,
|
||||
backend: self.builder.build().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for BackendBuilder {
|
||||
type Target = email::backend::BackendBuilder<BackendContextBuilder>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.builder
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<email::backend::BackendBuilder<BackendContextBuilder>> for BackendBuilder {
|
||||
fn into(self) -> email::backend::BackendBuilder<BackendContextBuilder> {
|
||||
self.builder
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Backend {
|
||||
toml_account_config: TomlAccountConfig,
|
||||
backend: email::backend::Backend<BackendContext>,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub async fn new(
|
||||
toml_account_config: TomlAccountConfig,
|
||||
account_config: AccountConfig,
|
||||
with_sending: bool,
|
||||
) -> Result<Self> {
|
||||
BackendBuilder::new(toml_account_config, account_config, with_sending)
|
||||
.await?
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
fn build_id_mapper(
|
||||
&self,
|
||||
folder: &str,
|
||||
backend_kind: Option<&BackendKind>,
|
||||
) -> Result<IdMapper> {
|
||||
let mut id_mapper = IdMapper::Dummy;
|
||||
|
||||
match backend_kind {
|
||||
Some(BackendKind::Maildir) => {
|
||||
if let Some(mdir_config) = &self.toml_account_config.maildir {
|
||||
id_mapper = IdMapper::new(
|
||||
&self.backend.account_config,
|
||||
folder,
|
||||
mdir_config.root_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Some(BackendKind::MaildirForSync) => {
|
||||
id_mapper = IdMapper::new(
|
||||
&self.backend.account_config,
|
||||
folder,
|
||||
self.backend.account_config.get_sync_dir()?,
|
||||
)?;
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
Some(BackendKind::Notmuch) => {
|
||||
if let Some(notmuch_config) = &self.toml_account_config.notmuch {
|
||||
id_mapper = IdMapper::new(
|
||||
&self.backend.account_config,
|
||||
folder,
|
||||
mdir_config.root_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
Ok(id_mapper)
|
||||
}
|
||||
|
||||
pub async fn list_envelopes(
|
||||
&self,
|
||||
folder: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Envelopes> {
|
||||
let backend_kind = self.toml_account_config.list_envelopes_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let envelopes = self.backend.list_envelopes(folder, page_size, page).await?;
|
||||
let envelopes = Envelopes::from_backend(&self.account_config, &id_mapper, envelopes)?;
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.add_flags(folder, &ids, flags).await
|
||||
}
|
||||
|
||||
pub async fn set_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.set_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.set_flags(folder, &ids, flags).await
|
||||
}
|
||||
|
||||
pub async fn remove_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.remove_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.remove_flags(folder, &ids, flags).await
|
||||
}
|
||||
|
||||
pub async fn peek_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
|
||||
let backend_kind = self.toml_account_config.get_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.peek_messages(folder, &ids).await
|
||||
}
|
||||
|
||||
pub async fn get_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
|
||||
let backend_kind = self.toml_account_config.get_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.get_messages(folder, &ids).await
|
||||
}
|
||||
|
||||
pub async fn copy_messages(
|
||||
&self,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: &[usize],
|
||||
) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.move_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend
|
||||
.copy_messages(from_folder, to_folder, &ids)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn move_messages(
|
||||
&self,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: &[usize],
|
||||
) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.move_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend
|
||||
.move_messages(from_folder, to_folder, &ids)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_messages(&self, folder: &str, ids: &[usize]) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.delete_messages_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.delete_messages(folder, &ids).await
|
||||
}
|
||||
|
||||
pub async fn add_raw_message(&self, folder: &str, email: &[u8]) -> Result<SingleId> {
|
||||
let backend_kind = self.toml_account_config.add_raw_message_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let id = self.backend.add_raw_message(folder, email).await?;
|
||||
id_mapper.create_alias(&*id)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn add_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.add_flag(folder, &ids, flag).await
|
||||
}
|
||||
|
||||
pub async fn set_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||
let backend_kind = self.toml_account_config.set_flags_kind();
|
||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||
self.backend.set_flag(folder, &ids, flag).await
|
||||
}
|
||||
|
||||
pub async fn remove_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
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Backend {
|
||||
type Target = email::backend::Backend<BackendContext>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.backend
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Select;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use crate::imap;
|
||||
#[cfg(feature = "notmuch")]
|
||||
use crate::notmuch;
|
||||
#[cfg(feature = "smtp")]
|
||||
use crate::smtp;
|
||||
use crate::{config::wizard::THEME, maildir, sendmail};
|
||||
|
||||
use super::{config::BackendConfig, BackendKind};
|
||||
|
||||
const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
|
||||
#[cfg(feature = "imap")]
|
||||
BackendKind::Imap,
|
||||
BackendKind::Maildir,
|
||||
#[cfg(feature = "notmuch")]
|
||||
BackendKind::Notmuch,
|
||||
];
|
||||
|
||||
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
|
||||
#[cfg(feature = "smtp")]
|
||||
BackendKind::Smtp,
|
||||
BackendKind::Sendmail,
|
||||
];
|
||||
|
||||
pub(crate) async fn configure(account_name: &str, email: &str) -> 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 {
|
||||
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
|
||||
#[cfg(feature = "imap")]
|
||||
Some(kind) if kind == BackendKind::Imap => {
|
||||
Some(imap::wizard::configure(account_name, email).await?)
|
||||
}
|
||||
#[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,
|
||||
) -> 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 {
|
||||
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
|
||||
#[cfg(feature = "smtp")]
|
||||
Some(kind) if kind == BackendKind::Smtp => {
|
||||
Some(smtp::wizard::configure(account_name, email).await?)
|
||||
}
|
||||
_ => 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;
|
29
src/cache/args.rs
vendored
29
src/cache/args.rs
vendored
|
@ -1,29 +0,0 @@
|
|||
//! This module provides arguments related to the cache.
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches};
|
||||
|
||||
const ARG_DISABLE_CACHE: &str = "disable-cache";
|
||||
|
||||
/// Represents the disable cache flag argument. This argument allows
|
||||
/// the user to disable any sort of cache.
|
||||
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||
[Arg::new(ARG_DISABLE_CACHE)
|
||||
.help("Disable any sort of cache")
|
||||
.long_help(
|
||||
"Disable any sort of cache.
|
||||
|
||||
The action depends on commands it apply on. For example, when listing
|
||||
envelopes using the IMAP backend, this flag will ensure that envelopes
|
||||
are fetched from the IMAP server and not from the synchronized local
|
||||
Maildir.",
|
||||
)
|
||||
.long("disable-cache")
|
||||
.alias("no-cache")
|
||||
.global(true)
|
||||
.action(ArgAction::SetTrue)]
|
||||
}
|
||||
|
||||
/// Represents the disable cache flag parser.
|
||||
pub fn parse_disable_cache_arg(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DISABLE_CACHE)
|
||||
}
|
169
src/cache/mod.rs
vendored
169
src/cache/mod.rs
vendored
|
@ -1,169 +0,0 @@
|
|||
pub mod arg;
|
||||
pub mod args;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use email::account::config::AccountConfig;
|
||||
use log::{debug, trace};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IdMapper {
|
||||
Dummy,
|
||||
Mapper(String, rusqlite::Connection),
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
|
||||
let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||
let mut db_parent_dir = dir.as_ref().parent();
|
||||
|
||||
while !db_path.is_file() {
|
||||
match db_parent_dir {
|
||||
Some(dir) => {
|
||||
db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
|
||||
db_parent_dir = dir.parent();
|
||||
}
|
||||
None => {
|
||||
db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db_path
|
||||
}
|
||||
|
||||
pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result<Self> {
|
||||
let folder = account_config.get_folder_alias(folder);
|
||||
let digest = md5::compute(account_config.name.clone() + &folder);
|
||||
let table = format!("id_mapper_{digest:x}");
|
||||
debug!("creating id mapper table {table} at {db_path:?}…");
|
||||
|
||||
let db_path = Self::find_closest_db_path(db_path);
|
||||
let conn = rusqlite::Connection::open(&db_path)
|
||||
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
|
||||
|
||||
let query = format!(
|
||||
"CREATE TABLE IF NOT EXISTS {table} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
internal_id TEXT UNIQUE
|
||||
)",
|
||||
);
|
||||
trace!("create table query: {query:#?}");
|
||||
|
||||
conn.execute(&query, [])
|
||||
.context("cannot create id mapper table")?;
|
||||
|
||||
Ok(Self::Mapper(table, 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(table, conn) => {
|
||||
debug!("creating alias for id {id}…");
|
||||
|
||||
let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
|
||||
trace!("insert query: {query:#?}");
|
||||
|
||||
conn.execute(&query, [id])
|
||||
.with_context(|| format!("cannot create id alias for id {id}"))?;
|
||||
|
||||
let alias = conn.last_insert_rowid().to_string();
|
||||
debug!("created 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(table, conn) => {
|
||||
debug!("getting alias for id {id}…");
|
||||
|
||||
let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
|
||||
trace!("select query: {query:#?}");
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
let aliases: Vec<i64> = stmt
|
||||
.query_map([id], |row| row.get(0))
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?
|
||||
.collect::<rusqlite::Result<_>>()
|
||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||
let alias = match aliases.first() {
|
||||
Some(alias) => {
|
||||
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();
|
||||
let alias = alias
|
||||
.parse::<i64>()
|
||||
.context(format!("cannot parse id mapper alias {alias}"))?;
|
||||
|
||||
match self {
|
||||
Self::Dummy => Ok(alias.to_string()),
|
||||
Self::Mapper(table, conn) => {
|
||||
debug!("getting id from alias {alias}…");
|
||||
|
||||
let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
|
||||
trace!("select query: {query:#?}");
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||
let ids: Vec<String> = stmt
|
||||
.query_map([alias], |row| row.get(0))
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?
|
||||
.collect::<rusqlite::Result<_>>()
|
||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||
let id = ids
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
|
||||
.to_owned();
|
||||
debug!("found id {id} from alias {alias}");
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
|
||||
where
|
||||
A: ToString,
|
||||
I: IntoIterator<Item = A>,
|
||||
{
|
||||
aliases
|
||||
.into_iter()
|
||||
.map(|alias| self.get_id(alias))
|
||||
.collect()
|
||||
}
|
||||
}
|
101
src/cli.rs
101
src/cli.rs
|
@ -1,11 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
long_version,
|
||||
terminal::{
|
||||
cli::{
|
||||
arg::path_parser,
|
||||
printer::{OutputFmt, Printer},
|
||||
},
|
||||
config::TomlConfig as _,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
account::command::AccountSubcommand,
|
||||
completion::command::CompletionGenerateCommand,
|
||||
config::{self, TomlConfig},
|
||||
config::TomlConfig,
|
||||
envelope::command::EnvelopeSubcommand,
|
||||
flag::command::FlagSubcommand,
|
||||
folder::command::FolderSubcommand,
|
||||
|
@ -14,28 +25,30 @@ 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)]
|
||||
pub command: HimalayaCommand,
|
||||
pub command: Option<HimalayaCommand>,
|
||||
|
||||
/// Override the default configuration file path
|
||||
/// Override the default configuration file path.
|
||||
///
|
||||
/// The given path is shell-expanded then canonicalized (if
|
||||
/// applicable). If the path does not point to a valid file, the
|
||||
/// wizard will propose to assist you in the creation of the
|
||||
/// configuration file.
|
||||
#[arg(short, long = "config", global = true)]
|
||||
#[arg(value_name = "PATH", value_parser = config::path_parser)]
|
||||
pub config_path: Option<PathBuf>,
|
||||
/// The given paths are shell-expanded then canonicalized (if
|
||||
/// applicable). If the first path does not point to a valid file,
|
||||
/// the wizard will propose to assist you in the creation of the
|
||||
/// configuration file. Other paths are merged with the first one,
|
||||
/// which allows you to separate your public config from your
|
||||
/// private(s) one(s).
|
||||
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
|
||||
#[arg(value_name = "PATH", value_parser = path_parser)]
|
||||
pub config_paths: Vec<PathBuf>,
|
||||
|
||||
/// Customize the output format
|
||||
/// Customize the output format.
|
||||
///
|
||||
/// The output format determine how to display commands output to
|
||||
/// the terminal.
|
||||
|
@ -50,29 +63,19 @@ pub struct Cli {
|
|||
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
|
||||
pub output: OutputFmt,
|
||||
|
||||
/// Control when to use colors
|
||||
/// Enable logs with spantrace.
|
||||
///
|
||||
/// The default setting is 'auto', which means himalaya will try
|
||||
/// to guess when to use colors. For example, if himalaya is
|
||||
/// printing to a terminal, then it will use colors, but if it is
|
||||
/// redirected to a file or a pipe, then it will suppress color
|
||||
/// output. himalaya will suppress color output in some other
|
||||
/// circumstances as well. For example, if the TERM environment
|
||||
/// variable is not set or set to 'dumb', then himalaya will not
|
||||
/// use colors.
|
||||
/// This is the same as running the command with `RUST_LOG=debug`
|
||||
/// environment variable.
|
||||
#[arg(long, global = true, conflicts_with = "trace")]
|
||||
pub debug: bool,
|
||||
|
||||
/// Enable verbose logs with backtrace.
|
||||
///
|
||||
/// 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,
|
||||
/// This is the same as running the command with `RUST_LOG=trace`
|
||||
/// and `RUST_BACKTRACE=1` environment variables.
|
||||
#[arg(long, global = true, conflicts_with = "debug")]
|
||||
pub trace: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
|
@ -116,42 +119,38 @@ pub enum HimalayaCommand {
|
|||
}
|
||||
|
||||
impl HimalayaCommand {
|
||||
pub async fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
|
||||
match self {
|
||||
Self::Account(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, config, config_paths.first()).await
|
||||
}
|
||||
Self::Folder(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Envelope(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Flag(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Message(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Attachment(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Template(cmd) => {
|
||||
let config = TomlConfig::from_some_path_or_default(config_path).await?;
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Manual(cmd) => cmd.execute(printer).await,
|
||||
Self::Completion(cmd) => cmd.execute(printer).await,
|
||||
Self::Completion(cmd) => cmd.execute().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use anyhow::Result;
|
||||
use clap::{value_parser, CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
use log::info;
|
||||
use std::io;
|
||||
|
||||
use crate::{cli::Cli, printer::Printer};
|
||||
use clap::{value_parser, CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
use color_eyre::Result;
|
||||
use tracing::info;
|
||||
|
||||
/// Print completion script for a shell to stdout.
|
||||
use crate::cli::Cli;
|
||||
|
||||
/// 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
|
||||
|
@ -19,18 +20,13 @@ pub struct CompletionGenerateCommand {
|
|||
}
|
||||
|
||||
impl CompletionGenerateCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer) -> Result<()> {
|
||||
info!("executing completion generate command");
|
||||
pub async fn execute(self) -> Result<()> {
|
||||
info!("executing generate completion command");
|
||||
|
||||
let mut cmd = Cli::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout());
|
||||
|
||||
printer.print(format!(
|
||||
"Shell script successfully generated for shell {}!",
|
||||
self.shell
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
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,26 +0,0 @@
|
|||
//! This module provides arguments related to the user config.
|
||||
|
||||
use clap::{Arg, ArgMatches};
|
||||
|
||||
const ARG_CONFIG: &str = "config";
|
||||
|
||||
/// Represents the config file path argument. This argument allows the
|
||||
/// user to customize the config file path.
|
||||
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||
[Arg::new(ARG_CONFIG)
|
||||
.help("Override the configuration file path")
|
||||
.long_help(
|
||||
"Override the configuration file path
|
||||
|
||||
If the file under the given path does not exist, the wizard will propose to create it.",
|
||||
)
|
||||
.long("config")
|
||||
.short('c')
|
||||
.global(true)
|
||||
.value_name("path")]
|
||||
}
|
||||
|
||||
/// Represents the config file path argument parser.
|
||||
pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod wizard;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dialoguer::Confirm;
|
||||
use dirs::{config_dir, home_dir};
|
||||
use email::{
|
||||
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
|
||||
folder::config::FolderConfig, message::config::MessageConfig,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shellexpand_utils::{canonicalize, expand};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
use toml;
|
||||
|
||||
use crate::{account::config::TomlAccountConfig, backend::BackendKind, wizard_prompt, wizard_warn};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TomlConfig {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, TomlAccountConfig>,
|
||||
}
|
||||
|
||||
impl TomlConfig {
|
||||
/// Read and parse the TOML configuration at the given path.
|
||||
///
|
||||
/// Returns an error if the configuration file cannot be read or
|
||||
/// if its content cannot be parsed.
|
||||
fn from_path(path: &Path) -> Result<Self> {
|
||||
let content =
|
||||
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
|
||||
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
|
||||
}
|
||||
|
||||
/// Create and save a TOML configuration using the wizard.
|
||||
///
|
||||
/// If the user accepts the confirmation, the wizard starts and
|
||||
/// help him to create his configuration file. Otherwise the
|
||||
/// program stops.
|
||||
///
|
||||
/// NOTE: the wizard can only be used with interactive shells.
|
||||
async fn from_wizard(path: PathBuf) -> Result<Self> {
|
||||
wizard_warn!("Cannot find existing configuration at {path:?}.");
|
||||
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to create one with the wizard?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if !confirm {
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
wizard::configure(path).await
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration from default paths.
|
||||
pub async fn from_default_paths() -> Result<Self> {
|
||||
match Self::first_valid_default_path() {
|
||||
Some(path) => Self::from_path(&path),
|
||||
None => Self::from_wizard(Self::default_path()?).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and parse the TOML configuration at the optional given
|
||||
/// path.
|
||||
///
|
||||
/// If the given path exists, then read and parse the TOML
|
||||
/// configuration from it.
|
||||
///
|
||||
/// If the given path does not exist, then create it using the
|
||||
/// wizard.
|
||||
///
|
||||
/// If no path is given, then either read and parse the TOML
|
||||
/// configuration at the first valid default path, otherwise
|
||||
/// create it using the wizard. wizard.
|
||||
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
|
||||
match path.map(Into::into) {
|
||||
Some(ref path) if path.exists() => Self::from_path(path),
|
||||
Some(path) => Self::from_wizard(path).await,
|
||||
None => Self::from_default_paths().await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default configuration path.
|
||||
///
|
||||
/// Returns an error if the XDG configuration directory cannot be
|
||||
/// found.
|
||||
pub fn default_path() -> Result<PathBuf> {
|
||||
Ok(config_dir()
|
||||
.ok_or(anyhow!("cannot get XDG config directory"))?
|
||||
.join("himalaya")
|
||||
.join("config.toml"))
|
||||
}
|
||||
|
||||
/// Get the first default configuration path that points to a
|
||||
/// valid file.
|
||||
///
|
||||
/// Tries paths in this order:
|
||||
///
|
||||
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
|
||||
/// `$XDG_CONFIG_DIR` in other OSes.)
|
||||
/// - `$HOME/.config/himalaya/config.toml`
|
||||
/// - `$HOME/.himalayarc`
|
||||
pub fn first_valid_default_path() -> Option<PathBuf> {
|
||||
Self::default_path()
|
||||
.ok()
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
||||
.filter(|p| p.exists())
|
||||
}
|
||||
|
||||
pub fn into_toml_account_config(
|
||||
&self,
|
||||
account_name: Option<&str>,
|
||||
) -> Result<(String, TomlAccountConfig)> {
|
||||
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 == true)
|
||||
.map(|_| (name.to_owned(), account.clone()))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account.clone()))
|
||||
.ok_or_else(|| anyhow!("cannot find account {name}")),
|
||||
}?;
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if let Some(imap_config) = toml_account_config.imap.as_mut() {
|
||||
imap_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
|
||||
smtp_config
|
||||
.auth
|
||||
.replace_undefined_keyring_entries(&account_name);
|
||||
}
|
||||
|
||||
Ok((account_name, toml_account_config))
|
||||
}
|
||||
|
||||
/// Build account configurations from a given account name.
|
||||
pub fn into_account_configs(
|
||||
self,
|
||||
account_name: Option<&str>,
|
||||
disable_cache: bool,
|
||||
) -> Result<(TomlAccountConfig, AccountConfig)> {
|
||||
let (account_name, mut toml_account_config) =
|
||||
self.into_toml_account_config(account_name)?;
|
||||
|
||||
if let Some(true) = toml_account_config.sync.as_ref().and_then(|c| c.enable) {
|
||||
if !disable_cache {
|
||||
toml_account_config.backend = Some(BackendKind::MaildirForSync);
|
||||
}
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
display_name: self.display_name,
|
||||
signature: self.signature,
|
||||
signature_delim: self.signature_delim,
|
||||
downloads_dir: self.downloads_dir,
|
||||
|
||||
accounts: HashMap::from_iter(self.accounts.clone().into_iter().map(
|
||||
|(name, config)| {
|
||||
(
|
||||
name.clone(),
|
||||
AccountConfig {
|
||||
name,
|
||||
email: config.email,
|
||||
display_name: config.display_name,
|
||||
signature: config.signature,
|
||||
signature_delim: config.signature_delim,
|
||||
downloads_dir: config.downloads_dir,
|
||||
|
||||
folder: config.folder.map(|c| FolderConfig {
|
||||
aliases: c.alias,
|
||||
list: c.list.map(|c| c.remote),
|
||||
}),
|
||||
envelope: config.envelope.map(|c| EnvelopeConfig {
|
||||
list: c.list.map(|c| c.remote),
|
||||
watch: c.watch.map(|c| c.remote),
|
||||
}),
|
||||
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),
|
||||
}),
|
||||
sync: config.sync,
|
||||
#[cfg(feature = "pgp")]
|
||||
pgp: config.pgp,
|
||||
},
|
||||
)
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
let account_config = config.account(&account_name)?;
|
||||
|
||||
Ok((toml_account_config, 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,791 +0,0 @@
|
|||
#[cfg(feature = "pgp-commands")]
|
||||
use email::account::CmdsPgpConfig;
|
||||
#[cfg(feature = "pgp-gpg")]
|
||||
use email::account::GpgConfig;
|
||||
#[cfg(feature = "pgp")]
|
||||
use email::account::PgpConfig;
|
||||
#[cfg(feature = "pgp-native")]
|
||||
use email::account::{NativePgpConfig, NativePgpSecretKey, SignedSecretKey};
|
||||
#[cfg(feature = "notmuch")]
|
||||
use email::backend::NotmuchConfig;
|
||||
#[cfg(feature = "imap")]
|
||||
use email::imap::config::{ImapAuthConfig, ImapConfig};
|
||||
#[cfg(feature = "smtp")]
|
||||
use email::smtp::config::{SmtpAuthConfig, SmtpConfig};
|
||||
use email::{
|
||||
account::config::{
|
||||
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
||||
passwd::PasswdConfig,
|
||||
},
|
||||
email::config::{EmailHooks, EmailTextPlainFormat},
|
||||
folder::sync::FolderSyncStrategy,
|
||||
maildir::config::MaildirConfig,
|
||||
sendmail::config::SendmailConfig,
|
||||
};
|
||||
use keyring::Entry;
|
||||
use process::{Cmd, Pipeline, SingleCmd};
|
||||
use secret::Secret;
|
||||
use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer};
|
||||
use std::{collections::HashSet, ops::Deref, path::PathBuf};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "Entry", from = "String")]
|
||||
pub struct EntryDef(#[serde(getter = "Deref::deref")] String);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SingleCmd", from = "String")]
|
||||
pub struct SingleCmdDef(#[serde(getter = "Deref::deref")] String);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "Pipeline", from = "Vec<String>")]
|
||||
pub struct PipelineDef(
|
||||
#[serde(getter = "Deref::deref", serialize_with = "pipeline")] Vec<SingleCmd>,
|
||||
);
|
||||
|
||||
// NOTE: did not find the way to do it with macros…
|
||||
pub fn pipeline<S>(cmds: &Vec<SingleCmd>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = s.serialize_seq(Some(cmds.len()))?;
|
||||
for cmd in cmds {
|
||||
seq.serialize_element(&cmd.to_string())?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "Cmd", untagged)]
|
||||
pub enum CmdDef {
|
||||
#[serde(with = "SingleCmdDef")]
|
||||
SingleCmd(SingleCmd),
|
||||
#[serde(with = "PipelineDef")]
|
||||
Pipeline(Pipeline),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "Option<Cmd>", from = "OptionCmd", into = "OptionCmd")]
|
||||
pub struct OptionCmdDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionCmd {
|
||||
#[serde(default, skip)]
|
||||
is_some: bool,
|
||||
#[serde(flatten, with = "CmdDef")]
|
||||
inner: Cmd,
|
||||
}
|
||||
|
||||
impl From<OptionCmd> for Option<Cmd> {
|
||||
fn from(cmd: OptionCmd) -> Option<Cmd> {
|
||||
if cmd.is_some {
|
||||
Some(cmd.inner)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionCmd> for Option<Cmd> {
|
||||
fn into(self) -> OptionCmd {
|
||||
match self {
|
||||
Some(cmd) => OptionCmd {
|
||||
is_some: true,
|
||||
inner: cmd,
|
||||
},
|
||||
None => OptionCmd {
|
||||
is_some: false,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "Secret", rename_all = "kebab-case")]
|
||||
pub enum SecretDef {
|
||||
Raw(String),
|
||||
#[serde(with = "CmdDef")]
|
||||
Cmd(Cmd),
|
||||
#[serde(with = "EntryDef", rename = "keyring")]
|
||||
KeyringEntry(Entry),
|
||||
#[default]
|
||||
Undefined,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "OAuth2Method")]
|
||||
pub enum OAuth2MethodDef {
|
||||
#[serde(rename = "xoauth2", alias = "XOAUTH2")]
|
||||
XOAuth2,
|
||||
#[serde(rename = "oauthbearer", alias = "OAUTHBEARER")]
|
||||
OAuthBearer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<ImapConfig>",
|
||||
from = "OptionImapConfig",
|
||||
into = "OptionImapConfig"
|
||||
)]
|
||||
pub struct OptionImapConfigDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionImapConfig {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "ImapConfigDef")]
|
||||
inner: ImapConfig,
|
||||
}
|
||||
|
||||
impl From<OptionImapConfig> for Option<ImapConfig> {
|
||||
fn from(config: OptionImapConfig) -> Option<ImapConfig> {
|
||||
if config.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(config.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionImapConfig> for Option<ImapConfig> {
|
||||
fn into(self) -> OptionImapConfig {
|
||||
match self {
|
||||
Some(config) => OptionImapConfig {
|
||||
is_none: false,
|
||||
inner: config,
|
||||
},
|
||||
None => OptionImapConfig {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "ImapConfig", rename_all = "kebab-case")]
|
||||
pub struct ImapConfigDef {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub ssl: Option<bool>,
|
||||
pub starttls: Option<bool>,
|
||||
pub insecure: Option<bool>,
|
||||
pub login: String,
|
||||
#[serde(flatten, with = "ImapAuthConfigDef")]
|
||||
pub auth: ImapAuthConfig,
|
||||
pub notify_cmd: Option<String>,
|
||||
pub notify_query: Option<String>,
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "ImapAuthConfig", tag = "auth")]
|
||||
pub enum ImapAuthConfigDef {
|
||||
#[serde(rename = "passwd", alias = "password", with = "ImapPasswdConfigDef")]
|
||||
Passwd(#[serde(default)] PasswdConfig),
|
||||
#[serde(rename = "oauth2", with = "ImapOAuth2ConfigDef")]
|
||||
OAuth2(OAuth2Config),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "PasswdConfig")]
|
||||
pub struct ImapPasswdConfigDef {
|
||||
#[serde(
|
||||
rename = "passwd",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub passwd: Secret,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "OAuth2Config")]
|
||||
pub struct ImapOAuth2ConfigDef {
|
||||
#[serde(rename = "imap-oauth2-method", with = "OAuth2MethodDef", default)]
|
||||
pub method: OAuth2Method,
|
||||
#[serde(rename = "imap-oauth2-client-id")]
|
||||
pub client_id: String,
|
||||
#[serde(
|
||||
rename = "imap-oauth2-client-secret",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub client_secret: Secret,
|
||||
#[serde(rename = "imap-oauth2-auth-url")]
|
||||
pub auth_url: String,
|
||||
#[serde(rename = "imap-oauth2-token-url")]
|
||||
pub token_url: String,
|
||||
#[serde(
|
||||
rename = "imap-oauth2-access-token",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub access_token: Secret,
|
||||
#[serde(
|
||||
rename = "imap-oauth2-refresh-token",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub refresh_token: Secret,
|
||||
#[serde(flatten, with = "ImapOAuth2ScopesDef")]
|
||||
pub scopes: OAuth2Scopes,
|
||||
#[serde(rename = "imap-oauth2-pkce", default)]
|
||||
pub pkce: bool,
|
||||
#[serde(
|
||||
rename = "imap-oauth2-redirect-host",
|
||||
default = "OAuth2Config::default_redirect_host"
|
||||
)]
|
||||
pub redirect_host: String,
|
||||
#[serde(
|
||||
rename = "imap-oauth2-redirect-port",
|
||||
default = "OAuth2Config::default_redirect_port"
|
||||
)]
|
||||
pub redirect_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "OAuth2Scopes")]
|
||||
pub enum ImapOAuth2ScopesDef {
|
||||
#[serde(rename = "imap-oauth2-scope")]
|
||||
Scope(String),
|
||||
#[serde(rename = "imap-oauth2-scopes")]
|
||||
Scopes(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<MaildirConfig>",
|
||||
from = "OptionMaildirConfig",
|
||||
into = "OptionMaildirConfig"
|
||||
)]
|
||||
pub struct OptionMaildirConfigDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionMaildirConfig {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "MaildirConfigDef")]
|
||||
inner: MaildirConfig,
|
||||
}
|
||||
|
||||
impl From<OptionMaildirConfig> for Option<MaildirConfig> {
|
||||
fn from(config: OptionMaildirConfig) -> Option<MaildirConfig> {
|
||||
if config.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(config.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionMaildirConfig> for Option<MaildirConfig> {
|
||||
fn into(self) -> OptionMaildirConfig {
|
||||
match self {
|
||||
Some(config) => OptionMaildirConfig {
|
||||
is_none: false,
|
||||
inner: config,
|
||||
},
|
||||
None => OptionMaildirConfig {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "MaildirConfig", rename_all = "kebab-case")]
|
||||
pub struct MaildirConfigDef {
|
||||
#[serde(rename = "maildir-root-dir")]
|
||||
pub root_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<NotmuchConfig>",
|
||||
from = "OptionNotmuchConfig",
|
||||
into = "OptionNotmuchConfig"
|
||||
)]
|
||||
pub struct OptionNotmuchConfigDef;
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionNotmuchConfig {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "NotmuchConfigDef")]
|
||||
inner: NotmuchConfig,
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
impl From<OptionNotmuchConfig> for Option<NotmuchConfig> {
|
||||
fn from(config: OptionNotmuchConfig) -> Option<NotmuchConfig> {
|
||||
if config.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(config.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
impl Into<OptionNotmuchConfig> for Option<NotmuchConfig> {
|
||||
fn into(self) -> OptionNotmuchConfig {
|
||||
match self {
|
||||
Some(config) => OptionNotmuchConfig {
|
||||
is_none: false,
|
||||
inner: config,
|
||||
},
|
||||
None => OptionNotmuchConfig {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "NotmuchConfig", rename_all = "kebab-case")]
|
||||
pub struct NotmuchConfigDef {
|
||||
#[serde(rename = "notmuch-db-path")]
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<EmailTextPlainFormat>",
|
||||
from = "OptionEmailTextPlainFormat",
|
||||
into = "OptionEmailTextPlainFormat"
|
||||
)]
|
||||
pub struct OptionEmailTextPlainFormatDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionEmailTextPlainFormat {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "EmailTextPlainFormatDef")]
|
||||
inner: EmailTextPlainFormat,
|
||||
}
|
||||
|
||||
impl From<OptionEmailTextPlainFormat> for Option<EmailTextPlainFormat> {
|
||||
fn from(fmt: OptionEmailTextPlainFormat) -> Option<EmailTextPlainFormat> {
|
||||
if fmt.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(fmt.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionEmailTextPlainFormat> for Option<EmailTextPlainFormat> {
|
||||
fn into(self) -> OptionEmailTextPlainFormat {
|
||||
match self {
|
||||
Some(config) => OptionEmailTextPlainFormat {
|
||||
is_none: false,
|
||||
inner: config,
|
||||
},
|
||||
None => OptionEmailTextPlainFormat {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "EmailTextPlainFormat",
|
||||
tag = "type",
|
||||
content = "width",
|
||||
rename_all = "kebab-case"
|
||||
)]
|
||||
pub enum EmailTextPlainFormatDef {
|
||||
#[default]
|
||||
Auto,
|
||||
Flowed,
|
||||
Fixed(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<SmtpConfig>",
|
||||
from = "OptionSmtpConfig",
|
||||
into = "OptionSmtpConfig"
|
||||
)]
|
||||
pub struct OptionSmtpConfigDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionSmtpConfig {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "SmtpConfigDef")]
|
||||
inner: SmtpConfig,
|
||||
}
|
||||
|
||||
impl From<OptionSmtpConfig> for Option<SmtpConfig> {
|
||||
fn from(config: OptionSmtpConfig) -> Option<SmtpConfig> {
|
||||
if config.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(config.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionSmtpConfig> for Option<SmtpConfig> {
|
||||
fn into(self) -> OptionSmtpConfig {
|
||||
match self {
|
||||
Some(config) => OptionSmtpConfig {
|
||||
is_none: false,
|
||||
inner: config,
|
||||
},
|
||||
None => OptionSmtpConfig {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SmtpConfig")]
|
||||
struct SmtpConfigDef {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub ssl: Option<bool>,
|
||||
pub starttls: Option<bool>,
|
||||
pub insecure: Option<bool>,
|
||||
pub login: String,
|
||||
#[serde(flatten, with = "SmtpAuthConfigDef")]
|
||||
pub auth: SmtpAuthConfig,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SmtpAuthConfig", tag = "auth")]
|
||||
pub enum SmtpAuthConfigDef {
|
||||
#[serde(rename = "passwd", alias = "password", with = "SmtpPasswdConfigDef")]
|
||||
Passwd(#[serde(default)] PasswdConfig),
|
||||
#[serde(rename = "oauth2", with = "SmtpOAuth2ConfigDef")]
|
||||
OAuth2(OAuth2Config),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "PasswdConfig", default)]
|
||||
pub struct SmtpPasswdConfigDef {
|
||||
#[serde(
|
||||
rename = "passwd",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub passwd: Secret,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "OAuth2Config", rename_all = "kebab-case")]
|
||||
pub struct SmtpOAuth2ConfigDef {
|
||||
#[serde(with = "OAuth2MethodDef", default)]
|
||||
pub method: OAuth2Method,
|
||||
pub client_id: String,
|
||||
#[serde(
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub client_secret: Secret,
|
||||
pub auth_url: String,
|
||||
pub token_url: String,
|
||||
#[serde(
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub access_token: Secret,
|
||||
#[serde(
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub refresh_token: Secret,
|
||||
#[serde(flatten, with = "SmtpOAuth2ScopesDef")]
|
||||
pub scopes: OAuth2Scopes,
|
||||
#[serde(default)]
|
||||
pub pkce: bool,
|
||||
#[serde(default = "OAuth2Config::default_redirect_host")]
|
||||
pub redirect_host: String,
|
||||
#[serde(default = "OAuth2Config::default_redirect_port")]
|
||||
pub redirect_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "OAuth2Scopes")]
|
||||
pub enum SmtpOAuth2ScopesDef {
|
||||
#[serde(rename = "smtp-oauth2-scope")]
|
||||
Scope(String),
|
||||
#[serde(rename = "smtp-oauth2-scopes")]
|
||||
Scopes(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<SendmailConfig>",
|
||||
from = "OptionSendmailConfig",
|
||||
into = "OptionSendmailConfig"
|
||||
)]
|
||||
pub struct OptionSendmailConfigDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionSendmailConfig {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "SendmailConfigDef")]
|
||||
inner: SendmailConfig,
|
||||
}
|
||||
|
||||
impl From<OptionSendmailConfig> for Option<SendmailConfig> {
|
||||
fn from(config: OptionSendmailConfig) -> Option<SendmailConfig> {
|
||||
if config.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(config.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionSendmailConfig> for Option<SendmailConfig> {
|
||||
fn into(self) -> OptionSendmailConfig {
|
||||
match self {
|
||||
Some(config) => OptionSendmailConfig {
|
||||
is_none: false,
|
||||
inner: config,
|
||||
},
|
||||
None => OptionSendmailConfig {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SendmailConfig", rename_all = "kebab-case")]
|
||||
pub struct SendmailConfigDef {
|
||||
#[serde(with = "CmdDef", default = "sendmail_default_cmd")]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
fn sendmail_default_cmd() -> Cmd {
|
||||
Cmd::from("/usr/sbin/sendmail")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<EmailHooks>",
|
||||
from = "OptionEmailHooks",
|
||||
into = "OptionEmailHooks"
|
||||
)]
|
||||
pub struct OptionEmailHooksDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionEmailHooks {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(
|
||||
flatten,
|
||||
skip_serializing_if = "EmailHooks::is_empty",
|
||||
with = "EmailHooksDef"
|
||||
)]
|
||||
inner: EmailHooks,
|
||||
}
|
||||
|
||||
impl From<OptionEmailHooks> for Option<EmailHooks> {
|
||||
fn from(hooks: OptionEmailHooks) -> Option<EmailHooks> {
|
||||
if hooks.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(hooks.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionEmailHooks> for Option<EmailHooks> {
|
||||
fn into(self) -> OptionEmailHooks {
|
||||
match self {
|
||||
Some(hooks) => OptionEmailHooks {
|
||||
is_none: false,
|
||||
inner: hooks,
|
||||
},
|
||||
None => OptionEmailHooks {
|
||||
is_none: true,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the email hooks. Useful for doing extra email
|
||||
/// processing before or after sending it.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "EmailHooks", rename_all = "kebab-case")]
|
||||
pub struct EmailHooksDef {
|
||||
/// Represents the hook called just before sending an email.
|
||||
#[serde(default, with = "OptionCmdDef")]
|
||||
pub pre_send: Option<Cmd>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
remote = "Option<FolderSyncStrategy>",
|
||||
from = "OptionFolderSyncStrategy",
|
||||
into = "OptionFolderSyncStrategy"
|
||||
)]
|
||||
pub struct OptionFolderSyncStrategyDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionFolderSyncStrategy {
|
||||
#[serde(default, skip)]
|
||||
is_some: bool,
|
||||
#[serde(
|
||||
flatten,
|
||||
skip_serializing_if = "FolderSyncStrategy::is_default",
|
||||
with = "FolderSyncStrategyDef"
|
||||
)]
|
||||
inner: FolderSyncStrategy,
|
||||
}
|
||||
|
||||
impl From<OptionFolderSyncStrategy> for Option<FolderSyncStrategy> {
|
||||
fn from(option: OptionFolderSyncStrategy) -> Option<FolderSyncStrategy> {
|
||||
if option.is_some {
|
||||
Some(option.inner)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<OptionFolderSyncStrategy> for Option<FolderSyncStrategy> {
|
||||
fn into(self) -> OptionFolderSyncStrategy {
|
||||
match self {
|
||||
Some(strategy) => OptionFolderSyncStrategy {
|
||||
is_some: true,
|
||||
inner: strategy,
|
||||
},
|
||||
None => OptionFolderSyncStrategy {
|
||||
is_some: false,
|
||||
inner: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "FolderSyncStrategy", rename_all = "kebab-case")]
|
||||
pub enum FolderSyncStrategyDef {
|
||||
#[default]
|
||||
All,
|
||||
#[serde(alias = "only")]
|
||||
Include(HashSet<String>),
|
||||
#[serde(alias = "except")]
|
||||
#[serde(alias = "ignore")]
|
||||
Exclude(HashSet<String>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "Option<PgpConfig>", from = "OptionPgpConfig")]
|
||||
pub struct OptionPgpConfigDef;
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OptionPgpConfig {
|
||||
#[serde(default, skip)]
|
||||
is_none: bool,
|
||||
#[serde(flatten, with = "PgpConfigDef")]
|
||||
inner: PgpConfig,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
impl From<OptionPgpConfig> for Option<PgpConfig> {
|
||||
fn from(config: OptionPgpConfig) -> Option<PgpConfig> {
|
||||
if config.is_none {
|
||||
None
|
||||
} else {
|
||||
Some(config.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "PgpConfig", tag = "backend", rename_all = "kebab-case")]
|
||||
pub enum PgpConfigDef {
|
||||
#[cfg(feature = "pgp-commands")]
|
||||
#[serde(with = "CmdsPgpConfigDef", alias = "commands")]
|
||||
Cmds(CmdsPgpConfig),
|
||||
#[cfg(feature = "pgp-gpg")]
|
||||
#[serde(with = "GpgConfigDef")]
|
||||
Gpg(GpgConfig),
|
||||
#[cfg(feature = "pgp-native")]
|
||||
#[serde(with = "NativePgpConfigDef")]
|
||||
Native(NativePgpConfig),
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp-gpg")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "GpgConfig", rename_all = "kebab-case")]
|
||||
pub struct GpgConfigDef;
|
||||
|
||||
#[cfg(feature = "pgp-commands")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "CmdsPgpConfig", rename_all = "kebab-case")]
|
||||
pub struct CmdsPgpConfigDef {
|
||||
#[serde(default, with = "OptionCmdDef")]
|
||||
encrypt_cmd: Option<Cmd>,
|
||||
#[serde(default)]
|
||||
encrypt_recipient_fmt: Option<String>,
|
||||
#[serde(default)]
|
||||
encrypt_recipients_sep: Option<String>,
|
||||
#[serde(default, with = "OptionCmdDef")]
|
||||
decrypt_cmd: Option<Cmd>,
|
||||
#[serde(default, with = "OptionCmdDef")]
|
||||
sign_cmd: Option<Cmd>,
|
||||
#[serde(default, with = "OptionCmdDef")]
|
||||
verify_cmd: Option<Cmd>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp-native")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "NativePgpConfig", rename_all = "kebab-case")]
|
||||
pub struct NativePgpConfigDef {
|
||||
#[serde(default, with = "NativePgpSecretKeyDef")]
|
||||
secret_key: NativePgpSecretKey,
|
||||
#[serde(default, with = "SecretDef")]
|
||||
secret_key_passphrase: Secret,
|
||||
#[serde(default = "NativePgpConfig::default_wkd")]
|
||||
wkd: bool,
|
||||
#[serde(default = "NativePgpConfig::default_key_servers")]
|
||||
key_servers: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp-native")]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "NativePgpSecretKey", rename_all = "kebab-case")]
|
||||
pub enum NativePgpSecretKeyDef {
|
||||
#[default]
|
||||
None,
|
||||
#[serde(skip)]
|
||||
Raw(SignedSecretKey),
|
||||
Path(PathBuf),
|
||||
#[serde(with = "EntryDef")]
|
||||
Keyring(Entry),
|
||||
}
|
|
@ -1,190 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
|
||||
use once_cell::sync::Lazy;
|
||||
use shellexpand_utils::expand;
|
||||
use std::{fs, io, path::PathBuf, process};
|
||||
use toml_edit::{Document, Item};
|
||||
|
||||
use crate::account;
|
||||
|
||||
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) static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
|
||||
|
||||
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 mut doc = toml::to_string(&config)?.parse::<Document>()?;
|
||||
|
||||
doc.iter_mut().for_each(|(_, item)| {
|
||||
set_table_dotted(item, "folder-aliases");
|
||||
set_table_dotted(item, "sync-folders-strategy");
|
||||
|
||||
set_table_dotted(item, "folder");
|
||||
get_table_mut(item, "folder").map(|item| {
|
||||
set_tables_dotted(item, ["add", "list", "expunge", "purge", "delete"]);
|
||||
});
|
||||
|
||||
set_table_dotted(item, "envelope");
|
||||
get_table_mut(item, "envelope").map(|item| {
|
||||
set_tables_dotted(item, ["list", "get"]);
|
||||
});
|
||||
|
||||
set_table_dotted(item, "flag");
|
||||
get_table_mut(item, "flag").map(|item| {
|
||||
set_tables_dotted(item, ["add", "set", "remove"]);
|
||||
});
|
||||
|
||||
set_table_dotted(item, "message");
|
||||
get_table_mut(item, "message").map(|item| {
|
||||
set_tables_dotted(
|
||||
item,
|
||||
["add", "send", "peek", "get", "copy", "move", "delete"],
|
||||
);
|
||||
});
|
||||
|
||||
set_table_dotted(item, "maildir");
|
||||
#[cfg(feature = "imap")]
|
||||
{
|
||||
set_table_dotted(item, "imap");
|
||||
get_table_mut(item, "imap").map(|item| {
|
||||
set_tables_dotted(item, ["passwd", "oauth2"]);
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
set_table_dotted(item, "notmuch");
|
||||
set_table_dotted(item, "sendmail");
|
||||
#[cfg(feature = "smtp")]
|
||||
{
|
||||
set_table_dotted(item, "smtp");
|
||||
get_table_mut(item, "smtp").map(|item| {
|
||||
set_tables_dotted(item, ["passwd", "oauth2"]);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "pgp")]
|
||||
set_table_dotted(item, "pgp");
|
||||
});
|
||||
|
||||
fs::create_dir_all(path.parent().unwrap_or(&path))?;
|
||||
fs::write(path, doc.to_string())?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
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) {
|
||||
get_table_mut(item, key)
|
||||
.and_then(|item| item.as_table_mut())
|
||||
.map(|table| 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)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_passwd(prompt: &str) -> io::Result<String> {
|
||||
Password::with_theme(&*THEME)
|
||||
.with_prompt(prompt)
|
||||
.with_confirmation(
|
||||
"Confirm password",
|
||||
"Passwords do not match, please try again.",
|
||||
)
|
||||
.interact()
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_secret(prompt: &str) -> io::Result<String> {
|
||||
Password::with_theme(&*THEME)
|
||||
.with_prompt(prompt)
|
||||
.report(false)
|
||||
.interact()
|
||||
}
|
|
@ -1,25 +1,31 @@
|
|||
use anyhow::Result;
|
||||
use std::{process::exit, sync::Arc};
|
||||
|
||||
use ariadne::{Color, Label, Report, ReportKind, Source};
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, config::Config, email::search_query,
|
||||
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalArg,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// 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: FolderNameOptionalArg,
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
/// The page number.
|
||||
///
|
||||
|
@ -34,43 +40,179 @@ pub struct ListEnvelopesCommand {
|
|||
#[arg(long, short = 's', value_name = "NUMBER")]
|
||||
pub page_size: Option<usize>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub table: TableMaxWidthFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
|
||||
/// The list envelopes filter and sort query.
|
||||
///
|
||||
/// The query can be a filter query, a sort query or both
|
||||
/// together.
|
||||
///
|
||||
/// A filter query is composed of operators and conditions. There
|
||||
/// is 3 operators and 8 conditions:
|
||||
///
|
||||
/// • not <condition> → filter envelopes that do not match the
|
||||
/// condition
|
||||
///
|
||||
/// • <condition> and <condition> → filter envelopes that match
|
||||
/// both conditions
|
||||
///
|
||||
/// • <condition> or <condition> → filter envelopes that match
|
||||
/// one of the conditions
|
||||
///
|
||||
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
|
||||
/// date
|
||||
///
|
||||
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
|
||||
/// before the given one
|
||||
///
|
||||
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
|
||||
/// after the given one
|
||||
///
|
||||
/// ◦ from <pattern> → filter envelopes with senders matching the
|
||||
/// given pattern
|
||||
///
|
||||
/// ◦ to <pattern> → filter envelopes with recipients matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ subject <pattern> → filter envelopes with subject matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ body <pattern> → filter envelopes with text bodies matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ flag <flag> → filter envelopes matching the given flag
|
||||
///
|
||||
/// A sort query starts by "order by", and is composed of kinds
|
||||
/// and orders. There is 4 kinds and 2 orders:
|
||||
///
|
||||
/// • date [order] → sort envelopes by date
|
||||
///
|
||||
/// • from [order] → sort envelopes by sender
|
||||
///
|
||||
/// • to [order] → sort envelopes by recipient
|
||||
///
|
||||
/// • subject [order] → sort envelopes by subject
|
||||
///
|
||||
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
|
||||
/// order
|
||||
///
|
||||
/// ◦ <kind> desc → sort envelopes by the given kind in
|
||||
/// descending order
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// subject foo and body bar → filter envelopes containing "foo"
|
||||
/// in their subject and "bar" in their text bodies
|
||||
///
|
||||
/// order by date desc subject → sort envelopes by descending date
|
||||
/// (most recent first), then by ascending subject
|
||||
///
|
||||
/// subject foo and body bar order by date desc subject →
|
||||
/// combination of the 2 previous examples
|
||||
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
|
||||
pub query: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ListEnvelopesCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing envelope list command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let page_size = self
|
||||
.page_size
|
||||
.unwrap_or(account_config.get_envelope_list_page_size());
|
||||
let page = 1.max(self.page) - 1;
|
||||
|
||||
let envelopes = backend.list_envelopes(folder, page_size, page).await?;
|
||||
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &account_config.get_message_read_format(),
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
impl Default for EnvelopeListCommand {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
folder: Default::default(),
|
||||
page: 1,
|
||||
page_size: Default::default(),
|
||||
account: Default::default(),
|
||||
query: Default::default(),
|
||||
table_max_width: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(), |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;
|
||||
let page_size = self
|
||||
.page_size
|
||||
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_list_envelopes(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let query = self
|
||||
.query
|
||||
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
|
||||
let query = match query {
|
||||
None => None,
|
||||
Some(Ok(query)) => Some(query),
|
||||
Some(Err(main_err)) => {
|
||||
let source = "query";
|
||||
let search_query::error::Error::ParseError(errs, query) = &main_err;
|
||||
for err in errs {
|
||||
Report::build(ReportKind::Error, source, err.span().start)
|
||||
.with_message(main_err.to_string())
|
||||
.with_label(
|
||||
Label::new((source, err.span().into_range()))
|
||||
.with_message(err.reason().to_string())
|
||||
.with_color(Color::Red),
|
||||
)
|
||||
.finish()
|
||||
.eprint((source, Source::from(&query)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
exit(0)
|
||||
}
|
||||
};
|
||||
|
||||
let opts = ListEnvelopesOptions {
|
||||
page,
|
||||
page_size,
|
||||
query,
|
||||
};
|
||||
|
||||
let envelopes = backend.list_envelopes(folder, opts).await?;
|
||||
let table = EnvelopesTable::from(envelopes)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(toml_account_config.envelope_list_table_preset())
|
||||
.with_some_unseen_char(toml_account_config.envelope_list_table_unseen_char())
|
||||
.with_some_replied_char(toml_account_config.envelope_list_table_replied_char())
|
||||
.with_some_flagged_char(toml_account_config.envelope_list_table_flagged_char())
|
||||
.with_some_attachment_char(toml_account_config.envelope_list_table_attachment_char())
|
||||
.with_some_id_color(toml_account_config.envelope_list_table_id_color())
|
||||
.with_some_flags_color(toml_account_config.envelope_list_table_flags_color())
|
||||
.with_some_subject_color(toml_account_config.envelope_list_table_subject_color())
|
||||
.with_some_sender_color(toml_account_config.envelope_list_table_sender_color())
|
||||
.with_some_date_color(toml_account_config.envelope_list_table_date_color());
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
pub mod list;
|
||||
pub mod watch;
|
||||
pub mod thread;
|
||||
|
||||
use anyhow::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,17 +18,18 @@ 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 {
|
||||
#[allow(unused)]
|
||||
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,44 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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,
|
||||
|
||||
#[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 envelopes watch command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
printer.print_log(format!(
|
||||
"Start watching folder {folder} for envelopes changes…"
|
||||
))?;
|
||||
|
||||
backend.watch_envelopes(&folder).await
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
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>,
|
||||
}
|
||||
|
||||
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(get) = &self.get {
|
||||
kinds.extend(get.get_used_backends());
|
||||
}
|
||||
|
||||
if let Some(watch) = &self.watch {
|
||||
kinds.extend(watch.get_used_backends());
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ListEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::envelope::list::config::EnvelopeListConfig,
|
||||
}
|
||||
|
||||
impl ListEnvelopesConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct WatchEnvelopesConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub remote: email::envelope::watch::config::WatchEnvelopeConfig,
|
||||
}
|
||||
|
||||
impl WatchEnvelopesConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct GetEnvelopeConfig {
|
||||
pub backend: Option<BackendKind>,
|
||||
}
|
||||
|
||||
impl GetEnvelopeConfig {
|
||||
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||
let mut kinds = HashSet::default();
|
||||
|
||||
if let Some(kind) = &self.backend {
|
||||
kinds.insert(kind);
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use clap::Parser;
|
||||
use email::flag::{Flag, Flags};
|
||||
use log::debug;
|
||||
use tracing::debug;
|
||||
|
||||
/// The ids and/or flags arguments parser.
|
||||
#[derive(Debug, Parser)]
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
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::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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).
|
||||
|
@ -24,28 +28,37 @@ pub struct FlagAddCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FlagAddCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing flag add command");
|
||||
info!("executing add flag(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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 anyhow::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)]
|
||||
|
@ -31,6 +31,7 @@ pub enum FlagSubcommand {
|
|||
}
|
||||
|
||||
impl FlagSubcommand {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
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::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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).
|
||||
|
@ -24,28 +28,37 @@ pub struct FlagRemoveCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FlagRemoveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing flag remove command");
|
||||
info!("executing remove flag(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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,18 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
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::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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).
|
||||
|
@ -24,28 +28,37 @@ pub struct FlagSetCommand {
|
|||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FlagSetCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing flag set command");
|
||||
info!("executing set flag(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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 set!"))
|
||||
printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
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>,
|
||||
}
|
||||
|
||||
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,123 +1,3 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod flag;
|
||||
|
||||
use anyhow::Result;
|
||||
use email::account::config::AccountConfig;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
cache::IdMapper,
|
||||
flag::{Flag, Flags},
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Mailbox {
|
||||
pub name: Option<String>,
|
||||
pub addr: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Envelope {
|
||||
pub id: String,
|
||||
pub flags: Flags,
|
||||
pub subject: String,
|
||||
pub from: Mailbox,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for Envelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("FROM").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let flags = {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if !unseen { " " } else { "✷" });
|
||||
flags.push_str(if self.flags.contains(&Flag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.flags.contains(&Flag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
};
|
||||
let subject = &self.subject;
|
||||
let sender = if let Some(name) = &self.from.name {
|
||||
name
|
||||
} else {
|
||||
&self.from.addr
|
||||
};
|
||||
let date = &self.date;
|
||||
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the list of envelopes.
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Envelopes(Vec<Envelope>);
|
||||
|
||||
impl Envelopes {
|
||||
pub fn from_backend(
|
||||
config: &AccountConfig,
|
||||
id_mapper: &IdMapper,
|
||||
envelopes: email::envelope::Envelopes,
|
||||
) -> Result<Envelopes> {
|
||||
let envelopes = envelopes
|
||||
.iter()
|
||||
.map(|envelope| {
|
||||
Ok(Envelope {
|
||||
id: id_mapper.get_or_create_alias(&envelope.id)?,
|
||||
flags: envelope.flags.clone().into(),
|
||||
subject: envelope.subject.clone(),
|
||||
from: Mailbox {
|
||||
name: envelope.from.name.clone(),
|
||||
addr: envelope.from.addr.clone(),
|
||||
},
|
||||
date: envelope.format_date(config),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Envelopes(envelopes))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Envelopes {
|
||||
type Target = Vec<Envelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Envelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ pub struct MessageRawBodyArg {
|
|||
|
||||
impl MessageRawBodyArg {
|
||||
pub fn raw(self) -> String {
|
||||
self.raw.join(" ").replace("\r", "").replace("\n", "\r\n")
|
||||
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ pub struct HeaderRawArgs {
|
|||
}
|
||||
|
||||
pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> {
|
||||
if let Some((key, val)) = raw_header.split_once(":") {
|
||||
if let Some((key, val)) = raw_header.split_once(':') {
|
||||
Ok((key.trim().to_owned(), val.trim().to_owned()))
|
||||
} else {
|
||||
Err(format!("cannot parse raw header {raw_header:?}"))
|
||||
|
|
|
@ -15,6 +15,6 @@ pub struct MessageRawArg {
|
|||
|
||||
impl MessageRawArg {
|
||||
pub fn raw(self) -> String {
|
||||
self.raw.join(" ").replace("\r", "").replace("\n", "\r\n")
|
||||
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use std::{fs, path::PathBuf};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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.
|
||||
|
@ -22,27 +26,39 @@ pub struct AttachmentDownloadCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl AttachmentDownloadCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing attachment download command");
|
||||
info!("executing download attachment(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let ids = &self.envelopes.ids;
|
||||
let emails = backend.get_messages(&folder, ids).await?;
|
||||
|
||||
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 emails = backend.get_messages(folder, ids).await?;
|
||||
|
||||
let mut emails_count = 0;
|
||||
let mut attachments_count = 0;
|
||||
|
@ -53,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()
|
||||
))?;
|
||||
|
||||
|
@ -70,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;
|
||||
|
@ -78,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 @@
|
|||
pub mod download;
|
||||
mod download;
|
||||
|
||||
use anyhow::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,18 +1,23 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
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::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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)]
|
||||
|
@ -24,31 +29,41 @@ pub struct MessageCopyCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageCopyCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message copy command");
|
||||
|
||||
let from_folder = &self.source_folder.name;
|
||||
let to_folder = &self.target_folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
info!("executing copy message(s) command");
|
||||
|
||||
let source = &self.source_folder.name;
|
||||
let target = &self.target_folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
backend.copy_messages(from_folder, to_folder, ids).await?;
|
||||
|
||||
printer.print(format!(
|
||||
"Message(s) successfully copied from {from_folder} to {to_folder}!"
|
||||
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 = 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.out(format!(
|
||||
"Message(s) successfully copied from {source} to {target}!\n"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
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::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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
|
||||
|
@ -22,28 +28,38 @@ pub struct MessageDeleteCommand {
|
|||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageDeleteCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message delete command");
|
||||
info!("executing delete message(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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,20 +1,23 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
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,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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
|
||||
|
@ -34,36 +37,48 @@ pub struct MessageForwardCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageForwardCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message forward command");
|
||||
info!("executing forward message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
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)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(anyhow!("cannot find message"))?
|
||||
.to_forward_tpl_builder(&account_config)
|
||||
.ok_or(eyre!("cannot find message"))?
|
||||
.to_forward_tpl_builder(account_config.clone())
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?;
|
||||
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::{debug, info};
|
||||
use mail_builder::MessageBuilder;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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
|
||||
|
@ -21,9 +24,6 @@ pub struct MessageMailtoCommand {
|
|||
#[arg()]
|
||||
pub url: Url,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
@ -32,50 +32,67 @@ impl MessageMailtoCommand {
|
|||
pub fn new(url: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: Url::parse(url)?,
|
||||
cache: Default::default(),
|
||||
account: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message mailto command");
|
||||
info!("executing mailto message command");
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = 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, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let mut builder = MessageBuilder::new().to(self.url.path());
|
||||
let mut body = String::new();
|
||||
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)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut msg = Vec::<u8>::new();
|
||||
let mut body = Vec::<u8>::new();
|
||||
|
||||
msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n");
|
||||
|
||||
for (key, val) in self.url.query_pairs() {
|
||||
match key.to_lowercase().as_bytes() {
|
||||
b"cc" => builder = builder.cc(val.to_string()),
|
||||
b"bcc" => builder = builder.bcc(val.to_string()),
|
||||
b"subject" => builder = builder.subject(val),
|
||||
b"body" => body += &val,
|
||||
_ => (),
|
||||
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() {
|
||||
Ok(Some(ref signature)) => builder = builder.text_body(body + "\n\n" + signature),
|
||||
Ok(None) => builder = builder.text_body(body),
|
||||
Err(err) => {
|
||||
debug!("cannot add signature to mailto message, skipping it: {err}");
|
||||
debug!("{err:?}");
|
||||
}
|
||||
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)
|
||||
.await?;
|
||||
.from_bytes(msg)
|
||||
.await?
|
||||
.into();
|
||||
|
||||
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod edit;
|
||||
pub mod export;
|
||||
pub mod forward;
|
||||
pub mod mailto;
|
||||
pub mod move_;
|
||||
pub mod r#move;
|
||||
pub mod read;
|
||||
pub mod reply;
|
||||
pub mod save;
|
||||
pub mod send;
|
||||
pub mod thread;
|
||||
pub mod write;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
|
||||
mailto::MessageMailtoCommand, 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,22 +37,26 @@ 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),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Save(MessageSaveCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Send(MessageSendCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
|
@ -64,12 +73,16 @@ pub enum MessageSubcommand {
|
|||
}
|
||||
|
||||
impl MessageSubcommand {
|
||||
#[allow(unused)]
|
||||
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,
|
||||
|
|
70
src/email/message/command/move.rs
Normal file
70
src/email/message/command/move.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
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;
|
||||
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
|
||||
};
|
||||
|
||||
/// Move the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageMoveCommand {
|
||||
#[command(flatten)]
|
||||
pub source_folder: SourceFolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub target_folder: TargetFolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageMoveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing move message(s) command");
|
||||
|
||||
let source = &self.source_folder.name;
|
||||
let target = &self.target_folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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.out(format!(
|
||||
"Message(s) successfully moved from {source} to {target}!\n"
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
|
||||
printer::Printer,
|
||||
};
|
||||
|
||||
/// Move a message from a source folder to a target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageMoveCommand {
|
||||
#[command(flatten)]
|
||||
pub source_folder: SourceFolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub target_folder: TargetFolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageMoveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message move command");
|
||||
|
||||
let from_folder = &self.source_folder.name;
|
||||
let to_folder = &self.target_folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let ids = &self.envelopes.ids;
|
||||
backend.move_messages(from_folder, to_folder, ids).await?;
|
||||
|
||||
printer.print(format!(
|
||||
"Message(s) successfully moved from {from_folder} to {to_folder}!"
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,19 +1,26 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use mml::message::FilterParts;
|
||||
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;
|
||||
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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)]
|
||||
|
@ -27,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,
|
||||
|
||||
|
@ -62,34 +48,46 @@ 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>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageReadCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message read command");
|
||||
info!("executing read message(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let emails = if self.preview {
|
||||
backend.peek_messages(&folder, &ids).await
|
||||
backend.peek_messages(folder, ids).await
|
||||
} else {
|
||||
backend.get_messages(&folder, &ids).await
|
||||
backend.get_messages(folder, ids).await
|
||||
}?;
|
||||
|
||||
let mut glue = "";
|
||||
|
@ -98,33 +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()?).into_owned());
|
||||
} else {
|
||||
let tpl: String = 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?
|
||||
.into();
|
||||
bodies.push_str(&tpl);
|
||||
}
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
bodies.push_str(&tpl);
|
||||
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.print(bodies)
|
||||
printer.out(bodies)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use email::flag::Flag;
|
||||
use log::info;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config, flag::Flag};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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
|
||||
|
@ -38,40 +40,53 @@ pub struct MessageReplyCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageReplyCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message reply command");
|
||||
info!("executing reply message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = 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, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
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)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(anyhow!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(&account_config)
|
||||
.ok_or(eyre!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(account_config.clone())
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.with_reply_all(self.reply.all)
|
||||
.build()
|
||||
.await?;
|
||||
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await?;
|
||||
|
||||
// TODO: let backend.send_reply_raw_message adding the flag
|
||||
backend.add_flag(&folder, &[id], Flag::Answered).await
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
|
||||
|
||||
backend.add_flag(folder, &[id], Flag::Answered).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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)]
|
||||
|
@ -20,24 +27,34 @@ pub struct MessageSaveCommand {
|
|||
#[command(flatten)]
|
||||
pub message: MessageRawArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageSaveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message save command");
|
||||
info!("executing save message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
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 = 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();
|
||||
let is_json = printer.is_json();
|
||||
|
@ -47,13 +64,13 @@ impl MessageSaveCommand {
|
|||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map_while(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
|
||||
backend.add_raw_message(folder, msg.as_bytes()).await?;
|
||||
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,14 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use std::io::{self, BufRead, IsTerminal};
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
config::TomlConfig, message::arg::MessageRawArg, printer::Printer,
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
/// Send a message.
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
|
||||
|
||||
/// Send the given raw message.
|
||||
///
|
||||
/// This command allows you to send a raw message and to save a copy
|
||||
/// to your send folder.
|
||||
|
@ -17,23 +22,32 @@ pub struct MessageSendCommand {
|
|||
#[command(flatten)]
|
||||
pub message: MessageRawArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageSendCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message send command");
|
||||
info!("executing send message command");
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = 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, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let msg = if io::stdin().is_terminal() {
|
||||
self.message.raw()
|
||||
|
@ -41,13 +55,13 @@ impl MessageSendCommand {
|
|||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map_while(Result::ok)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
|
||||
backend.send_raw_message(msg.as_bytes()).await?;
|
||||
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,19 +1,24 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use email::message::Message;
|
||||
use log::info;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, message::Message},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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
|
||||
|
@ -27,30 +32,41 @@ pub struct MessageWriteCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageWriteCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing message write command");
|
||||
info!("executing write message command");
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = 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, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let tpl = Message::new_tpl_builder(&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)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = Message::new_tpl_builder(account_config.clone())
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
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>,
|
||||
#[serde(rename = "move")]
|
||||
pub move_: Option<MessageMoveConfig>,
|
||||
}
|
||||
|
||||
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.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_raw::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_raw::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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
pub mod arg;
|
||||
pub mod attachment;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod template;
|
||||
|
|
|
@ -12,7 +12,7 @@ pub struct TemplateRawBodyArg {
|
|||
|
||||
impl TemplateRawBodyArg {
|
||||
pub fn raw(self) -> String {
|
||||
self.raw.join(" ").replace("\r", "")
|
||||
self.raw.join(" ").replace('\r', "")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,6 @@ pub struct TemplateRawArg {
|
|||
|
||||
impl TemplateRawArg {
|
||||
pub fn raw(self) -> String {
|
||||
self.raw.join(" ").replace("\r", "")
|
||||
self.raw.join(" ").replace('\r', "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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.
|
||||
|
@ -32,38 +36,49 @@ pub struct TemplateForwardCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl TemplateForwardCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing template forward command");
|
||||
info!("executing forward template command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
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 id = self.envelope.id;
|
||||
let tpl: String = backend
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(anyhow!("cannot find message {id}"))?
|
||||
.to_forward_tpl_builder(&account_config)
|
||||
.ok_or(eyre!("cannot find message {id}"))?
|
||||
.to_forward_tpl_builder(account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?
|
||||
.into();
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
pub mod forward;
|
||||
pub mod reply;
|
||||
pub mod save;
|
||||
pub mod send;
|
||||
pub mod write;
|
||||
mod forward;
|
||||
mod reply;
|
||||
mod save;
|
||||
mod send;
|
||||
mod write;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, 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,16 +1,20 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
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.
|
||||
|
@ -36,39 +40,50 @@ pub struct TemplateReplyCommand {
|
|||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl TemplateReplyCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing template reply command");
|
||||
info!("executing reply template command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let id = self.envelope.id;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let tpl: String = backend
|
||||
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 tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(anyhow!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(&account_config)
|
||||
.ok_or(eyre!("cannot find message {id}"))?
|
||||
.to_reply_tpl_builder(account_config)
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.with_reply_all(self.reply.all)
|
||||
.build()
|
||||
.await?
|
||||
.into();
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use color_eyre::Result;
|
||||
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;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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.
|
||||
|
@ -24,24 +31,36 @@ pub struct TemplateSaveCommand {
|
|||
#[command(flatten)]
|
||||
pub template: TemplateRawArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl TemplateSaveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing template save command");
|
||||
info!("executing save template command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
|
||||
let (toml_account_config, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
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)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let is_tty = io::stdin().is_terminal();
|
||||
let is_json = printer.is_json();
|
||||
|
@ -51,7 +70,7 @@ impl TemplateSaveCommand {
|
|||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map_while(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
@ -59,12 +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_raw_message(folder, &msg).await?;
|
||||
|
||||
printer.print(format!("Template successfully saved to {folder}!"))
|
||||
backend.add_message(folder, &msg).await?;
|
||||
|
||||
printer.out(format!("Template successfully saved to {folder}!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
use anyhow::Result;
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use color_eyre::Result;
|
||||
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;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
config::TomlConfig, email::template::arg::TemplateRawArg, printer::Printer,
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
|
||||
};
|
||||
|
||||
/// Send a template.
|
||||
|
@ -20,23 +28,34 @@ pub struct TemplateSendCommand {
|
|||
#[command(flatten)]
|
||||
pub template: TemplateRawArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl TemplateSendCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing template send command");
|
||||
info!("executing send template command");
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = 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, account_config) =
|
||||
config.clone().into_account_configs(account, cache)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||
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)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let tpl = if io::stdin().is_terminal() {
|
||||
self.template.raw()
|
||||
|
@ -44,7 +63,7 @@ impl TemplateSendCommand {
|
|||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map_while(Result::ok)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
@ -52,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_raw_message(&msg).await?;
|
||||
backend.send_message_then_save_copy(&msg).await?;
|
||||
|
||||
printer.print("Template successfully sent!")
|
||||
printer.out("Message successfully sent!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use email::message::Message;
|
||||
use log::info;
|
||||
use color_eyre::Result;
|
||||
use email::{config::Config, message::Message};
|
||||
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, cache::arg::disable::CacheDisableFlag, config::TomlConfig,
|
||||
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.
|
||||
|
@ -21,29 +23,26 @@ pub struct TemplateWriteCommand {
|
|||
#[command(flatten)]
|
||||
pub body: TemplateRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl TemplateWriteCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing template write command");
|
||||
info!("executing write template command");
|
||||
|
||||
let account = self.account.name.as_ref().map(String::as_str);
|
||||
let cache = self.cache.disable;
|
||||
let (_, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let (_, account_config) = config.clone().into_account_configs(account, cache)?;
|
||||
|
||||
let tpl: String = 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?
|
||||
.into();
|
||||
.await?;
|
||||
|
||||
printer.print(tpl)
|
||||
printer.out(tpl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,14 @@ pub struct FolderNameOptionalFlag {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
impl Default for FolderNameOptionalFlag {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: INBOX.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional folder name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderNameOptionalArg {
|
||||
|
@ -18,6 +26,14 @@ pub struct FolderNameOptionalArg {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
impl Default for FolderNameOptionalArg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: INBOX.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The required folder name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderNameArg {
|
||||
|
|
61
src/folder/command/add.rs
Normal file
61
src/folder/command/add.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::add::AddFolder},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
|
||||
};
|
||||
|
||||
/// Create the given folder.
|
||||
///
|
||||
/// This command allows you to create a new folder using the given
|
||||
/// name.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderAddCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
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(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
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.out(format!("Folder {folder} successfully created!\n"))
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer,
|
||||
};
|
||||
|
||||
/// Create a new folder.
|
||||
///
|
||||
/// This command allows you to create a new folder using the given
|
||||
/// name.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderCreateCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FolderCreateCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing folder create command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
backend.add_folder(&folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully created!"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,15 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use std::{process, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use dialoguer::Confirm;
|
||||
use log::info;
|
||||
use std::process;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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.
|
||||
|
@ -18,38 +25,49 @@ pub struct FolderDeleteCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
#[arg(long, short)]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
impl FolderDeleteCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing folder delete command");
|
||||
info!("executing delete folder command");
|
||||
|
||||
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.");
|
||||
|
||||
if !prompt::bool(confirm, false)? {
|
||||
process::exit(0);
|
||||
};
|
||||
}
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
backend.delete_folder(&folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully deleted!"))?;
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
backend.delete_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully deleted!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, config::Config, folder::expunge::ExpungeFolder,
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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"
|
||||
|
@ -17,28 +25,36 @@ pub struct FolderExpungeCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FolderExpungeCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing folder expunge command");
|
||||
info!("executing expunge folder command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
backend.expunge_folder(&folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully expunged!"))?;
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
backend.expunge_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully expunged!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +1,73 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::CacheDisableFlag,
|
||||
config::TomlConfig,
|
||||
folder::Folders,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::TableMaxWidthFlag,
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
config::Config,
|
||||
{backend::feature::BackendFeatureSource, folder::list::ListFolders},
|
||||
};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{
|
||||
backend::BackendBuilder,
|
||||
config::{Folders, FoldersTable},
|
||||
},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use 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,
|
||||
|
||||
#[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 folder list command");
|
||||
info!("executing list folders command");
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let folders: Folders = backend.list_folders().await?.into();
|
||||
let toml_account_config = Arc::new(toml_account_config);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(folders),
|
||||
PrintTableOpts {
|
||||
format: &account_config.get_message_read_format(),
|
||||
max_width: self.table.max_width,
|
||||
let backend = BackendBuilder::new(
|
||||
toml_account_config.clone(),
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
mod create;
|
||||
mod add;
|
||||
mod delete;
|
||||
mod expunge;
|
||||
mod list;
|
||||
mod purge;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
create::FolderCreateCommand, 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(alias = "add", alias = "new")]
|
||||
Create(FolderCreateCommand),
|
||||
#[command(visible_alias = "create", alias = "new")]
|
||||
Add(FolderAddCommand),
|
||||
|
||||
#[command(alias = "lst")]
|
||||
List(FolderListCommand),
|
||||
|
@ -37,9 +38,10 @@ pub enum FolderSubcommand {
|
|||
}
|
||||
|
||||
impl FolderSubcommand {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Create(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Expunge(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Purge(cmd) => cmd.execute(printer, config).await,
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use std::{process, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use dialoguer::Confirm;
|
||||
use log::info;
|
||||
use std::process;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config, folder::purge::PurgeFolder};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||
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.
|
||||
|
@ -18,38 +22,49 @@ pub struct FolderPurgeCommand {
|
|||
#[command(flatten)]
|
||||
pub folder: FolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: CacheDisableFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
#[arg(long, short)]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
impl FolderPurgeCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing folder purge command");
|
||||
info!("executing purge folder command");
|
||||
|
||||
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 some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
backend.purge_folder(&folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully purged!"))?;
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
backend.purge_folder(folder).await?;
|
||||
|
||||
printer.out(format!("Folder {folder} successfully purged!\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
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>,
|
||||
}
|
||||
|
||||
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 anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folder {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl From<&email::folder::Folder> for Folder {
|
||||
fn from(folder: &email::folder::Folder) -> Self {
|
||||
Folder {
|
||||
name: folder.name.clone(),
|
||||
desc: folder.desc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Folder {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("DESC").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).blue())
|
||||
.cell(Cell::new(&self.desc).green())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folders(Vec<Folder>);
|
||||
|
||||
impl ops::Deref for Folders {
|
||||
type Target = Vec<Folder>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<email::folder::Folders> for Folders {
|
||||
fn from(folders: email::folder::Folders) -> Self {
|
||||
Folders(folders.iter().map(Folder::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Folders {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue