mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-20 08:13:39 +00:00
Compare commits
472 commits
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 | ||
![]() |
12e71a5ba8 | ||
![]() |
ce2b292d2b | ||
![]() |
be877f0b3e | ||
![]() |
131acd6230 | ||
![]() |
fa2c6c44bc | ||
![]() |
a59d1ca2c6 | ||
![]() |
eee17f9173 | ||
![]() |
bc36ce1255 | ||
![]() |
8d12528da6 | ||
![]() |
5ede53476b | ||
![]() |
cdf0a9a846 | ||
![]() |
2351cfdd28 | ||
![]() |
92a94c8ff1 | ||
![]() |
cd7cecca6e | ||
![]() |
3f2f691e85 | ||
![]() |
4ab81c0fe9 | ||
![]() |
9838854ec0 | ||
![]() |
9632508dc7 | ||
![]() |
77f5e590b8 | ||
![]() |
f398eb0d30 | ||
![]() |
c11f00d791 | ||
![]() |
73e1824a0d | ||
![]() |
6942c59097 | ||
![]() |
bcef05a54c | ||
![]() |
d542b2496e | ||
![]() |
d6bf407653 | ||
![]() |
7fccdd822a | ||
![]() |
a68d297366 | ||
![]() |
24bb6f10d7 | ||
![]() |
b623468d15 | ||
![]() |
3e3f111d3b | ||
![]() |
2e0ec913cf | ||
![]() |
8e05be7f77 | ||
![]() |
203ed2f917 | ||
![]() |
04e721d591 | ||
![]() |
ef3214f36f | ||
![]() |
fff11fbe20 | ||
![]() |
b28f12c367 | ||
![]() |
b8ef771614 | ||
![]() |
a47902af7d | ||
![]() |
5e1a03e3c1 | ||
![]() |
2c33dd2f9f | ||
![]() |
4a77253c1d | ||
![]() |
abe4c7f4ea | ||
![]() |
d2308221d7 | ||
![]() |
7a10a7fc25 | ||
![]() |
8b1a289f4d | ||
![]() |
ea9c28b9d7 | ||
![]() |
c54ada730b | ||
![]() |
f24a0475cc | ||
![]() |
a5cacb3f67 | ||
![]() |
41a2f02699 | ||
![]() |
fb8f356e8c | ||
![]() |
a0888067da | ||
![]() |
7629a66c9c | ||
![]() |
20f6973c55 | ||
![]() |
9f6a9a1333 | ||
![]() |
1f88b27468 | ||
![]() |
cec658aff4 | ||
![]() |
56fc31b367 | ||
![]() |
4b60379070 | ||
![]() |
606162452e | ||
![]() |
70fe936e3b | ||
![]() |
7ad1772c83 | ||
![]() |
f61a1f6669 | ||
![]() |
43c270bd44 | ||
![]() |
2b0f378a31 | ||
![]() |
176da9eeeb | ||
![]() |
0eed8f355d | ||
![]() |
183c0272cc | ||
![]() |
1ecceca1e6 | ||
![]() |
99ec7c6d97 | ||
![]() |
183aa2f306 | ||
![]() |
a8bd265181 | ||
![]() |
fff82498ba |
166 changed files with 9620 additions and 9493 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 }}
|
150
.github/workflows/release.yml
vendored
150
.github/workflows/release.yml
vendored
|
@ -1,150 +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_github:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: create_release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: linux
|
||||
os: ubuntu-latest
|
||||
- target: macos
|
||||
os: macos-latest
|
||||
- target: musl
|
||||
os: ubuntu-latest
|
||||
# TODO: put back when nix package .#windows is fixed
|
||||
# - target: windows
|
||||
# os: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v22
|
||||
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 release
|
||||
run: nix build .#${{ matrix.target }}
|
||||
- name: Compress executable
|
||||
run: |
|
||||
mkdir -p {man,completions}
|
||||
cp result/bin/himalaya* .
|
||||
nix run .#${{ matrix.target }} man ./man
|
||||
nix run .#${{ matrix.target }} completion bash > ./completions/himalaya.bash
|
||||
nix run .#${{ matrix.target }} completion elvish > ./completions/himalaya.elvish
|
||||
nix run .#${{ matrix.target }} completion fish > ./completions/himalaya.fish
|
||||
nix run .#${{ matrix.target }} completion powershell > ./completions/himalaya.powershell
|
||||
nix run .#${{ matrix.target }} completion zsh > ./completions/himalaya.zsh
|
||||
tar -czf himalaya.tgz himalaya* man completions
|
||||
zip -r himalaya.zip himalaya* man completions
|
||||
- name: Upload tar.gz release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya-${{ matrix.target }}.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya-${{ matrix.target }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
# TODO: remove me when nix package .#windows is fixed
|
||||
deploy_windows_github:
|
||||
runs-on: windows-latest
|
||||
needs: create_release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Builds release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release
|
||||
- name: Compress executable
|
||||
run: |
|
||||
mkdir man
|
||||
mkdir completions
|
||||
copy target/release/himalaya.exe .
|
||||
./himalaya.exe man ./man
|
||||
./himalaya.exe completion bash > ./completions/himalaya.bash
|
||||
./himalaya.exe completion elvish > ./completions/himalaya.elvish
|
||||
./himalaya.exe completion fish > ./completions/himalaya.fish
|
||||
./himalaya.exe completion powershell > ./completions/himalaya.powershell
|
||||
./himalaya.exe completion zsh > ./completions/himalaya.zsh
|
||||
tar -czf himalaya.tgz himalaya.exe man completions
|
||||
Compress-Archive -Path himalaya.exe,man,completions -DestinationPath himalaya.zip
|
||||
- name: Upload tar.gz release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: himalaya.tgz
|
||||
asset_name: himalaya-windows.tgz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload zip release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: himalaya.zip
|
||||
asset_name: himalaya-windows.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
deploy_crates:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create_release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v22
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-22.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
|
796
CHANGELOG.md
796
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -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).
|
||||
|
|
5003
Cargo.lock
generated
5003
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
101
Cargo.toml
101
Cargo.toml
|
@ -1,71 +1,70 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "CLI to manage your emails."
|
||||
version = "0.8.3"
|
||||
description = "CLI to manage emails"
|
||||
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/"
|
||||
documentation = "https://pimalaya.org/himalaya/"
|
||||
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 = ["imap-backend", "smtp-sender"]
|
||||
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"]
|
||||
|
||||
# backends
|
||||
imap-backend = ["pimalaya-email/imap-backend"]
|
||||
notmuch-backend = ["pimalaya-email/notmuch-backend"]
|
||||
|
||||
# senders
|
||||
smtp-sender = ["pimalaya-email/smtp-sender"]
|
||||
[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.0"
|
||||
atty = "0.2"
|
||||
chrono = "0.4.24"
|
||||
clap = "4.0"
|
||||
clap_complete = "4.0"
|
||||
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.0"
|
||||
email_address = "0.2.4"
|
||||
env_logger = "0.8"
|
||||
erased-serde = "0.3"
|
||||
indicatif = "0.17"
|
||||
log = "0.4"
|
||||
md5 = "0.7.0"
|
||||
once_cell = "1.16.0"
|
||||
pimalaya-email = "=0.14.0"
|
||||
pimalaya-keyring = "=0.0.5"
|
||||
pimalaya-oauth2 = "=0.0.3"
|
||||
pimalaya-process = "=0.0.5"
|
||||
pimalaya-secret = "=0.0.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shellexpand = "2.1"
|
||||
termcolor = "1.1"
|
||||
terminal_size = "0.1"
|
||||
color-eyre = "0.6"
|
||||
email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
|
||||
mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
|
||||
once_cell = "1.16"
|
||||
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 = [] }
|
||||
[target.'cfg(not(target_env = "musl"))'.dependencies]
|
||||
rusqlite = { version = "0.29", features = ["bundled"] }
|
||||
[patch.crates-io]
|
||||
imap-codec.git = "https://github.com/duesee/imap-codec"
|
||||
|
||||
[target.'cfg(not(target_env = "windows"))'.dependencies]
|
||||
coredump = "=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"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 soywod <clement.douin@posteo.net>
|
||||
Copyright (c) 2022-2024 soywod <clement.douin@posteo.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
737
README.md
737
README.md
|
@ -1,119 +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/
|
||||
```
|
||||
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
|
||||
```
|
||||
|
||||
CLI to manage your emails, based on the [pimalaya-email](https://sr.ht/~soywod/pimalaya/) library.
|
||||
|
||||

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

|
||||
|
||||
## Features
|
||||
|
||||
- [Folder listing]
|
||||
- [Envelopes listing], [searching] and [sorting]
|
||||
- [Email composition] based on `$EDITOR`
|
||||
- Email manipulation ([copy]/[move]/[delete])
|
||||
- [Multi-accounting]
|
||||
- [Account listing]
|
||||
- [Account synchronization] for offline usage
|
||||
- IMAP, Maildir and Notmuch support
|
||||
- IMAP IDLE mode for [real-time notifications]
|
||||
- PGP end-to-end encryption
|
||||
- [Completions] for various shells
|
||||
- JSON output
|
||||
- …
|
||||
- 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)
|
||||
|
||||
[Folder listing]: https://pimalaya.org/himalaya/cli/usage/folders/list.html
|
||||
[Envelopes listing]: https://pimalaya.org/himalaya/cli/usage/envelopes/list.html
|
||||
[searching]: https://pimalaya.org/himalaya/cli/usage/envelopes/search.html
|
||||
[sorting]: https://pimalaya.org/himalaya/cli/usage/envelopes/sort.html
|
||||
[Email composition]: https://pimalaya.org/himalaya/cli/usage/emails/write.html
|
||||
[copy]: https://pimalaya.org/himalaya/cli/usage/emails/copy.html
|
||||
[move]: https://pimalaya.org/himalaya/cli/usage/emails/move.html
|
||||
[delete]: https://pimalaya.org/himalaya/cli/usage/emails/delete.html
|
||||
[Multi-accounting]: https://pimalaya.org/himalaya/cli/configuration/index.html
|
||||
[Account listing]: https://pimalaya.org/himalaya/cli/usage/accounts/list.html
|
||||
[Account synchronization]: https://pimalaya.org/himalaya/cli/usage/accounts/synchronize.html
|
||||
[real-time notifications]: https://pimalaya.org/himalaya/cli/usage/notifications.html
|
||||
[Completions]: https://pimalaya.org/himalaya/cli/tips/completion.html
|
||||
*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.
|
||||
|
||||
*See the [documentation](https://pimalaya.org/himalaya/cli/installation/index.html) for other installation methods.*
|
||||
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
*Such binaries are built with the default cargo features. If you want to enable or disable a feature, please use another installation method.*
|
||||
</details>
|
||||
|
||||
<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/configuration/index.html).
|
||||
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 find a **bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
You can also manually edit your own configuration, from scratch:
|
||||
|
||||
If you have a **question**, please send an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
|
||||
- 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 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).
|
||||
<details>
|
||||
<summary>Proton Mail (Bridge)</summary>
|
||||
|
||||
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).
|
||||
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
|
||||
|
||||
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).
|
||||
- Id order may be reversed or shuffled, but envelopes will still be sorted by date.
|
||||
- SSL/TLS needs to be deactivated manually.
|
||||
- The password to use is the one generated by Proton Bridge, not the one from your Proton Mail account.
|
||||
|
||||
If you want to **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).
|
||||
```toml
|
||||
[accounts.proton]
|
||||
email = "example@proton.me"
|
||||
|
||||
## Credits
|
||||
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 = "*****"
|
||||
|
||||
[](https://nlnet.nl/project/Himalaya/index.html)
|
||||
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 = "*****"
|
||||
```
|
||||
|
||||
Special thanks to the [nlnet](https://nlnet.nl/project/Himalaya/index.html) foundation that helped Himalaya to receive financial support from the [NGI Assure](https://www.ngi.eu/ngi-projects/ngi-assure/) program of the European Commission in September, 2022.
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
* [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib)
|
||||
* [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
|
||||
* [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
|
||||
* [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage
|
||||
* [NeoMutt](https://neomutt.org/), an email terminal user interface
|
||||
* [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface
|
||||
* [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync
|
||||
* [rust-imap](https://github.com/jonhoo/rust-imap), a Rust IMAP library
|
||||
* [lettre](https://github.com/lettre/lettre), a Rust mailer library
|
||||
* [mailparse](https://github.com/staktrace/mailparse), a Rust MIME email parser.
|
||||
- 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/)
|
||||
|
||||
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/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,54 +1,651 @@
|
|||
display-name = "Display NAME"
|
||||
signature-delim = "~~"
|
||||
signature = "~/.signature"
|
||||
downloads-dir = "~/downloads"
|
||||
folder-listing-page-size = 12
|
||||
email-listing-page-size = 12
|
||||
email-reading-headers = ["From", "To"]
|
||||
email-reading-verify-cmd = "gpg --verify -q"
|
||||
email-reading-decrypt-cmd = "gpg -dq"
|
||||
email-writing-sign-cmd = "gpg -o - -saq"
|
||||
email-writing-encrypt-cmd = "gpg -o - -eqar <recipient>"
|
||||
################################################################################
|
||||
###[ Global configuration ]#####################################################
|
||||
################################################################################
|
||||
|
||||
[example]
|
||||
# Default display name for all accounts. It is used to build the full
|
||||
# email address of an account: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# Default signature for all accounts. The signature is put at the
|
||||
# bottom of all messages. It can be a path or a string. Supports TOML
|
||||
# multilines.
|
||||
#
|
||||
#signature = "/path/to/signature/file"
|
||||
#signature = """
|
||||
# Thanks you,
|
||||
# Regards
|
||||
#"""
|
||||
signature = "Regards,\n"
|
||||
|
||||
# Default signature delimiter for all accounts. It delimits the end of
|
||||
# the message body from the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# Default downloads directory path for all accounts. It is mostly used
|
||||
# for downloading attachments. Defaults to the system temporary
|
||||
# directory.
|
||||
#
|
||||
downloads-dir = "~/Downloads"
|
||||
|
||||
# Customizes the charset used to build the accounts listing
|
||||
# table. Defaults to markdown table style.
|
||||
#
|
||||
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
|
||||
#
|
||||
account.list.table.preset = "|| |-||| "
|
||||
|
||||
# Customizes the color of the NAME column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.name-color = "green"
|
||||
|
||||
# Customizes the color of the BACKENDS column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.backends-color = "blue"
|
||||
|
||||
# Customizes the color of the DEFAULT column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.default-color = "black"
|
||||
|
||||
################################################################################
|
||||
###[ Account configuration ]####################################################
|
||||
################################################################################
|
||||
|
||||
# The account name should be unique.
|
||||
#
|
||||
[accounts.example]
|
||||
|
||||
# Defaultness of the account. The current account will be used by
|
||||
# default in all commands.
|
||||
#
|
||||
default = true
|
||||
display-name = "Display NAME (gmail)"
|
||||
email = "display.name@gmail.local"
|
||||
|
||||
backend = "imap"
|
||||
imap-host = "imap.gmail.com"
|
||||
imap-login = "display.name@gmail.local"
|
||||
imap-auth = "passwd"
|
||||
imap-passwd.cmd = "pass show gmail"
|
||||
imap-port = 993
|
||||
imap-ssl = true
|
||||
imap-starttls = false
|
||||
imap-notify-cmd = """📫 "<sender>" "<subject>""""
|
||||
imap-notify-query = "NOT SEEN"
|
||||
imap-watch-cmds = ["echo \"received server changes!\""]
|
||||
# The email address associated to the current account.
|
||||
#
|
||||
email = "example@localhost"
|
||||
|
||||
sender = "smtp"
|
||||
smtp-host = "smtp.gmail.com"
|
||||
smtp-login = "display.name@gmail.local"
|
||||
smtp-auth = "passwd"
|
||||
smtp-passwd.cmd = "pass show piana/gmail"
|
||||
smtp-port = 465
|
||||
smtp-ssl = true
|
||||
smtp-starttls = false
|
||||
# The display name of the account. This and the email are used to
|
||||
# build the full email address: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
sync = true
|
||||
sync-dir = "/tmp/sync/gmail"
|
||||
sync-folders-strategy.include = ["INBOX"]
|
||||
# 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"
|
||||
|
||||
[example.folder-aliases]
|
||||
inbox = "INBOX"
|
||||
drafts = "[Gmail]/Drafts"
|
||||
sent = "[Gmail]/Sent Mail"
|
||||
trash = "[Gmail]/Trash"
|
||||
# Signature delimiter. It delimits the end of the message body from
|
||||
# the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
[example.email-hooks]
|
||||
pre-send = "echo $1"
|
||||
# Downloads directory path. It is mostly used for downloading
|
||||
# attachments. Defaults to the system temporary directory.
|
||||
#
|
||||
downloads-dir = "~/downloads"
|
||||
|
||||
[example.email-reading-format]
|
||||
type = "fixed"
|
||||
width = 64
|
||||
|
||||
|
||||
# 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"
|
||||
|
||||
# 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
|
||||
|
||||
# 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 = "|| |-||| "
|
||||
|
||||
# Customizes the character of the unseen flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.unseen-char = "*"
|
||||
|
||||
# Customizes the character of the replied flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.replied-char = "R"
|
||||
|
||||
# Customizes the character of the flagged flag of the envelope listing
|
||||
# table.
|
||||
#
|
||||
envelope.list.table.flagged-char = "!"
|
||||
|
||||
# Customizes the character of the attachment property of the envelope
|
||||
# listing table.
|
||||
#
|
||||
envelope.list.table.attachment-char = "@"
|
||||
|
||||
# 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
|
||||
|
||||
# Hook called just before sending a message. The command should take a
|
||||
# raw message as standard input (stdin) and returns the modified raw
|
||||
# message to the standard output (stdout).
|
||||
#
|
||||
message.send.pre-hook = "process-markdown.sh"
|
||||
|
||||
# Customizes the message deletion style. Message deletion can be
|
||||
# performed either by moving messages to the Trash folder or by adding
|
||||
# the Deleted flag to their respective envelopes.
|
||||
#
|
||||
#message.delete.style = "flag"
|
||||
message.delete.style = "folder"
|
||||
|
||||
|
||||
|
||||
# Defines how and where the signature should be displayed when writing
|
||||
# a new message.
|
||||
#
|
||||
#template.new.signature-style = "hidden"
|
||||
#template.new.signature-style = "attached"
|
||||
template.new.signature-style = "inlined"
|
||||
|
||||
# Defines the posting style when replying to a message.
|
||||
#
|
||||
# See <https://en.wikipedia.org/wiki/Posting_style>.
|
||||
#
|
||||
#template.reply.posting-style = "interleaved"
|
||||
#template.reply.posting-style = "bottom"
|
||||
template.reply.posting-style = "top"
|
||||
|
||||
# Defines how and where the signature should be displayed when
|
||||
# repyling to a message.
|
||||
#
|
||||
#template.reply.signature-style = "hidden"
|
||||
#template.reply.signature-style = "attached"
|
||||
#template.reply.signature-style = "above-quote"
|
||||
template.reply.signature-style = "below-quote"
|
||||
|
||||
# Defines the headline format put at the top of a quote when replying
|
||||
# to a message.
|
||||
#
|
||||
# Available placeholders: {senders}
|
||||
# See supported date formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
|
||||
#
|
||||
template.reply.quote-headline-fmt = "On %d/%m/%Y %H:%M, {senders} wrote:\n"
|
||||
|
||||
# Defines the posting style when forwarding a message.
|
||||
#
|
||||
# See <https://en.wikipedia.org/wiki/Posting_style>.
|
||||
#
|
||||
#template.forward.posting-style = "attached"
|
||||
template.forward.posting-style = "top"
|
||||
|
||||
# Defines how and where the signature should be displayed when
|
||||
# forwarding a message.
|
||||
#
|
||||
#template.forward.signature-style = "hidden"
|
||||
#template.forward.signature-style = "attached"
|
||||
template.forward.signature-style = "inlined"
|
||||
|
||||
# Defines the headline format put at the top of the quote when
|
||||
# forwarding a message.
|
||||
#
|
||||
template.forward.quote-headline = "-------- Forwarded Message --------\n"
|
||||
|
||||
|
||||
|
||||
# Enables PGP using GPG bindings. It requires the GPG lib to be
|
||||
# installed on the system, and the `pgp-gpg` cargo feature on.
|
||||
#
|
||||
#pgp.type = "gpg"
|
||||
|
||||
|
||||
|
||||
# Enables PGP using shell commands. A PGP client needs to be installed
|
||||
# on the system, like gpg. It also requires the `pgp-commands` cargo
|
||||
# feature.
|
||||
#
|
||||
#pgp.type = "commands"
|
||||
|
||||
# Defines the encrypt command. The special placeholder `<recipients>`
|
||||
# represents the list of recipients, formatted by
|
||||
# `pgp.encrypt-recipient-fmt`.
|
||||
#
|
||||
#pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
|
||||
|
||||
# Formats recipients for `pgp.encrypt-cmd`. The special placeholder
|
||||
# `<recipient>` is replaced by an actual recipient at runtime.
|
||||
#
|
||||
#pgp.encrypt-recipient-fmt = "--recipient <recipient>"
|
||||
|
||||
# Defines the separator used between formatted recipients
|
||||
# `pgp.encrypt-recipient-fmt`.
|
||||
#
|
||||
#pgp.encrypt-recipients-sep = " "
|
||||
|
||||
# Defines the decrypt command.
|
||||
#
|
||||
#pgp.decrypt-cmd = "gpg --decrypt --quiet"
|
||||
|
||||
# Defines the sign command.
|
||||
#
|
||||
#pgp.sign-cmd = "gpg --sign --quiet --armor"
|
||||
|
||||
# Defines the verify command.
|
||||
#
|
||||
#pgp.verify-cmd = "gpg --verify --quiet"
|
||||
|
||||
|
||||
|
||||
# Enables the native Rust implementation of PGP. It requires the
|
||||
# `pgp-native` cargo feature.
|
||||
#
|
||||
#pgp.type = "native"
|
||||
|
||||
# Defines where to find the PGP secret key.
|
||||
#
|
||||
#pgp.secret-key.path = "/path/to/secret.key"
|
||||
#pgp.secret-key.keyring = "my-pgp-secret-key"
|
||||
|
||||
# Defines how to retrieve the PGP secret key passphrase.
|
||||
#
|
||||
#pgp.secret-key-passphrase.raw = "p@assw0rd"
|
||||
#pgp.secret-key-passphrase.keyring = "my-pgp-passphrase"
|
||||
#pgp.secret-key-passphrase.cmd = "pass show pgp-passphrase"
|
||||
|
||||
# Enables the Web Key Discovery protocol to discover recipients'
|
||||
# public key based on their email address.
|
||||
#
|
||||
#pgp.wkd = true
|
||||
|
||||
# Enables public key servers discovery.
|
||||
#
|
||||
#pgp.key-servers = ["hkps://keys.openpgp.org", "hkps://keys.mailvelope.com"]
|
||||
|
||||
|
||||
|
||||
# Defines the IMAP backend as the default one for all features.
|
||||
#
|
||||
backend.type = "imap"
|
||||
|
||||
# IMAP server host name.
|
||||
#
|
||||
backend.host = "localhost"
|
||||
|
||||
# IMAP server port.
|
||||
#
|
||||
#backend.port = 143
|
||||
backend.port = 993
|
||||
|
||||
# IMAP server encryption.
|
||||
#
|
||||
#backend.encryption.type = "none"
|
||||
#backend.encryption.type = "start-tls"
|
||||
backend.encryption.type = "tls"
|
||||
|
||||
# IMAP server login.
|
||||
#
|
||||
backend.login = "example@localhost"
|
||||
|
||||
# IMAP server password authentication configuration.
|
||||
#
|
||||
backend.auth.type = "password"
|
||||
#
|
||||
# Password can be inlined (not recommended).
|
||||
#
|
||||
#backend.auth.raw = "p@assw0rd"
|
||||
#
|
||||
# Password can be stored inside your system global keyring (requires
|
||||
# the keyring cargo feature). You must run at least once `himalaya
|
||||
# account configure` to set up the password.
|
||||
#
|
||||
#backend.auth.keyring = "example-imap"
|
||||
#
|
||||
# Password can be retrieved from a shell command.
|
||||
#
|
||||
backend.auth.cmd = "pass show example-imap"
|
||||
|
||||
# IMAP server OAuth 2.0 authorization configuration.
|
||||
#
|
||||
#backend.auth.type = "oauth2"
|
||||
#
|
||||
# Client identifier issued to the client during the registration
|
||||
# process described in RFC6749.
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#backend.auth.client-id = "client-id"
|
||||
#
|
||||
# Client password issued to the client during the registration process
|
||||
# described in RFC6749.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-imap-client-secret".
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#backend.auth.client-secret.raw = "<raw-client-secret>"
|
||||
#backend.auth.client-secret.keyring = "example-imap-client-secret"
|
||||
#backend.auth.client-secret.cmd = "pass show example-imap-client-secret"
|
||||
#
|
||||
# Method for presenting an OAuth 2.0 bearer token to a service for
|
||||
# authentication.
|
||||
#
|
||||
#backend.auth.method = "oauthbearer"
|
||||
#backend.auth.method = "xoauth2"
|
||||
#
|
||||
# URL of the authorization server's authorization endpoint.
|
||||
#
|
||||
#backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
#
|
||||
# URL of the authorization server's token endpoint.
|
||||
#
|
||||
#backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
#
|
||||
# Access token returned by the token endpoint and used to access
|
||||
# protected resources. It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-imap-access-token".
|
||||
#
|
||||
#backend.auth.access-token.raw = "<raw-access-token>"
|
||||
#backend.auth.access-token.keyring = "example-imap-access-token"
|
||||
#backend.auth.access-token.cmd = "pass show example-imap-access-token"
|
||||
#
|
||||
# Refresh token used to obtain a new access token (if supported by the
|
||||
# authorization server). It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-imap-refresh-token".
|
||||
#
|
||||
#backend.auth.refresh-token.raw = "<raw-refresh-token>"
|
||||
#backend.auth.refresh-token.keyring = "example-imap-refresh-token"
|
||||
#backend.auth.refresh-token.cmd = "pass show example-imap-refresh-token"
|
||||
#
|
||||
# Enable the protection, as defined in RFC7636.
|
||||
#
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc7636>.
|
||||
#
|
||||
#backend.auth.pkce = true
|
||||
#
|
||||
# Access token scope(s), as defined by the authorization server.
|
||||
#
|
||||
#backend.auth.scope = "unique scope"
|
||||
#backend.auth.scopes = ["multiple", "scopes"]
|
||||
#
|
||||
# URL scheme of the redirect server.
|
||||
# Defaults to http.
|
||||
#
|
||||
#backend.auth.redirect-scheme = "http"
|
||||
#
|
||||
# Host name of the redirect server.
|
||||
# Defaults to localhost.
|
||||
#
|
||||
#backend.auth.redirect-host = "localhost"
|
||||
#
|
||||
# Port of the redirect server.
|
||||
# Defaults to the first available one.
|
||||
#
|
||||
#backend.auth.redirect-port = 9999
|
||||
|
||||
|
||||
|
||||
# Defines the Maildir backend as the default one for all features.
|
||||
#
|
||||
#backend.type = "maildir"
|
||||
|
||||
# The Maildir root directory. The path should point to the root level
|
||||
# of the Maildir directory.
|
||||
#
|
||||
#backend.root-dir = "~/.Mail/example"
|
||||
|
||||
# Does the Maildir folder follows the Maildir++ standard?
|
||||
#
|
||||
# See <https://en.wikipedia.org/wiki/Maildir#Maildir++>.
|
||||
#
|
||||
#backend.maildirpp = false
|
||||
|
||||
|
||||
|
||||
# Defines the Notmuch backend as the default one for all features.
|
||||
#
|
||||
#backend.type = "notmuch"
|
||||
|
||||
# The path to the Notmuch database. The path should point to the root
|
||||
# directory containing the Notmuch database (usually the root Maildir
|
||||
# directory).
|
||||
#
|
||||
#backend.db-path = "~/.Mail/example"
|
||||
|
||||
# Overrides the default path to the Maildir folder.
|
||||
#
|
||||
#backend.maildir-path = "~/.Mail/example"
|
||||
|
||||
# Overrides the default Notmuch configuration file path.
|
||||
#
|
||||
#backend.config-path = "~/.notmuchrc"
|
||||
|
||||
# Override the default Notmuch profile name.
|
||||
#
|
||||
#backend.profile = "example"
|
||||
|
||||
|
||||
|
||||
# Defines the SMTP backend for the message sending feature.
|
||||
#
|
||||
message.send.backend.type = "smtp"
|
||||
|
||||
# SMTP server host name.
|
||||
#
|
||||
message.send.backend.host = "localhost"
|
||||
|
||||
# SMTP server port.
|
||||
#
|
||||
#message.send.backend.port = 25
|
||||
#message.send.backend.port = 465
|
||||
message.send.backend.port = 587
|
||||
|
||||
# SMTP server encryption.
|
||||
#
|
||||
#message.send.backend.encryption.type = "none"
|
||||
#message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.encryption.type = "tls"
|
||||
|
||||
# SMTP server login.
|
||||
#
|
||||
message.send.backend.login = "example@localhost"
|
||||
|
||||
# SMTP server password authentication configuration.
|
||||
#
|
||||
message.send.backend.auth.type = "password"
|
||||
#
|
||||
# Password can be inlined (not recommended).
|
||||
#
|
||||
#message.send.backend.auth.raw = "p@assw0rd"
|
||||
#
|
||||
# Password can be stored inside your system global keyring (requires
|
||||
# the keyring cargo feature). You must run at least once `himalaya
|
||||
# account configure` to set up the password.
|
||||
#
|
||||
#message.send.backend.auth.keyring = "example-smtp"
|
||||
#
|
||||
# Password can be retrieved from a shell command.
|
||||
#
|
||||
message.send.backend.auth.cmd = "pass show example-smtp"
|
||||
|
||||
# SMTP server OAuth 2.0 authorization configuration.
|
||||
#
|
||||
#message.send.backend.auth.type = "oauth2"
|
||||
#
|
||||
# Client identifier issued to the client during the registration
|
||||
# process described in RFC6749.
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#message.send.backend.auth.client-id = "client-id"
|
||||
#
|
||||
# Client password issued to the client during the registration process
|
||||
# described in RFC6749.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-smtp-client-secret".
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
|
||||
#
|
||||
#message.send.backend.auth.client-secret.raw = "<raw-client-secret>"
|
||||
#message.send.backend.auth.client-secret.keyring = "example-smtp-client-secret"
|
||||
#message.send.backend.auth.client-secret.cmd = "pass show example-smtp-client-secret"
|
||||
#
|
||||
# Method for presenting an OAuth 2.0 bearer token to a service for
|
||||
# authentication.
|
||||
#
|
||||
#message.send.backend.auth.method = "oauthbearer"
|
||||
#message.send.backend.auth.method = "xoauth2"
|
||||
#
|
||||
# URL of the authorization server's authorization endpoint.
|
||||
#
|
||||
#message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
#
|
||||
# URL of the authorization server's token endpoint.
|
||||
#
|
||||
#message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
#
|
||||
# Access token returned by the token endpoint and used to access
|
||||
# protected resources. It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-smtp-access-token".
|
||||
#
|
||||
#message.send.backend.auth.access-token.raw = "<raw-access-token>"
|
||||
#message.send.backend.auth.access-token.keyring = "example-smtp-access-token"
|
||||
#message.send.backend.auth.access-token.cmd = "pass show example-smtp-access-token"
|
||||
#
|
||||
# Refresh token used to obtain a new access token (if supported by the
|
||||
# authorization server). It is recommended to use the keyring variant,
|
||||
# as it will refresh automatically.
|
||||
#
|
||||
# Defaults to keyring "<account-name>-smtp-refresh-token".
|
||||
#
|
||||
#message.send.backend.auth.refresh-token.raw = "<raw-refresh-token>"
|
||||
#message.send.backend.auth.refresh-token.keyring = "example-smtp-refresh-token"
|
||||
#message.send.backend.auth.refresh-token.cmd = "pass show example-smtp-refresh-token"
|
||||
#
|
||||
# Enable the protection, as defined in RFC7636.
|
||||
#
|
||||
# See <https://datatracker.ietf.org/doc/html/rfc7636>.
|
||||
#
|
||||
#message.send.backend.auth.pkce = true
|
||||
#
|
||||
# Access token scope(s), as defined by the authorization server.
|
||||
#
|
||||
#message.send.backend.auth.scope = "unique scope"
|
||||
#message.send.backend.auth.scopes = ["multiple", "scopes"]
|
||||
#
|
||||
# URL scheme of the redirect server.
|
||||
# Defaults to http.
|
||||
#
|
||||
#message.send.backend.auth.redirect-scheme = "http"
|
||||
#
|
||||
# Host name of the redirect server.
|
||||
# Defaults to localhost.
|
||||
#
|
||||
#message.send.backend.auth.redirect-host = "localhost"
|
||||
#
|
||||
# Port of the redirect server.
|
||||
# Defaults to the first available one.
|
||||
#
|
||||
#message.send.backend.auth.redirect-port = 9999
|
||||
|
||||
|
||||
|
||||
# Defines the Sendmail backend for the message sending feature.
|
||||
#
|
||||
#message.send.backend.type = "sendmail"
|
||||
|
||||
# Customizes the sendmail shell command.
|
||||
#
|
||||
#message.send.backend.cmd = "/usr/bin/sendmail"
|
||||
|
|
41
default.nix
41
default.nix
|
@ -1,12 +1,29 @@
|
|||
# This file exists for legacy Nix installs (nix-build & nix-env)
|
||||
# https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix
|
||||
# You generally do *not* have to modify this ever.
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).defaultNix
|
||||
{
|
||||
pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"),
|
||||
...
|
||||
}@args:
|
||||
|
||||
pimalaya.mkDefault (
|
||||
{
|
||||
src = ./.;
|
||||
version = "1.0.0";
|
||||
mkPackage = (
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
rustPlatform,
|
||||
defaultFeatures,
|
||||
features,
|
||||
}:
|
||||
pkgs.callPackage ./package.nix {
|
||||
inherit lib rustPlatform;
|
||||
apple-sdk = pkgs.apple-sdk;
|
||||
installShellCompletions = false;
|
||||
installManPages = false;
|
||||
buildNoDefaultFeatures = !defaultFeatures;
|
||||
buildFeatures = lib.splitString "," features;
|
||||
}
|
||||
);
|
||||
}
|
||||
// removeAttrs args [ "pimalaya" ]
|
||||
)
|
||||
|
|
136
flake.lock
generated
136
flake.lock
generated
|
@ -8,127 +8,66 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1686032467,
|
||||
"narHash": "sha256-KUCS237H0G1QGx5ehhEmh5yKtcDGCxvVXVtz8xEDAKE=",
|
||||
"owner": "nix-community",
|
||||
"lastModified": 1732405626,
|
||||
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"rev": "1a3e0f661119a7435099b118912d65bdbbf3bb11",
|
||||
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1685518550,
|
||||
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1660459072,
|
||||
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1679567394,
|
||||
"narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "88cd22380154a2c36799fe8098888f0f59861a15",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1685883127,
|
||||
"narHash": "sha256-zPDaPNrAtBnO24rNqjHLINHsqTdRbgWy1c/TL3EdwlM=",
|
||||
"lastModified": 1736437047,
|
||||
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d4a9ff82fc18723219b60c66fb2ccb0734c460eb",
|
||||
"rev": "f17b95775191ea44bc426831235d87affb10faba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-22.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": 1685984106,
|
||||
"narHash": "sha256-dOEuU1AuASOWdXT/SbVpD8uX7JjiW3lCp08SbviHuww=",
|
||||
"lastModified": 1732050317,
|
||||
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "d42d55feaafa71e14521bbfe6e7011fbb41980f0",
|
||||
"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",
|
||||
|
|
138
flake.nix
138
flake.nix
|
@ -1,134 +1,26 @@
|
|||
{
|
||||
description = "CLI to manage your emails.";
|
||||
description = "CLI to manage emails";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
gitignore = {
|
||||
url = "github:hercules-ci/gitignore.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
# 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 {
|
||||
buildInputs = with pkgs; [
|
||||
# Nix env
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
|
||||
# Rust env
|
||||
rust-toolchain
|
||||
|
||||
# notmuch
|
||||
notmuch
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
mkPackage = pkgs: buildPlatform: targetPlatform: package:
|
||||
let
|
||||
toolchain = mkToolchain.fromTarget {
|
||||
inherit pkgs buildPlatform targetPlatform;
|
||||
};
|
||||
naersk' = naersk.lib.${buildPlatform}.override {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
package' = {
|
||||
name = "himalaya";
|
||||
src = gitignoreSource ./.;
|
||||
overrideMain = _: {
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/applications/
|
||||
cp assets/himalaya.desktop $out/share/applications/
|
||||
'';
|
||||
};
|
||||
doCheck = 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; };
|
||||
mkPackageWithTarget = mkPackage pkgs buildPlatform;
|
||||
defaultPackage = mkPackage pkgs buildPlatform null { };
|
||||
in
|
||||
{
|
||||
default = defaultPackage;
|
||||
linux = defaultPackage;
|
||||
macos = defaultPackage;
|
||||
musl = mkPackageWithTarget "x86_64-unknown-linux-musl" (with pkgs.pkgsStatic; {
|
||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||
SQLITE3_STATIC = 1;
|
||||
SQLITE3_LIB_DIR = "${sqlite.out}/lib";
|
||||
hardeningDisable = [ "all" ];
|
||||
});
|
||||
# FIXME: package does not build, assembler messages: unknown
|
||||
# pseudo-op…
|
||||
windows = mkPackageWithTarget "x86_64-pc-windows-gnu" {
|
||||
strictDeps = true;
|
||||
depsBuildBuild = with pkgs.pkgsCross.mingwW64; [
|
||||
stdenv.cc
|
||||
windows.pthreads
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
mkApp = drv: flake-utils.lib.mkApp {
|
||||
inherit drv;
|
||||
name = "himalaya";
|
||||
};
|
||||
|
||||
mkApps = buildPlatform: {
|
||||
default = mkApp self.packages.${buildPlatform}.default;
|
||||
linux = mkApp self.packages.${buildPlatform}.linux;
|
||||
macos = mkApp self.packages.${buildPlatform}.macos;
|
||||
musl = mkApp self.packages.${buildPlatform}.musl;
|
||||
windows =
|
||||
let
|
||||
pkgs = import nixpkgs { system = buildPlatform; };
|
||||
wine = pkgs.wine.override { wineBuild = "wine64"; };
|
||||
himalaya = self.packages.${buildPlatform}.windows;
|
||||
app = pkgs.writeShellScriptBin "himalaya" ''
|
||||
export WINEPREFIX="$(mktemp -d)"
|
||||
${wine}/bin/wine64 ${himalaya}/bin/himalaya.exe $@
|
||||
'';
|
||||
in
|
||||
mkApp app;
|
||||
};
|
||||
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: {
|
||||
devShells = mkDevShells system;
|
||||
packages = mkPackages system;
|
||||
apps = mkApps system;
|
||||
});
|
||||
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 = "ks0nMEGGXKrHnfv4Fku+vhQ7gx76ruv6Ij4fKZR3l78=";
|
||||
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
src/account/arg/mod.rs
Normal file
1
src/account/arg/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod name;
|
37
src/account/arg/name.rs
Normal file
37
src/account/arg/name.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use clap::Parser;
|
||||
|
||||
/// The account name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountNameArg {
|
||||
/// The name of the account.
|
||||
///
|
||||
/// An account name corresponds to an entry in the table at the
|
||||
/// root level of your TOML configuration file.
|
||||
#[arg(name = "account_name", value_name = "ACCOUNT")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The optional account name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct OptionalAccountNameArg {
|
||||
/// The name of the account.
|
||||
///
|
||||
/// An account name corresponds to an entry in the table at the
|
||||
/// root level of your TOML configuration file.
|
||||
///
|
||||
/// If omitted, the account marked as default will be used.
|
||||
#[arg(name = "account_name", value_name = "ACCOUNT")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// The account name flag parser.
|
||||
#[derive(Debug, Default, Parser)]
|
||||
pub struct AccountNameFlag {
|
||||
/// Override the default account.
|
||||
///
|
||||
/// An account name corresponds to an entry in the table at the
|
||||
/// root level of your TOML configuration file.
|
||||
#[arg(long = "account", short = 'a')]
|
||||
#[arg(name = "account_name", value_name = "NAME")]
|
||||
pub name: Option<String>,
|
||||
}
|
52
src/account/command/configure.rs
Normal file
52
src/account/command/configure.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
|
||||
|
||||
/// Configure the given account.
|
||||
///
|
||||
/// This command allows you to configure an existing account or to
|
||||
/// create a new one, using the wizard. The `wizard` cargo feature is
|
||||
/// required.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountConfigureCommand {
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameArg,
|
||||
}
|
||||
|
||||
impl AccountConfigureCommand {
|
||||
#[cfg(feature = "wizard")]
|
||||
pub async fn execute(
|
||||
self,
|
||||
mut config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
|
||||
info!("executing account configure command");
|
||||
|
||||
let path = match config_path {
|
||||
Some(path) => path.clone(),
|
||||
None => TomlConfig::default_path()?,
|
||||
};
|
||||
|
||||
let account_name = Some(self.account.name.as_str());
|
||||
|
||||
let account_config = config
|
||||
.accounts
|
||||
.remove(&self.account.name)
|
||||
.unwrap_or_default();
|
||||
|
||||
wizard::edit(path, config, account_name, account_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
41
src/account/command/list.rs
Normal file
41
src/account/command/list.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::{
|
||||
himalaya::config::{Accounts, AccountsTable},
|
||||
terminal::cli::printer::Printer,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
/// List all existing accounts.
|
||||
///
|
||||
/// This command lists all the accounts defined in your TOML
|
||||
/// configuration file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AccountListCommand {
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width, in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
}
|
||||
|
||||
impl AccountListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing list accounts command");
|
||||
|
||||
let accounts = Accounts::from(config.accounts.iter());
|
||||
let table = AccountsTable::from(accounts)
|
||||
.with_some_width(self.table_max_width)
|
||||
.with_some_preset(config.account_list_table_preset())
|
||||
.with_some_name_color(config.account_list_table_name_color())
|
||||
.with_some_backends_color(config.account_list_table_backends_color())
|
||||
.with_some_default_color(config.account_list_table_default_color());
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
41
src/account/command/mod.rs
Normal file
41
src/account/command/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
mod configure;
|
||||
mod doctor;
|
||||
mod list;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
|
||||
};
|
||||
|
||||
/// Configure, list and diagnose your accounts.
|
||||
///
|
||||
/// 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 {
|
||||
Configure(AccountConfigureCommand),
|
||||
Doctor(AccountDoctorCommand),
|
||||
List(AccountListCommand),
|
||||
}
|
||||
|
||||
impl AccountSubcommand {
|
||||
pub async fn execute(
|
||||
self,
|
||||
printer: &mut impl Printer,
|
||||
config: TomlConfig,
|
||||
config_path: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Self::Configure(cmd) => cmd.execute(config, config_path).await,
|
||||
Self::Doctor(cmd) => cmd.execute(&config).await,
|
||||
Self::List(cmd) => cmd.execute(printer, &config).await,
|
||||
}
|
||||
}
|
||||
}
|
3
src/account/config.rs
Normal file
3
src/account/config.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
|
||||
|
||||
pub type TomlAccountConfig = HimalayaTomlAccountConfig;
|
3
src/account/mod.rs
Normal file
3
src/account/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod config;
|
24
src/cache/args.rs
vendored
24
src/cache/args.rs
vendored
|
@ -1,24 +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 arg() -> Arg {
|
||||
Arg::new(ARG_DISABLE_CACHE)
|
||||
.help("Disable any sort of cache")
|
||||
.long_help(
|
||||
"Disable any sort of cache. The action depends on
|
||||
the command it applies on.",
|
||||
)
|
||||
.long("disable-cache")
|
||||
.global(true)
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the disable cache flag parser.
|
||||
pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DISABLE_CACHE)
|
||||
}
|
192
src/cache/id_mapper.rs
vendored
192
src/cache/id_mapper.rs
vendored
|
@ -1,192 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, trace};
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use pimalaya_email::backend::ImapBackend;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use pimalaya_email::backend::NotmuchBackend;
|
||||
use pimalaya_email::{
|
||||
account::AccountConfig,
|
||||
backend::{Backend, MaildirBackend},
|
||||
};
|
||||
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(
|
||||
backend: &dyn Backend,
|
||||
account_config: &AccountConfig,
|
||||
folder: &str,
|
||||
) -> Result<Self> {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
if backend.as_any().is::<ImapBackend>() {
|
||||
return Ok(IdMapper::Dummy);
|
||||
}
|
||||
|
||||
let mut db_path = PathBuf::new();
|
||||
|
||||
if let Some(backend) = backend.as_any().downcast_ref::<MaildirBackend>() {
|
||||
db_path = Self::find_closest_db_path(backend.path())
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
if let Some(backend) = backend.as_any().downcast_ref::<NotmuchBackend>() {
|
||||
db_path = Self::find_closest_db_path(backend.path())
|
||||
}
|
||||
|
||||
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 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: AsRef<str>,
|
||||
{
|
||||
let alias = alias.as_ref();
|
||||
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: AsRef<str>,
|
||||
I: IntoIterator<Item = A>,
|
||||
{
|
||||
aliases
|
||||
.into_iter()
|
||||
.map(|alias| self.get_id(alias))
|
||||
.collect()
|
||||
}
|
||||
}
|
4
src/cache/mod.rs
vendored
4
src/cache/mod.rs
vendored
|
@ -1,4 +0,0 @@
|
|||
pub mod args;
|
||||
mod id_mapper;
|
||||
|
||||
pub use id_mapper::IdMapper;
|
156
src/cli.rs
Normal file
156
src/cli.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
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::TomlConfig,
|
||||
envelope::command::EnvelopeSubcommand,
|
||||
flag::command::FlagSubcommand,
|
||||
folder::command::FolderSubcommand,
|
||||
manual::command::ManualGenerateCommand,
|
||||
message::{
|
||||
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
|
||||
template::command::TemplateSubcommand,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[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: Option<HimalayaCommand>,
|
||||
|
||||
/// Override the default configuration file path.
|
||||
///
|
||||
/// The given paths are shell-expanded then canonicalized (if
|
||||
/// applicable). If the first path does not point to a valid file,
|
||||
/// the wizard will propose to assist you in the creation of the
|
||||
/// configuration file. Other paths are merged with the first one,
|
||||
/// which allows you to separate your public config from your
|
||||
/// private(s) one(s).
|
||||
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
|
||||
#[arg(value_name = "PATH", value_parser = path_parser)]
|
||||
pub config_paths: Vec<PathBuf>,
|
||||
|
||||
/// Customize the output format.
|
||||
///
|
||||
/// The output format determine how to display commands output to
|
||||
/// the terminal.
|
||||
///
|
||||
/// The possible values are:
|
||||
///
|
||||
/// - json: output will be in a form of a JSON-compatible object
|
||||
///
|
||||
/// - plain: output will be in a form of either a plain text or
|
||||
/// table, depending on the command
|
||||
#[arg(long, short, global = true)]
|
||||
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
|
||||
pub output: OutputFmt,
|
||||
|
||||
/// Enable logs with spantrace.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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)]
|
||||
pub enum HimalayaCommand {
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "accounts")]
|
||||
Account(AccountSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(visible_alias = "mailbox", aliases = ["mailboxes", "mboxes", "mbox"])]
|
||||
#[command(alias = "folders")]
|
||||
Folder(FolderSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "envelopes")]
|
||||
Envelope(EnvelopeSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "flags")]
|
||||
Flag(FlagSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "messages", alias = "msgs", alias = "msg")]
|
||||
Message(MessageSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "attachments")]
|
||||
Attachment(AttachmentSubcommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "templates", alias = "tpls", alias = "tpl")]
|
||||
Template(TemplateSubcommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(alias = "manuals", alias = "mans")]
|
||||
Manual(ManualGenerateCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(alias = "completions")]
|
||||
Completion(CompletionGenerateCommand),
|
||||
}
|
||||
|
||||
impl HimalayaCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
|
||||
match self {
|
||||
Self::Account(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, config, config_paths.first()).await
|
||||
}
|
||||
Self::Folder(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Envelope(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Flag(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Message(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Attachment(cmd) => {
|
||||
let config = TomlConfig::from_paths_or_default(config_paths).await?;
|
||||
cmd.execute(printer, &config).await
|
||||
}
|
||||
Self::Template(cmd) => {
|
||||
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().await,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
//! Module related to completion CLI.
|
||||
//!
|
||||
//! This module provides subcommands and a command matcher related to completion.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{value_parser, Arg, ArgMatches, Command};
|
||||
use clap_complete::Shell;
|
||||
use log::debug;
|
||||
|
||||
const ARG_SHELL: &str = "shell";
|
||||
const CMD_COMPLETION: &str = "completion";
|
||||
|
||||
type SomeShell = Shell;
|
||||
|
||||
/// Completion commands.
|
||||
pub enum Cmd {
|
||||
/// Generate completion script for the given shell.
|
||||
Generate(SomeShell),
|
||||
}
|
||||
|
||||
/// Completion command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
|
||||
let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
|
||||
debug!("shell: {:?}", shell);
|
||||
return Ok(Some(Cmd::Generate(shell)));
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Completion subcommands.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_COMPLETION)
|
||||
.about("Generates the completion script for the given shell")
|
||||
.args(&[Arg::new(ARG_SHELL)
|
||||
.value_parser(value_parser!(Shell))
|
||||
.required(true)])
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//! Module related to completion handling.
|
||||
//!
|
||||
//! This module gathers all completion commands.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use clap_complete::Shell;
|
||||
use std::io::stdout;
|
||||
|
||||
/// Generates completion script from the given [`clap::App`] for the given shell slice.
|
||||
pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> {
|
||||
let name = cmd.get_name().to_string();
|
||||
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
|
||||
Ok(())
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
//! Module related to shell completion.
|
||||
//!
|
||||
//! This module allows users to generate autocompletion scripts for
|
||||
//! their shells. You can see the list of available shells directly on
|
||||
//! the clap's [docs.rs](https://docs.rs/clap/2.33.3/clap/enum.Shell.html).
|
||||
|
||||
pub mod args;
|
||||
pub mod handlers;
|
32
src/completion/command.rs
Normal file
32
src/completion/command.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use std::io;
|
||||
|
||||
use clap::{value_parser, CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
use color_eyre::Result;
|
||||
use tracing::info;
|
||||
|
||||
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
|
||||
/// to write it to a file, just use unix redirection.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CompletionGenerateCommand {
|
||||
/// Shell for which completion script should be generated for.
|
||||
#[arg(value_parser = value_parser!(Shell))]
|
||||
pub shell: Shell,
|
||||
}
|
||||
|
||||
impl CompletionGenerateCommand {
|
||||
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());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
1
src/completion/mod.rs
Normal file
1
src/completion/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod command;
|
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,21 +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 arg() -> Arg {
|
||||
Arg::new(ARG_CONFIG)
|
||||
.help("Set a custom configuration file path")
|
||||
.long("config")
|
||||
.short('c')
|
||||
.global(true)
|
||||
.value_name("PATH")
|
||||
}
|
||||
|
||||
/// Represents the config file path argument parser.
|
||||
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
|
||||
}
|
|
@ -1,620 +0,0 @@
|
|||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dialoguer::Confirm;
|
||||
use dirs::{config_dir, home_dir};
|
||||
use log::{debug, trace};
|
||||
use pimalaya_email::{
|
||||
account::AccountConfig,
|
||||
email::{EmailHooks, EmailTextPlainFormat},
|
||||
};
|
||||
use pimalaya_process::Cmd;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fs, path::PathBuf, process};
|
||||
use toml;
|
||||
|
||||
use crate::{
|
||||
account::DeserializedAccountConfig,
|
||||
config::{prelude::*, wizard},
|
||||
wizard_prompt, wizard_warn,
|
||||
};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedConfig {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
pub folder_listing_page_size: Option<usize>,
|
||||
pub folder_aliases: Option<HashMap<String, String>>,
|
||||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_listing_datetime_fmt: Option<String>,
|
||||
pub email_listing_datetime_local_tz: Option<bool>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "EmailTextPlainFormatDef",
|
||||
skip_serializing_if = "EmailTextPlainFormat::is_default"
|
||||
)]
|
||||
pub email_reading_format: EmailTextPlainFormat,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_reading_verify_cmd: Option<Cmd>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_reading_decrypt_cmd: Option<Cmd>,
|
||||
pub email_writing_headers: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_writing_sign_cmd: Option<Cmd>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_writing_encrypt_cmd: Option<Cmd>,
|
||||
pub email_sending_save_copy: Option<bool>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "EmailHooksDef",
|
||||
skip_serializing_if = "EmailHooks::is_empty"
|
||||
)]
|
||||
pub email_hooks: EmailHooks,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, DeserializedAccountConfig>,
|
||||
}
|
||||
|
||||
impl DeserializedConfig {
|
||||
/// Tries to create a config from an optional path.
|
||||
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
|
||||
debug!("path: {:?}", path);
|
||||
|
||||
let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) {
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
toml::from_str(&content).context("cannot parse config file")?
|
||||
} else {
|
||||
wizard_warn!("Himalaya could not find an already existing configuration file.");
|
||||
|
||||
if !Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to create one with the wizard?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
wizard::configure()?
|
||||
};
|
||||
|
||||
if config.accounts.is_empty() {
|
||||
return Err(anyhow!("config file must contain at least one account"));
|
||||
}
|
||||
|
||||
trace!("config: {:#?}", config);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Tries to return a config path from a few default settings.
|
||||
///
|
||||
/// 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"`
|
||||
///
|
||||
/// Returns `Some(path)` if the path exists, otherwise `None`.
|
||||
pub fn path() -> Option<PathBuf> {
|
||||
config_dir()
|
||||
.map(|p| p.join("himalaya").join("config.toml"))
|
||||
.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 to_account_config(&self, account_name: Option<&str>) -> Result<AccountConfig> {
|
||||
let (account_name, deserialized_account_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(name, account)| {
|
||||
account
|
||||
.default
|
||||
.filter(|default| *default == true)
|
||||
.map(|_| (name.clone(), account))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_string(), account))
|
||||
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
|
||||
}?;
|
||||
|
||||
Ok(deserialized_account_config.to_account_config(account_name, self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pimalaya_email::{
|
||||
account::PasswdConfig,
|
||||
backend::{BackendConfig, MaildirConfig},
|
||||
sender::{SenderConfig, SendmailConfig},
|
||||
};
|
||||
use pimalaya_secret::Secret;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use pimalaya_email::backend::NotmuchConfig;
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use pimalaya_email::backend::{ImapAuthConfig, ImapConfig};
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
use pimalaya_email::sender::{SmtpAuthConfig, SmtpConfig};
|
||||
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_config(config: &str) -> Result<DeserializedConfig> {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
write!(file, "{}", config).unwrap();
|
||||
DeserializedConfig::from_opt_path(file.into_temp_path().to_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_config() {
|
||||
let config = make_config("");
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"config file must contain at least one account"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_email_field() {
|
||||
let config = make_config("[account]");
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `email`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `backend`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_invalid_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"bad\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("unknown variant `bad`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imap_account_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-host`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-port`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-login`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_passwd_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `imap-auth`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_maildir_missing_root_dir_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `maildir-root-dir`"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[test]
|
||||
fn account_backend_notmuch_missing_db_path_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `notmuch-db-path`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `sender`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_invalid_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"bad\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-host`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-port`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-login`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_auth_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `smtp-auth`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_sendmail_sender_missing_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.contains("missing field `sendmail-cmd`"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
#[test]
|
||||
fn account_smtp_sender_minimum_config() {
|
||||
use pimalaya_email::sender::SenderConfig;
|
||||
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"
|
||||
smtp-auth = \"passwd\"
|
||||
smtp-passwd = { cmd = \"echo password\" }",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Smtp(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 25,
|
||||
login: "login".into(),
|
||||
auth: SmtpAuthConfig::Passwd(PasswdConfig {
|
||||
passwd: Secret::new_cmd(String::from("echo password"))
|
||||
}),
|
||||
..SmtpConfig::default()
|
||||
}),
|
||||
..DeserializedAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_sendmail_sender_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"
|
||||
sendmail-cmd = \"echo send\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||
cmd: Cmd::from("echo send")
|
||||
}),
|
||||
..DeserializedAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"
|
||||
imap-auth = \"passwd\"
|
||||
imap-passwd = { cmd = \"echo password\" }",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Imap(ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 993,
|
||||
login: "login".into(),
|
||||
auth: ImapAuthConfig::Passwd(PasswdConfig {
|
||||
passwd: Secret::new_cmd(String::from("echo password"))
|
||||
}),
|
||||
..ImapConfig::default()
|
||||
}),
|
||||
..DeserializedAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn account_backend_maildir_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"
|
||||
maildir-root-dir = \"/tmp/maildir\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Maildir(MaildirConfig {
|
||||
root_dir: "/tmp/maildir".into(),
|
||||
}),
|
||||
..DeserializedAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[test]
|
||||
fn account_backend_notmuch_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"
|
||||
notmuch-db-path = \"/tmp/notmuch.db\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
backend: BackendConfig::Notmuch(NotmuchConfig {
|
||||
db_path: "/tmp/notmuch.db".into(),
|
||||
}),
|
||||
..DeserializedAccountConfig::default()
|
||||
}
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod prelude;
|
||||
pub mod wizard;
|
||||
|
||||
pub use config::*;
|
|
@ -1,389 +0,0 @@
|
|||
#[cfg(feature = "notmuch-backend")]
|
||||
use pimalaya_email::backend::NotmuchConfig;
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use pimalaya_email::backend::{ImapAuthConfig, ImapConfig};
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
use pimalaya_email::sender::{SmtpAuthConfig, SmtpConfig};
|
||||
use pimalaya_email::{
|
||||
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
|
||||
backend::{BackendConfig, MaildirConfig},
|
||||
email::{EmailHooks, EmailTextPlainFormat},
|
||||
folder::sync::FolderSyncStrategy,
|
||||
sender::{SenderConfig, SendmailConfig},
|
||||
};
|
||||
use pimalaya_keyring::Entry;
|
||||
use pimalaya_process::{Cmd, Pipeline, SingleCmd};
|
||||
use pimalaya_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")]
|
||||
pub struct OptionCmdDef;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OptionCmd {
|
||||
#[default]
|
||||
#[serde(skip_serializing)]
|
||||
None,
|
||||
#[serde(with = "SingleCmdDef")]
|
||||
SingleCmd(SingleCmd),
|
||||
#[serde(with = "PipelineDef")]
|
||||
Pipeline(Pipeline),
|
||||
}
|
||||
|
||||
impl From<OptionCmd> for Option<Cmd> {
|
||||
fn from(cmd: OptionCmd) -> Option<Cmd> {
|
||||
match cmd {
|
||||
OptionCmd::None => None,
|
||||
OptionCmd::SingleCmd(cmd) => Some(Cmd::SingleCmd(cmd)),
|
||||
OptionCmd::Pipeline(pipeline) => Some(Cmd::Pipeline(pipeline)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = "BackendConfig", tag = "backend", rename_all = "kebab-case")]
|
||||
pub enum BackendConfigDef {
|
||||
#[default]
|
||||
None,
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[serde(with = "ImapConfigDef")]
|
||||
Imap(ImapConfig),
|
||||
#[serde(with = "MaildirConfigDef")]
|
||||
Maildir(MaildirConfig),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[serde(with = "NotmuchConfigDef")]
|
||||
Notmuch(NotmuchConfig),
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "ImapConfig")]
|
||||
pub struct ImapConfigDef {
|
||||
#[serde(rename = "imap-host")]
|
||||
pub host: String,
|
||||
#[serde(rename = "imap-port")]
|
||||
pub port: u16,
|
||||
#[serde(rename = "imap-ssl")]
|
||||
pub ssl: Option<bool>,
|
||||
#[serde(rename = "imap-starttls")]
|
||||
pub starttls: Option<bool>,
|
||||
#[serde(rename = "imap-insecure")]
|
||||
pub insecure: Option<bool>,
|
||||
#[serde(rename = "imap-login")]
|
||||
pub login: String,
|
||||
#[serde(flatten, with = "ImapAuthConfigDef")]
|
||||
pub auth: ImapAuthConfig,
|
||||
#[serde(rename = "imap-notify-cmd")]
|
||||
pub notify_cmd: Option<String>,
|
||||
#[serde(rename = "imap-notify-query")]
|
||||
pub notify_query: Option<String>,
|
||||
#[serde(rename = "imap-watch-cmds")]
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "ImapAuthConfig", tag = "imap-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 = "imap-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 = "MaildirConfig", rename_all = "kebab-case")]
|
||||
pub struct MaildirConfigDef {
|
||||
#[serde(rename = "maildir-root-dir")]
|
||||
pub root_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[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 = "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 = "SenderConfig", tag = "sender", rename_all = "kebab-case")]
|
||||
pub enum SenderConfigDef {
|
||||
#[default]
|
||||
None,
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
#[serde(with = "SmtpConfigDef")]
|
||||
Smtp(SmtpConfig),
|
||||
#[serde(with = "SendmailConfigDef")]
|
||||
Sendmail(SendmailConfig),
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SmtpConfig")]
|
||||
struct SmtpConfigDef {
|
||||
#[serde(rename = "smtp-host")]
|
||||
pub host: String,
|
||||
#[serde(rename = "smtp-port")]
|
||||
pub port: u16,
|
||||
#[serde(rename = "smtp-ssl")]
|
||||
pub ssl: Option<bool>,
|
||||
#[serde(rename = "smtp-starttls")]
|
||||
pub starttls: Option<bool>,
|
||||
#[serde(rename = "smtp-insecure")]
|
||||
pub insecure: Option<bool>,
|
||||
#[serde(rename = "smtp-login")]
|
||||
pub login: String,
|
||||
#[serde(flatten, with = "SmtpAuthConfigDef")]
|
||||
pub auth: SmtpAuthConfig,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SmtpAuthConfig", tag = "smtp-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 = "smtp-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 SmtpOAuth2ConfigDef {
|
||||
#[serde(rename = "smtp-oauth2-method", with = "OAuth2MethodDef", default)]
|
||||
pub method: OAuth2Method,
|
||||
#[serde(rename = "smtp-oauth2-client-id")]
|
||||
pub client_id: String,
|
||||
#[serde(
|
||||
rename = "smtp-oauth2-client-secret",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub client_secret: Secret,
|
||||
#[serde(rename = "smtp-oauth2-auth-url")]
|
||||
pub auth_url: String,
|
||||
#[serde(rename = "smtp-oauth2-token-url")]
|
||||
pub token_url: String,
|
||||
#[serde(
|
||||
rename = "smtp-oauth2-access-token",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub access_token: Secret,
|
||||
#[serde(
|
||||
rename = "smtp-oauth2-refresh-token",
|
||||
with = "SecretDef",
|
||||
default,
|
||||
skip_serializing_if = "Secret::is_undefined"
|
||||
)]
|
||||
pub refresh_token: Secret,
|
||||
#[serde(flatten, with = "SmtpOAuth2ScopesDef")]
|
||||
pub scopes: OAuth2Scopes,
|
||||
#[serde(rename = "smtp-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 SmtpOAuth2ScopesDef {
|
||||
#[serde(rename = "smtp-oauth2-scope")]
|
||||
Scope(String),
|
||||
#[serde(rename = "smtp-oauth2-scopes")]
|
||||
Scopes(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(remote = "SendmailConfig", rename_all = "kebab-case")]
|
||||
pub struct SendmailConfigDef {
|
||||
#[serde(rename = "sendmail-cmd", with = "CmdDef")]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
/// 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 = "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>),
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
use super::DeserializedConfig;
|
||||
use crate::account;
|
||||
use anyhow::Result;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{env, fs, io, path::PathBuf, process};
|
||||
|
||||
#[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) fn configure() -> Result<DeserializedConfig> {
|
||||
wizard_log!("Configuring your first account:");
|
||||
|
||||
let mut config = DeserializedConfig::default();
|
||||
|
||||
while let Some((name, account_config)) = account::wizard::configure()? {
|
||||
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 => 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(
|
||||
dirs::config_dir()
|
||||
.map(|p| p.join("himalaya").join("config.toml"))
|
||||
.unwrap_or_else(|| env::temp_dir().join("himalaya").join("config.toml"))
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
.validate_with(|path: &String| shellexpand::full(path).map(|_| ()))
|
||||
.interact()?;
|
||||
let path: PathBuf = shellexpand::full(&path).unwrap().to_string().into();
|
||||
|
||||
println!("Writing the configuration to {path:?}…");
|
||||
|
||||
fs::create_dir_all(path.parent().unwrap_or(&path))?;
|
||||
fs::write(path, toml::to_string(&config)?)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
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,54 +0,0 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains the definition of the printable account,
|
||||
//! which is only used by the "accounts" command to list all available
|
||||
//! accounts from the config file.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::ui::table::{Cell, Row, Table};
|
||||
|
||||
/// 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("BACKEND").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())
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains the definition of the printable account,
|
||||
//! which is only used by the "accounts" command to list all available
|
||||
//! accounts from the config file.
|
||||
|
||||
use anyhow::Result;
|
||||
use pimalaya_email::backend::BackendConfig;
|
||||
use serde::Serialize;
|
||||
use std::{collections::hash_map::Iter, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
use super::{Account, DeserializedAccountConfig};
|
||||
|
||||
/// 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, DeserializedAccountConfig>> for Accounts {
|
||||
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
|
||||
let mut accounts: Vec<_> = map
|
||||
.map(|(name, account)| match &account.backend {
|
||||
BackendConfig::None => Account::new(name, "none", false),
|
||||
BackendConfig::Maildir(_) => {
|
||||
Account::new(name, "maildir", account.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "imap-backend")]
|
||||
BackendConfig::Imap(_) => {
|
||||
Account::new(name, "imap", account.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
BackendConfig::Notmuch(_) => {
|
||||
Account::new(name, "notmuch", account.default.unwrap_or_default())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
Self(accounts)
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
//! This module provides arguments related to the user account config.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use log::info;
|
||||
use pimalaya_email::folder::sync::FolderSyncStrategy;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{folder, ui::table};
|
||||
|
||||
const ARG_ACCOUNT: &str = "account";
|
||||
const ARG_DRY_RUN: &str = "dry-run";
|
||||
const ARG_RESET: &str = "reset";
|
||||
const CMD_ACCOUNTS: &str = "accounts";
|
||||
const CMD_CONFIGURE: &str = "configure";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_SYNC: &str = "sync";
|
||||
|
||||
type DryRun = bool;
|
||||
type Reset = bool;
|
||||
|
||||
/// Represents the account commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
/// Represents the list accounts command.
|
||||
List(table::args::MaxTableWidth),
|
||||
/// Represents the sync account command.
|
||||
Sync(Option<FolderSyncStrategy>, DryRun),
|
||||
/// Configure the current selected account.
|
||||
Configure(Reset),
|
||||
}
|
||||
|
||||
/// Represents the account command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_SYNC) {
|
||||
info!("sync account subcommand matched");
|
||||
let dry_run = parse_dry_run_arg(m);
|
||||
let include = folder::args::parse_include_arg(m);
|
||||
let exclude = folder::args::parse_exclude_arg(m);
|
||||
let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) {
|
||||
Some(FolderSyncStrategy::Include(HashSet::from_iter([
|
||||
folder.to_owned()
|
||||
])))
|
||||
} else if !include.is_empty() {
|
||||
Some(FolderSyncStrategy::Include(include.to_owned()))
|
||||
} else if !exclude.is_empty() {
|
||||
Some(FolderSyncStrategy::Exclude(exclude))
|
||||
} else if folder::args::parse_all_arg(m) {
|
||||
Some(FolderSyncStrategy::All)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Cmd::Sync(folders_strategy, dry_run))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
info!("list accounts subcommand matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
|
||||
info!("configure account subcommand matched");
|
||||
let reset = parse_reset_flag(m);
|
||||
Some(Cmd::Configure(reset))
|
||||
} else {
|
||||
info!("no account subcommand matched, falling back to subcommand list");
|
||||
Some(Cmd::List(None))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the account subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_ACCOUNTS)
|
||||
.about("Manage accounts")
|
||||
.subcommands([
|
||||
Command::new(CMD_LIST)
|
||||
.about("List all accounts from the config file")
|
||||
.arg(table::args::max_width()),
|
||||
Command::new(CMD_SYNC)
|
||||
.about("Synchronize the given account locally")
|
||||
.arg(folder::args::all_arg("Synchronize all folders"))
|
||||
.arg(folder::args::include_arg(
|
||||
"Synchronize only the given folders",
|
||||
))
|
||||
.arg(folder::args::exclude_arg(
|
||||
"Synchronize all folders except the given ones",
|
||||
))
|
||||
.arg(dry_run()),
|
||||
Command::new(CMD_CONFIGURE)
|
||||
.about("Configure the current selected account")
|
||||
.aliases(["config", "conf", "cfg"])
|
||||
.arg(reset_flag()),
|
||||
])
|
||||
}
|
||||
|
||||
/// Represents the user account name argument. This argument allows
|
||||
/// the user to select a different account than the default one.
|
||||
pub fn arg() -> Arg {
|
||||
Arg::new(ARG_ACCOUNT)
|
||||
.help("Set the account")
|
||||
.long("account")
|
||||
.short('a')
|
||||
.global(true)
|
||||
.value_name("STRING")
|
||||
}
|
||||
|
||||
/// Represents the user account name argument parser.
|
||||
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the user account sync dry run flag. This flag allows
|
||||
/// the user to see the changes of a sync without applying them.
|
||||
pub fn dry_run() -> Arg {
|
||||
Arg::new(ARG_DRY_RUN)
|
||||
.help("Do not apply changes of the synchronization")
|
||||
.long_help(
|
||||
"Do not apply changes of the synchronization.
|
||||
Changes can be visualized with the RUST_LOG=trace environment variable.",
|
||||
)
|
||||
.short('d')
|
||||
.long("dry-run")
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the user account sync dry run flag parser.
|
||||
pub fn parse_dry_run_arg(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DRY_RUN)
|
||||
}
|
||||
|
||||
pub fn reset_flag() -> Arg {
|
||||
Arg::new(ARG_RESET)
|
||||
.help("Reset the configuration")
|
||||
.short('r')
|
||||
.long("reset")
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
pub fn parse_reset_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_RESET)
|
||||
}
|
|
@ -1,263 +0,0 @@
|
|||
//! Deserialized account config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of an
|
||||
//! account in the accounts section of the user configuration file.
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use pimalaya_email::backend::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
use pimalaya_email::sender::SmtpAuthConfig;
|
||||
use pimalaya_email::{
|
||||
account::AccountConfig,
|
||||
backend::BackendConfig,
|
||||
email::{EmailHooks, EmailTextPlainFormat},
|
||||
folder::sync::FolderSyncStrategy,
|
||||
sender::SenderConfig,
|
||||
};
|
||||
use pimalaya_process::Cmd;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::{prelude::*, DeserializedConfig};
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "backend", rename_all = "kebab-case")]
|
||||
pub struct DeserializedAccountConfig {
|
||||
pub email: String,
|
||||
pub default: Option<bool>,
|
||||
pub display_name: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
pub folder_listing_page_size: Option<usize>,
|
||||
pub folder_aliases: Option<HashMap<String, String>>,
|
||||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_listing_datetime_fmt: Option<String>,
|
||||
pub email_listing_datetime_local_tz: Option<bool>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "EmailTextPlainFormatDef",
|
||||
skip_serializing_if = "EmailTextPlainFormat::is_default"
|
||||
)]
|
||||
pub email_reading_format: EmailTextPlainFormat,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_reading_verify_cmd: Option<Cmd>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_reading_decrypt_cmd: Option<Cmd>,
|
||||
pub email_writing_headers: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_writing_sign_cmd: Option<Cmd>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "OptionCmdDef",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub email_writing_encrypt_cmd: Option<Cmd>,
|
||||
pub email_sending_save_copy: Option<bool>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "EmailHooksDef",
|
||||
skip_serializing_if = "EmailHooks::is_empty"
|
||||
)]
|
||||
pub email_hooks: EmailHooks,
|
||||
|
||||
pub sync: Option<bool>,
|
||||
pub sync_dir: Option<PathBuf>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "FolderSyncStrategyDef",
|
||||
skip_serializing_if = "FolderSyncStrategy::is_default"
|
||||
)]
|
||||
pub sync_folders_strategy: FolderSyncStrategy,
|
||||
|
||||
#[serde(flatten, with = "BackendConfigDef")]
|
||||
pub backend: BackendConfig,
|
||||
#[serde(flatten, with = "SenderConfigDef")]
|
||||
pub sender: SenderConfig,
|
||||
}
|
||||
|
||||
impl DeserializedAccountConfig {
|
||||
pub fn to_account_config(&self, name: String, config: &DeserializedConfig) -> AccountConfig {
|
||||
let mut folder_aliases = config
|
||||
.folder_aliases
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default();
|
||||
folder_aliases.extend(
|
||||
self.folder_aliases
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
AccountConfig {
|
||||
name: name.clone(),
|
||||
email: self.email.to_owned(),
|
||||
display_name: self
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.display_name.as_ref().map(ToOwned::to_owned)),
|
||||
signature_delim: self
|
||||
.signature_delim
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.signature_delim.as_ref().map(ToOwned::to_owned)),
|
||||
signature: self
|
||||
.signature
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.signature.as_ref().map(ToOwned::to_owned)),
|
||||
downloads_dir: self
|
||||
.downloads_dir
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.downloads_dir.as_ref().map(ToOwned::to_owned)),
|
||||
folder_listing_page_size: self
|
||||
.folder_listing_page_size
|
||||
.or_else(|| config.folder_listing_page_size),
|
||||
folder_aliases,
|
||||
email_listing_page_size: self
|
||||
.email_listing_page_size
|
||||
.or_else(|| config.email_listing_page_size),
|
||||
email_listing_datetime_fmt: self
|
||||
.email_listing_datetime_fmt
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_listing_datetime_fmt
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_listing_datetime_local_tz: self
|
||||
.email_listing_datetime_local_tz
|
||||
.or_else(|| config.email_listing_datetime_local_tz),
|
||||
email_reading_headers: self
|
||||
.email_reading_headers
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)),
|
||||
email_reading_format: self.email_reading_format.clone(),
|
||||
email_reading_verify_cmd: self
|
||||
.email_reading_verify_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_reading_verify_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_reading_decrypt_cmd: self
|
||||
.email_reading_decrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_reading_decrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_sign_cmd: self
|
||||
.email_writing_sign_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_writing_sign_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_encrypt_cmd: self
|
||||
.email_writing_encrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_writing_encrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_headers: self
|
||||
.email_writing_headers
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)),
|
||||
email_sending_save_copy: self.email_sending_save_copy.unwrap_or(true),
|
||||
email_hooks: EmailHooks {
|
||||
pre_send: self.email_hooks.pre_send.clone(),
|
||||
},
|
||||
sync: self.sync.unwrap_or_default(),
|
||||
sync_dir: self.sync_dir.clone(),
|
||||
sync_folders_strategy: self.sync_folders_strategy.clone(),
|
||||
|
||||
backend: {
|
||||
let mut backend = self.backend.clone();
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
if let BackendConfig::Imap(config) = &mut backend {
|
||||
match &mut config.auth {
|
||||
ImapAuthConfig::Passwd(secret) => {
|
||||
secret.set_keyring_entry_if_undefined(format!("{name}-imap-passwd"));
|
||||
}
|
||||
ImapAuthConfig::OAuth2(config) => {
|
||||
config.client_secret.set_keyring_entry_if_undefined(format!(
|
||||
"{name}-imap-oauth2-client-secret"
|
||||
));
|
||||
config.access_token.set_keyring_entry_if_undefined(format!(
|
||||
"{name}-imap-oauth2-access-token"
|
||||
));
|
||||
config.refresh_token.set_keyring_entry_if_undefined(format!(
|
||||
"{name}-imap-oauth2-refresh-token"
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
backend
|
||||
},
|
||||
sender: {
|
||||
let mut sender = self.sender.clone();
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
if let SenderConfig::Smtp(config) = &mut sender {
|
||||
match &mut config.auth {
|
||||
SmtpAuthConfig::Passwd(secret) => {
|
||||
secret.set_keyring_entry_if_undefined(format!("{name}-smtp-passwd"));
|
||||
}
|
||||
SmtpAuthConfig::OAuth2(config) => {
|
||||
config.client_secret.set_keyring_entry_if_undefined(format!(
|
||||
"{name}-smtp-oauth2-client-secret"
|
||||
));
|
||||
config.access_token.set_keyring_entry_if_undefined(format!(
|
||||
"{name}-smtp-oauth2-access-token"
|
||||
));
|
||||
config.refresh_token.set_keyring_entry_if_undefined(format!(
|
||||
"{name}-smtp-oauth2-refresh-token"
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
sender
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,387 +0,0 @@
|
|||
//! Account handlers module.
|
||||
//!
|
||||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use log::{info, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use pimalaya_email::backend::ImapAuthConfig;
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
use pimalaya_email::sender::SmtpAuthConfig;
|
||||
use pimalaya_email::{
|
||||
account::{
|
||||
sync::{AccountSyncBuilder, AccountSyncProgressEvent},
|
||||
AccountConfig,
|
||||
},
|
||||
backend::BackendConfig,
|
||||
sender::SenderConfig,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
wizard::{prompt_passwd, prompt_secret},
|
||||
DeserializedConfig,
|
||||
},
|
||||
printer::{PrintTableOpts, Printer},
|
||||
Accounts,
|
||||
};
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
/// Configure the current selected account
|
||||
pub async fn configure(config: &AccountConfig, reset: bool) -> Result<()> {
|
||||
info!("entering the configure account handler");
|
||||
|
||||
if reset {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
if let BackendConfig::Imap(imap_config) = &config.backend {
|
||||
let reset = match &imap_config.auth {
|
||||
ImapAuthConfig::Passwd(passwd) => passwd.reset(),
|
||||
ImapAuthConfig::OAuth2(oauth2) => oauth2.reset(),
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting imap secrets, skipping it");
|
||||
warn!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
if let SenderConfig::Smtp(smtp_config) = &config.sender {
|
||||
let reset = match &smtp_config.auth {
|
||||
SmtpAuthConfig::Passwd(passwd) => passwd.reset(),
|
||||
SmtpAuthConfig::OAuth2(oauth2) => oauth2.reset(),
|
||||
};
|
||||
if let Err(err) = reset {
|
||||
warn!("error while resetting smtp secrets, skipping it");
|
||||
warn!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
if let BackendConfig::Imap(imap_config) = &config.backend {
|
||||
match &imap_config.auth {
|
||||
ImapAuthConfig::Passwd(passwd) => {
|
||||
passwd.configure(|| prompt_passwd("IMAP password")).await
|
||||
}
|
||||
ImapAuthConfig::OAuth2(oauth2) => {
|
||||
oauth2
|
||||
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
if let SenderConfig::Smtp(smtp_config) = &config.sender {
|
||||
match &smtp_config.auth {
|
||||
SmtpAuthConfig::Passwd(passwd) => {
|
||||
passwd.configure(|| prompt_passwd("SMTP password")).await
|
||||
}
|
||||
SmtpAuthConfig::OAuth2(oauth2) => {
|
||||
oauth2
|
||||
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Account successfully {}configured!",
|
||||
if reset { "re" } else { "" }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all accounts.
|
||||
pub fn list<'a, P: Printer>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
deserialized_config: &DeserializedConfig,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!("entering the list accounts handler");
|
||||
|
||||
let accounts: Accounts = deserialized_config.accounts.iter().into();
|
||||
trace!("accounts: {:?}", accounts);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
info!("<< account list handler");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes the account defined using argument `-a|--account`. If
|
||||
/// no account given, synchronizes the default one.
|
||||
pub async fn sync<P: Printer>(
|
||||
printer: &mut P,
|
||||
sync_builder: AccountSyncBuilder,
|
||||
dry_run: bool,
|
||||
) -> Result<()> {
|
||||
info!("entering the sync accounts handler");
|
||||
trace!("dry run: {dry_run}");
|
||||
|
||||
if 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 to be synchronized: {hunks_count}",
|
||||
))?;
|
||||
} else if printer.is_json() {
|
||||
sync_builder.sync().await?;
|
||||
printer.print("Account 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("Account successfully synchronized!")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pimalaya_email::{account::AccountConfig, backend::ImapConfig};
|
||||
use std::{collections::HashMap, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
account::DeserializedAccountConfig,
|
||||
printer::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds_accounts() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.content = String::default();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let config = AccountConfig::default();
|
||||
let deserialized_config = DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account-1".into(),
|
||||
DeserializedAccountConfig {
|
||||
default: Some(true),
|
||||
backend: BackendConfig::Imap(ImapConfig::default()),
|
||||
..DeserializedAccountConfig::default()
|
||||
},
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
};
|
||||
|
||||
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │BACKEND │DEFAULT \n",
|
||||
"account-1 │imap │yes \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
pub mod account;
|
||||
pub mod accounts;
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
pub(crate) mod wizard;
|
||||
|
||||
pub use account::*;
|
||||
pub use accounts::*;
|
||||
pub use config::*;
|
|
@ -1,39 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use dialoguer::Input;
|
||||
use email_address::EmailAddress;
|
||||
|
||||
use crate::{backend, config::wizard::THEME, sender};
|
||||
|
||||
use super::DeserializedAccountConfig;
|
||||
|
||||
pub(crate) fn configure() -> Result<Option<(String, DeserializedAccountConfig)>> {
|
||||
let mut config = DeserializedAccountConfig::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 {
|
||||
Err(anyhow!("Invalid email address: {email}"))
|
||||
}
|
||||
})
|
||||
.interact()?;
|
||||
|
||||
config.display_name = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Full display name")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
config.backend = backend::wizard::configure(&account_name, &config.email)?;
|
||||
|
||||
config.sender = sender::wizard::configure(&account_name, &config.email)?;
|
||||
|
||||
Ok(Some((account_name, config)))
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
//! Module related to IMAP CLI.
|
||||
//!
|
||||
//! This module provides subcommands and a command matcher related to IMAP.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{value_parser, Arg, ArgMatches, Command};
|
||||
use log::debug;
|
||||
|
||||
const ARG_KEEPALIVE: &str = "keepalive";
|
||||
const CMD_NOTIFY: &str = "notify";
|
||||
const CMD_WATCH: &str = "watch";
|
||||
|
||||
type Keepalive = u64;
|
||||
|
||||
/// IMAP commands.
|
||||
pub enum Cmd {
|
||||
/// Start the IMAP notify mode with the give keepalive duration.
|
||||
Notify(Keepalive),
|
||||
/// Start the IMAP watch mode with the give keepalive duration.
|
||||
Watch(Keepalive),
|
||||
}
|
||||
|
||||
/// IMAP command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
if let Some(m) = m.subcommand_matches(CMD_NOTIFY) {
|
||||
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Cmd::Notify(*keepalive)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches(CMD_WATCH) {
|
||||
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Cmd::Watch(*keepalive)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// IMAP subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![
|
||||
Command::new(CMD_NOTIFY)
|
||||
.about("Notifies when new messages arrive in the given folder")
|
||||
.alias("idle")
|
||||
.arg(keepalive_arg()),
|
||||
Command::new(CMD_WATCH)
|
||||
.about("Watches IMAP server changes")
|
||||
.arg(keepalive_arg()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Represents the keepalive argument.
|
||||
pub fn keepalive_arg() -> Arg {
|
||||
Arg::new(ARG_KEEPALIVE)
|
||||
.help("Specifies the keepalive duration.")
|
||||
.long("keepalive")
|
||||
.short('k')
|
||||
.value_name("SECS")
|
||||
.default_value("500")
|
||||
.value_parser(value_parser!(u64))
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
//! Module related to IMAP handling.
|
||||
//!
|
||||
//! This module gathers all IMAP handlers triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use pimalaya_email::backend::ImapBackend;
|
||||
|
||||
pub async fn notify(imap: &mut ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
|
||||
imap.notify(keepalive, folder).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn watch(imap: &mut ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
|
||||
imap.watch(keepalive, folder).await?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
||||
pub(crate) mod wizard;
|
|
@ -1,221 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::{Confirm, Input, Password, Select};
|
||||
use pimalaya_email::{
|
||||
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
|
||||
backend::{BackendConfig, ImapAuthConfig, ImapConfig},
|
||||
};
|
||||
use pimalaya_oauth2::{AuthorizationCodeGrant, Client};
|
||||
use pimalaya_secret::Secret;
|
||||
|
||||
use crate::{
|
||||
config::wizard::{prompt_passwd, THEME},
|
||||
wizard_log, wizard_prompt,
|
||||
};
|
||||
|
||||
const SSL_TLS: &str = "SSL/TLS";
|
||||
const STARTTLS: &str = "STARTTLS";
|
||||
const NONE: &str = "None";
|
||||
const PROTOCOLS: &[&str] = &[SSL_TLS, STARTTLS, NONE];
|
||||
|
||||
const PASSWD: &str = "Password";
|
||||
const OAUTH2: &str = "OAuth 2.0";
|
||||
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];
|
||||
|
||||
const XOAUTH2: &str = "XOAUTH2";
|
||||
const OAUTHBEARER: &str = "OAUTHBEARER";
|
||||
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
||||
|
||||
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
|
||||
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
|
||||
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
|
||||
const CMD: &str = "Ask me a shell command that exposes my password";
|
||||
|
||||
pub(crate) fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||
let mut config = ImapConfig::default();
|
||||
|
||||
config.host = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP host")
|
||||
.default(format!("imap.{}", email.rsplit_once('@').unwrap().1))
|
||||
.interact()?;
|
||||
|
||||
let protocol = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP security protocol")
|
||||
.items(PROTOCOLS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let default_port = match protocol {
|
||||
Some(idx) if PROTOCOLS[idx] == SSL_TLS => {
|
||||
config.ssl = Some(true);
|
||||
993
|
||||
}
|
||||
Some(idx) if PROTOCOLS[idx] == STARTTLS => {
|
||||
config.starttls = Some(true);
|
||||
143
|
||||
}
|
||||
_ => 143,
|
||||
};
|
||||
|
||||
config.port = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP port")
|
||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||
.default(default_port.to_string())
|
||||
.interact()
|
||||
.map(|input| input.parse::<u16>().unwrap())?;
|
||||
|
||||
config.login = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP login")
|
||||
.default(email.to_owned())
|
||||
.interact()?;
|
||||
|
||||
let auth = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP authentication mechanism")
|
||||
.items(AUTH_MECHANISMS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
config.auth = match auth {
|
||||
Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
|
||||
let secret = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP authentication strategy")
|
||||
.items(SECRETS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let config = match secret {
|
||||
Some(idx) if SECRETS[idx] == KEYRING => {
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-passwd"))
|
||||
.set_keyring_entry_secret(prompt_passwd("IMAP password")?)?;
|
||||
PasswdConfig::default()
|
||||
}
|
||||
Some(idx) if SECRETS[idx] == RAW => PasswdConfig {
|
||||
passwd: Secret::Raw(prompt_passwd("IMAP password")?),
|
||||
},
|
||||
Some(idx) if SECRETS[idx] == CMD => PasswdConfig {
|
||||
passwd: Secret::new_cmd(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Shell command")
|
||||
.default(format!("pass show {account_name}-imap-passwd"))
|
||||
.interact()?,
|
||||
),
|
||||
},
|
||||
_ => PasswdConfig::default(),
|
||||
};
|
||||
ImapAuthConfig::Passwd(config)
|
||||
}
|
||||
Some(idx) if AUTH_MECHANISMS[idx] == OAUTH2 => {
|
||||
let mut config = OAuth2Config::default();
|
||||
|
||||
let method = Select::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 mechanism")
|
||||
.items(OAUTH2_MECHANISMS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
config.method = match method {
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||
_ => OAuth2Method::XOAuth2,
|
||||
};
|
||||
|
||||
config.client_id = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 client id")
|
||||
.interact()?;
|
||||
|
||||
let client_secret: String = Password::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 client secret")
|
||||
.interact()?;
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))
|
||||
.set_keyring_entry_secret(&client_secret)?;
|
||||
|
||||
config.auth_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 authorization URL")
|
||||
.interact()?;
|
||||
|
||||
config.token_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 token URL")
|
||||
.interact()?;
|
||||
|
||||
config.scopes = OAuth2Scopes::Scope(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("IMAP OAuth 2.0 main scope")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
while Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to add more IMAP OAuth 2.0 scopes?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let mut scopes = match config.scopes {
|
||||
OAuth2Scopes::Scope(scope) => vec![scope],
|
||||
OAuth2Scopes::Scopes(scopes) => scopes,
|
||||
};
|
||||
|
||||
scopes.push(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Additional IMAP OAuth 2.0 scope")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
config.scopes = OAuth2Scopes::Scopes(scopes);
|
||||
}
|
||||
|
||||
config.pkce = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to enable PKCE verification?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or(true);
|
||||
|
||||
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
|
||||
|
||||
let client = Client::new(
|
||||
config.client_id.clone(),
|
||||
client_secret,
|
||||
config.auth_url.clone(),
|
||||
config.token_url.clone(),
|
||||
)?
|
||||
.with_redirect_host(config.redirect_host.clone())
|
||||
.with_redirect_port(config.redirect_port)
|
||||
.build()?;
|
||||
|
||||
let mut auth_code_grant = AuthorizationCodeGrant::new()
|
||||
.with_redirect_host(config.redirect_host.clone())
|
||||
.with_redirect_port(config.redirect_port);
|
||||
|
||||
if config.pkce {
|
||||
auth_code_grant = auth_code_grant.with_pkce();
|
||||
}
|
||||
|
||||
for scope in config.scopes.clone() {
|
||||
auth_code_grant = auth_code_grant.with_scope(scope);
|
||||
}
|
||||
|
||||
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
|
||||
|
||||
println!("{}", redirect_url.to_string());
|
||||
println!("");
|
||||
|
||||
let (access_token, refresh_token) =
|
||||
auth_code_grant.wait_for_redirection(&client, csrf_token)?;
|
||||
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))
|
||||
.set_keyring_entry_secret(access_token)?;
|
||||
|
||||
if let Some(refresh_token) = &refresh_token {
|
||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))
|
||||
.set_keyring_entry_secret(refresh_token)?;
|
||||
}
|
||||
|
||||
ImapAuthConfig::OAuth2(config)
|
||||
}
|
||||
_ => ImapAuthConfig::default(),
|
||||
};
|
||||
|
||||
Ok(BackendConfig::Imap(config))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,23 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use dirs::home_dir;
|
||||
use pimalaya_email::backend::{BackendConfig, MaildirConfig};
|
||||
|
||||
use crate::config::wizard::THEME;
|
||||
|
||||
pub(crate) fn configure() -> Result<BackendConfig> {
|
||||
let mut config = MaildirConfig::default();
|
||||
|
||||
let mut input = Input::with_theme(&*THEME);
|
||||
|
||||
if let Some(home) = home_dir() {
|
||||
input.default(home.join("Mail").display().to_string());
|
||||
};
|
||||
|
||||
config.root_dir = input
|
||||
.with_prompt("Maildir directory")
|
||||
.interact_text()?
|
||||
.into();
|
||||
|
||||
Ok(BackendConfig::Maildir(config))
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
#[cfg(feature = "imap-backend")]
|
||||
pub mod imap;
|
||||
pub mod maildir;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub mod notmuch;
|
||||
pub(crate) mod wizard;
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,20 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use pimalaya_email::backend::{BackendConfig, NotmuchBackend, NotmuchConfig};
|
||||
|
||||
use crate::config::wizard::THEME;
|
||||
|
||||
pub(crate) fn configure() -> Result<BackendConfig> {
|
||||
let mut config = NotmuchConfig::default();
|
||||
|
||||
config.db_path = if let Ok(db_path) = NotmuchBackend::get_default_db_path() {
|
||||
db_path
|
||||
} else {
|
||||
let db_path: String = Input::with_theme(&*THEME)
|
||||
.with_prompt("Notmuch database path")
|
||||
.interact_text()?;
|
||||
db_path.into()
|
||||
};
|
||||
|
||||
Ok(BackendConfig::Notmuch(config))
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Select;
|
||||
use pimalaya_email::backend::BackendConfig;
|
||||
|
||||
use crate::config::wizard::THEME;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use super::imap;
|
||||
use super::maildir;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use super::notmuch;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
const IMAP: &str = "IMAP";
|
||||
const MAILDIR: &str = "Maildir";
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
const NOTMUCH: &str = "Notmuch";
|
||||
const NONE: &str = "None";
|
||||
|
||||
const BACKENDS: &[&str] = &[
|
||||
#[cfg(feature = "imap-backend")]
|
||||
IMAP,
|
||||
MAILDIR,
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
NOTMUCH,
|
||||
NONE,
|
||||
];
|
||||
|
||||
pub(crate) fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||
let backend = Select::with_theme(&*THEME)
|
||||
.with_prompt("Email backend")
|
||||
.items(BACKENDS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
match backend {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Some(idx) if BACKENDS[idx] == IMAP => imap::wizard::configure(account_name, email),
|
||||
Some(idx) if BACKENDS[idx] == MAILDIR => maildir::wizard::configure(),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Some(idx) if BACKENDS[idx] == NOTMUCH => notmuch::wizard::configure(),
|
||||
_ => Ok(BackendConfig::None),
|
||||
}
|
||||
}
|
|
@ -1,438 +0,0 @@
|
|||
//! Email CLI module.
|
||||
//!
|
||||
//! This module contains the command matcher, the subcommands and the
|
||||
//! arguments related to the email domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
|
||||
use crate::{flag, folder, tpl, ui::table};
|
||||
|
||||
const ARG_CRITERIA: &str = "criterion";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
const ARG_ID: &str = "id";
|
||||
const ARG_IDS: &str = "ids";
|
||||
const ARG_MIME_TYPE: &str = "mime-type";
|
||||
const ARG_PAGE: &str = "page";
|
||||
const ARG_PAGE_SIZE: &str = "page-size";
|
||||
const ARG_QUERY: &str = "query";
|
||||
const ARG_RAW: &str = "raw";
|
||||
const ARG_REPLY_ALL: &str = "reply-all";
|
||||
const CMD_ATTACHMENTS: &str = "attachments";
|
||||
const CMD_COPY: &str = "copy";
|
||||
const CMD_DELETE: &str = "delete";
|
||||
const CMD_FORWARD: &str = "forward";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_MOVE: &str = "move";
|
||||
const CMD_READ: &str = "read";
|
||||
const CMD_REPLY: &str = "reply";
|
||||
const CMD_SAVE: &str = "save";
|
||||
const CMD_SEARCH: &str = "search";
|
||||
const CMD_SEND: &str = "send";
|
||||
const CMD_SORT: &str = "sort";
|
||||
const CMD_WRITE: &str = "write";
|
||||
|
||||
pub type All = bool;
|
||||
pub type Criteria = String;
|
||||
pub type Folder<'a> = &'a str;
|
||||
pub type Headers<'a> = Vec<&'a str>;
|
||||
pub type Id<'a> = &'a str;
|
||||
pub type Ids<'a> = Vec<&'a str>;
|
||||
pub type Page = usize;
|
||||
pub type PageSize = usize;
|
||||
pub type Query = String;
|
||||
pub type Raw = bool;
|
||||
pub type RawEmail = String;
|
||||
pub type TextMime<'a> = &'a str;
|
||||
|
||||
/// Represents the email commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Attachments(Ids<'a>),
|
||||
Copy(Ids<'a>, Folder<'a>),
|
||||
Delete(Ids<'a>),
|
||||
Flag(Option<flag::args::Cmd<'a>>),
|
||||
Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Ids<'a>, Folder<'a>),
|
||||
Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>),
|
||||
Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
Save(RawEmail),
|
||||
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawEmail),
|
||||
Sort(
|
||||
Criteria,
|
||||
Query,
|
||||
table::args::MaxTableWidth,
|
||||
Option<PageSize>,
|
||||
Page,
|
||||
),
|
||||
Tpl(Option<tpl::args::Cmd<'a>>),
|
||||
Write(tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
}
|
||||
|
||||
/// Email command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Attachments(ids)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Copy(ids, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Delete(ids)
|
||||
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
|
||||
Cmd::Flag(flag::args::matches(m)?)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
let id = parse_id_arg(m);
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Forward(id, headers, body)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
Cmd::List(max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Move(ids, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let mime = parse_mime_type_arg(m);
|
||||
let raw = parse_raw_flag(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
Cmd::Read(ids, mime, raw, headers)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
let id = parse_id_arg(m);
|
||||
let all = parse_reply_all_flag(m);
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Reply(id, all, headers, body)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Save(email)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
let query = parse_query_arg(m);
|
||||
Cmd::Search(query, max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SORT) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
let criteria = parse_criteria_arg(m);
|
||||
let query = parse_query_arg(m);
|
||||
Cmd::Sort(criteria, query, max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Send(email)
|
||||
} else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
|
||||
Cmd::Tpl(tpl::args::matches(m)?)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Write(headers, body)
|
||||
} else {
|
||||
Cmd::List(None, None, 0)
|
||||
};
|
||||
|
||||
Ok(Some(cmd))
|
||||
}
|
||||
|
||||
/// Represents the email subcommands.
|
||||
pub fn subcmds() -> Vec<Command> {
|
||||
vec![
|
||||
flag::args::subcmds(),
|
||||
tpl::args::subcmds(),
|
||||
vec![
|
||||
Command::new(CMD_ATTACHMENTS)
|
||||
.about("Downloads all emails attachments")
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_LIST)
|
||||
.alias("lst")
|
||||
.about("List envelopes")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width()),
|
||||
Command::new(CMD_SEARCH)
|
||||
.aliases(["query", "q"])
|
||||
.about("Filter envelopes matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(query_arg()),
|
||||
Command::new(CMD_SORT)
|
||||
.about("Sort envelopes by the given criteria and matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(criteria_arg())
|
||||
.arg(query_arg()),
|
||||
Command::new(CMD_WRITE)
|
||||
.about("Write a new email")
|
||||
.aliases(["new", "n"])
|
||||
.args(tpl::args::args()),
|
||||
Command::new(CMD_SEND)
|
||||
.about("Send a raw email")
|
||||
.arg(raw_arg()),
|
||||
Command::new(CMD_SAVE)
|
||||
.about("Save a raw email")
|
||||
.arg(raw_arg()),
|
||||
Command::new(CMD_READ)
|
||||
.about("Read text bodies of emails")
|
||||
.arg(mime_type_arg())
|
||||
.arg(raw_flag())
|
||||
.arg(headers_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_REPLY)
|
||||
.about("Answer to an email")
|
||||
.arg(reply_all_flag())
|
||||
.args(tpl::args::args())
|
||||
.arg(id_arg()),
|
||||
Command::new(CMD_FORWARD)
|
||||
.aliases(["fwd", "f"])
|
||||
.about("Forward an email")
|
||||
.args(tpl::args::args())
|
||||
.arg(id_arg()),
|
||||
Command::new(CMD_COPY)
|
||||
.alias("cp")
|
||||
.about("Copy emails to the given folder")
|
||||
.arg(folder::args::target_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_MOVE)
|
||||
.alias("mv")
|
||||
.about("Move emails to the given folder")
|
||||
.arg(folder::args::target_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_DELETE)
|
||||
.aliases(["remove", "rm"])
|
||||
.about("Delete emails")
|
||||
.arg(ids_arg()),
|
||||
],
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
/// Represents the email id argument.
|
||||
pub fn id_arg() -> Arg {
|
||||
Arg::new(ARG_ID)
|
||||
.help("Specifies the target email")
|
||||
.value_name("ID")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email id argument parser.
|
||||
pub fn parse_id_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_ID).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email ids argument.
|
||||
pub fn ids_arg() -> Arg {
|
||||
Arg::new(ARG_IDS)
|
||||
.help("Email ids")
|
||||
.value_name("IDS")
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email ids argument parser.
|
||||
pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> {
|
||||
matches
|
||||
.get_many::<String>(ARG_IDS)
|
||||
.unwrap()
|
||||
.map(String::as_str)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the email sort criteria argument.
|
||||
pub fn criteria_arg<'a>() -> Arg {
|
||||
Arg::new(ARG_CRITERIA)
|
||||
.help("Email sorting preferences")
|
||||
.long("criterion")
|
||||
.short('c')
|
||||
.value_name("CRITERION:ORDER")
|
||||
.action(ArgAction::Append)
|
||||
.value_parser([
|
||||
"arrival",
|
||||
"arrival:asc",
|
||||
"arrival:desc",
|
||||
"cc",
|
||||
"cc:asc",
|
||||
"cc:desc",
|
||||
"date",
|
||||
"date:asc",
|
||||
"date:desc",
|
||||
"from",
|
||||
"from:asc",
|
||||
"from:desc",
|
||||
"size",
|
||||
"size:asc",
|
||||
"size:desc",
|
||||
"subject",
|
||||
"subject:asc",
|
||||
"subject:desc",
|
||||
"to",
|
||||
"to:asc",
|
||||
"to:desc",
|
||||
])
|
||||
}
|
||||
|
||||
/// Represents the email sort criteria argument parser.
|
||||
pub fn parse_criteria_arg(matches: &ArgMatches) -> String {
|
||||
matches
|
||||
.get_many::<String>(ARG_CRITERIA)
|
||||
.unwrap_or_default()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// Represents the email reply all argument.
|
||||
pub fn reply_all_flag() -> Arg {
|
||||
Arg::new(ARG_REPLY_ALL)
|
||||
.help("Include all recipients")
|
||||
.long("all")
|
||||
.short('A')
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the email reply all argument parser.
|
||||
pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
|
||||
matches.get_flag(ARG_REPLY_ALL)
|
||||
}
|
||||
|
||||
/// Represents the page size argument.
|
||||
fn page_size_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE_SIZE)
|
||||
.help("Page size")
|
||||
.long("page-size")
|
||||
.short('s')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the page size argument parser.
|
||||
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE_SIZE)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Represents the page argument.
|
||||
fn page_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE)
|
||||
.help("Page number")
|
||||
.short('p')
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
/// Represents the page argument parser.
|
||||
fn parse_page_arg(matches: &ArgMatches) -> usize {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email headers argument.
|
||||
pub fn headers_arg() -> Arg {
|
||||
Arg::new(ARG_HEADERS)
|
||||
.help("Shows additional headers with the email")
|
||||
.long("header")
|
||||
.short('H')
|
||||
.value_name("STRING")
|
||||
.action(ArgAction::Append)
|
||||
}
|
||||
|
||||
/// Represents the email headers argument parser.
|
||||
pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> {
|
||||
m.get_many::<String>(ARG_HEADERS)
|
||||
.unwrap_or_default()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Represents the raw flag.
|
||||
pub fn raw_flag() -> Arg {
|
||||
Arg::new(ARG_RAW)
|
||||
.help("Returns raw version of email")
|
||||
.long("raw")
|
||||
.short('r')
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the raw flag parser.
|
||||
pub fn parse_raw_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_RAW)
|
||||
}
|
||||
|
||||
/// Represents the email raw argument.
|
||||
pub fn raw_arg() -> Arg {
|
||||
Arg::new(ARG_RAW).raw(true)
|
||||
}
|
||||
|
||||
/// Represents the email raw argument parser.
|
||||
pub fn parse_raw_arg(m: &ArgMatches) -> String {
|
||||
m.get_one::<String>(ARG_RAW).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email MIME type argument.
|
||||
pub fn mime_type_arg() -> Arg {
|
||||
Arg::new(ARG_MIME_TYPE)
|
||||
.help("MIME type to use")
|
||||
.short('t')
|
||||
.long("mime-type")
|
||||
.value_name("MIME")
|
||||
.value_parser(["plain", "html"])
|
||||
.default_value("plain")
|
||||
}
|
||||
|
||||
/// Represents the email MIME type argument parser.
|
||||
pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_MIME_TYPE).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email query argument.
|
||||
pub fn query_arg() -> Arg {
|
||||
Arg::new(ARG_QUERY)
|
||||
.long_help("The query system depends on the backend, see the wiki for more details")
|
||||
.value_name("QUERY")
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email query argument parser.
|
||||
pub fn parse_query_arg(matches: &ArgMatches) -> String {
|
||||
matches
|
||||
.get_many::<String>(ARG_QUERY)
|
||||
.unwrap_or_default()
|
||||
.fold((false, vec![]), |(escape, mut cmds), cmd| {
|
||||
match (cmd.as_str(), escape) {
|
||||
// Next command is an arg and needs to be escaped
|
||||
("subject", _) | ("body", _) | ("text", _) => {
|
||||
cmds.push(cmd.to_string());
|
||||
(true, cmds)
|
||||
}
|
||||
// Escaped arg commands
|
||||
(_, true) => {
|
||||
cmds.push(format!("\"{}\"", cmd));
|
||||
(false, cmds)
|
||||
}
|
||||
// Regular commands
|
||||
(_, false) => {
|
||||
cmds.push(cmd.to_string());
|
||||
(false, cmds)
|
||||
}
|
||||
}
|
||||
})
|
||||
.1
|
||||
.join(" ")
|
||||
}
|
|
@ -1,420 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use atty::Stream;
|
||||
use log::{debug, trace};
|
||||
use pimalaya_email::{
|
||||
account::AccountConfig,
|
||||
backend::Backend,
|
||||
email::{template::FilterParts, Flag, Flags, Message, MessageBuilder},
|
||||
sender::Sender,
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::editor,
|
||||
Envelopes, IdMapper,
|
||||
};
|
||||
|
||||
pub async fn attachments<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let emails = backend.get_emails(&folder, ids.clone()).await?;
|
||||
let mut index = 0;
|
||||
|
||||
let mut emails_count = 0;
|
||||
let mut attachments_count = 0;
|
||||
|
||||
for email in emails.to_vec() {
|
||||
let id = ids.get(index).unwrap();
|
||||
let attachments = email.attachments()?;
|
||||
|
||||
index = index + 1;
|
||||
|
||||
if attachments.is_empty() {
|
||||
printer.print_log(format!("No attachment found for email #{}", id))?;
|
||||
continue;
|
||||
} else {
|
||||
emails_count = emails_count + 1;
|
||||
}
|
||||
|
||||
printer.print_log(format!(
|
||||
"{} attachment(s) found for email #{}…",
|
||||
attachments.len(),
|
||||
id
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let filename = attachment
|
||||
.filename
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
let filepath = config.download_fpath(&filename)?;
|
||||
printer.print_log(format!("Downloading {:?}…", filepath))?;
|
||||
fs::write(&filepath, &attachment.body).context("cannot download attachment")?;
|
||||
attachments_count = attachments_count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
match attachments_count {
|
||||
0 => printer.print("No attachment found!"),
|
||||
1 => printer.print("Downloaded 1 attachment!"),
|
||||
n => printer.print(format!(
|
||||
"Downloaded {} attachment(s) from {} email(s)!",
|
||||
n, emails_count,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn copy<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
backend.copy_emails(&from_folder, &to_folder, ids).await?;
|
||||
printer.print("Email(s) successfully copied!")
|
||||
}
|
||||
|
||||
pub async fn delete<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
backend.delete_emails(&folder, ids).await?;
|
||||
printer.print("Email(s) successfully deleted!")
|
||||
}
|
||||
|
||||
pub async fn forward<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
sender: &mut dyn Sender,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
headers: Option<Vec<(&str, &str)>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids([id])?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
|
||||
let tpl = backend
|
||||
.get_emails(&folder, ids)
|
||||
.await?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_forward_tpl_builder(config)
|
||||
.with_some_headers(headers)
|
||||
.with_some_body(body)
|
||||
.build()
|
||||
.await?;
|
||||
trace!("initial template: {}", *tpl);
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
|
||||
let envelopes = Envelopes::from_backend(
|
||||
config,
|
||||
id_mapper,
|
||||
backend.list_envelopes(&folder, page_size, page).await?,
|
||||
)?;
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses and edits a message from a [mailto] URL string.
|
||||
///
|
||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
pub async fn mailto<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
backend: &mut dyn Backend,
|
||||
sender: &mut dyn Sender,
|
||||
printer: &mut P,
|
||||
url: &Url,
|
||||
) -> Result<()> {
|
||||
let mut builder = MessageBuilder::new().to(url.path());
|
||||
|
||||
for (key, val) in 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" => builder = builder.text_body(val),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let tpl = config
|
||||
.generate_tpl_interpreter()
|
||||
.show_only_headers(config.email_writing_headers())
|
||||
.interpret_msg_builder(builder)
|
||||
.await?;
|
||||
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await
|
||||
}
|
||||
|
||||
pub async fn move_<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
backend.move_emails(&from_folder, &to_folder, ids).await?;
|
||||
printer.print("Email(s) successfully moved!")
|
||||
}
|
||||
|
||||
pub async fn read<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
text_mime: &str,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let emails = backend.get_emails(&folder, ids).await?;
|
||||
|
||||
let mut glue = "";
|
||||
let mut bodies = String::default();
|
||||
|
||||
for email in emails.to_vec() {
|
||||
bodies.push_str(glue);
|
||||
|
||||
if 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(&config, |tpl| match text_mime {
|
||||
"html" => tpl
|
||||
.hide_all_headers()
|
||||
.filter_parts(FilterParts::Only("text/html".into())),
|
||||
_ => tpl.show_additional_headers(&headers),
|
||||
})
|
||||
.await?
|
||||
.into();
|
||||
bodies.push_str(&tpl);
|
||||
}
|
||||
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.print(bodies)
|
||||
}
|
||||
|
||||
pub async fn reply<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
sender: &mut dyn Sender,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
all: bool,
|
||||
headers: Option<Vec<(&str, &str)>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids([id])?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
|
||||
let tpl = backend
|
||||
.get_emails(&folder, ids)
|
||||
.await?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_reply_tpl_builder(config)
|
||||
.with_some_headers(headers)
|
||||
.with_some_body(body)
|
||||
.with_reply_all(all)
|
||||
.build()
|
||||
.await?;
|
||||
trace!("initial template: {}", *tpl);
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
|
||||
backend
|
||||
.add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
raw_email: String,
|
||||
) -> Result<()> {
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
let is_json = printer.is_json();
|
||||
let raw_email = if is_tty || is_json {
|
||||
raw_email.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
|
||||
let id = backend
|
||||
.add_email(&folder, raw_email.as_bytes(), &Flags::default())
|
||||
.await?;
|
||||
id_mapper.create_alias(id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
let envelopes = Envelopes::from_backend(
|
||||
config,
|
||||
id_mapper,
|
||||
backend
|
||||
.search_envelopes(&folder, &query, "", page_size, page)
|
||||
.await?,
|
||||
)?;
|
||||
let opts = PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
};
|
||||
|
||||
printer.print_table(Box::new(envelopes), opts)
|
||||
}
|
||||
|
||||
pub async fn sort<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
sort: String,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
let envelopes = Envelopes::from_backend(
|
||||
config,
|
||||
id_mapper,
|
||||
backend
|
||||
.search_envelopes(&folder, &query, &sort, page_size, page)
|
||||
.await?,
|
||||
)?;
|
||||
let opts = PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
};
|
||||
|
||||
printer.print_table(Box::new(envelopes), opts)
|
||||
}
|
||||
|
||||
pub async fn send<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
sender: &mut dyn Sender,
|
||||
raw_email: String,
|
||||
) -> Result<()> {
|
||||
let folder = config.sent_folder_alias()?;
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
let is_json = printer.is_json();
|
||||
let raw_email = if is_tty || is_json {
|
||||
raw_email.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
trace!("raw email: {:?}", raw_email);
|
||||
sender.send(raw_email.as_bytes()).await?;
|
||||
if config.email_sending_save_copy {
|
||||
backend
|
||||
.add_email(
|
||||
&folder,
|
||||
raw_email.as_bytes(),
|
||||
&Flags::from_iter([Flag::Seen]),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
sender: &mut dyn Sender,
|
||||
headers: Option<Vec<(&str, &str)>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tpl = Message::new_tpl_builder(config)
|
||||
.with_some_headers(headers)
|
||||
.with_some_body(body)
|
||||
.build()
|
||||
.await?;
|
||||
trace!("initial template: {}", *tpl);
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -1,66 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
ui::{Cell, Row, Table},
|
||||
Flag, Flags,
|
||||
};
|
||||
|
||||
#[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())
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use pimalaya_email::account::AccountConfig;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
Envelope, IdMapper, Mailbox,
|
||||
};
|
||||
|
||||
/// 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: pimalaya_email::email::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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::DateTime;
|
||||
use pimalaya_email::account::AccountConfig;
|
||||
use std::env;
|
||||
|
||||
use crate::{Envelopes, IdMapper};
|
||||
|
||||
#[test]
|
||||
fn default_datetime_fmt() {
|
||||
let config = AccountConfig::default();
|
||||
let id_mapper = IdMapper::Dummy;
|
||||
|
||||
let envelopes =
|
||||
pimalaya_email::email::Envelopes::from_iter([pimalaya_email::email::Envelope {
|
||||
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
||||
..Default::default()
|
||||
}]);
|
||||
let envelopes = Envelopes::from_backend(&config, &id_mapper, envelopes).unwrap();
|
||||
|
||||
let expected_date = "2023-06-15 09:42+04:00";
|
||||
let date = &envelopes.first().unwrap().date;
|
||||
|
||||
assert_eq!(date, expected_date);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_datetime_fmt() {
|
||||
let id_mapper = IdMapper::Dummy;
|
||||
let config = AccountConfig {
|
||||
email_listing_datetime_fmt: Some("%d/%m/%Y %Hh%M".into()),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
|
||||
let envelopes =
|
||||
pimalaya_email::email::Envelopes::from_iter([pimalaya_email::email::Envelope {
|
||||
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
||||
..Default::default()
|
||||
}]);
|
||||
let envelopes = Envelopes::from_backend(&config, &id_mapper, envelopes).unwrap();
|
||||
|
||||
let expected_date = "15/06/2023 09h42";
|
||||
let date = &envelopes.first().unwrap().date;
|
||||
|
||||
assert_eq!(date, expected_date);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_datetime_fmt_with_local_tz() {
|
||||
env::set_var("TZ", "UTC");
|
||||
|
||||
let id_mapper = IdMapper::Dummy;
|
||||
let config = AccountConfig {
|
||||
email_listing_datetime_fmt: Some("%d/%m/%Y %Hh%M".into()),
|
||||
email_listing_datetime_local_tz: Some(true),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
|
||||
let envelopes =
|
||||
pimalaya_email::email::Envelopes::from_iter([pimalaya_email::email::Envelope {
|
||||
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
||||
..Default::default()
|
||||
}]);
|
||||
let envelopes = Envelopes::from_backend(&config, &id_mapper, envelopes).unwrap();
|
||||
|
||||
let expected_date = "15/06/2023 05h42";
|
||||
let date = &envelopes.first().unwrap().date;
|
||||
|
||||
assert_eq!(date, expected_date);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod envelope;
|
||||
pub mod envelopes;
|
||||
|
||||
pub use envelope::*;
|
||||
pub use envelopes::*;
|
|
@ -1,105 +0,0 @@
|
|||
//! Email flag CLI module.
|
||||
//!
|
||||
//! This module contains the command matcher, the subcommands and the
|
||||
//! arguments related to the email flag domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use log::{debug, info};
|
||||
use pimalaya_email::email::{Flag, Flags};
|
||||
|
||||
use crate::email;
|
||||
|
||||
const ARG_FLAGS: &str = "flag";
|
||||
|
||||
const CMD_ADD: &str = "add";
|
||||
const CMD_REMOVE: &str = "remove";
|
||||
const CMD_SET: &str = "set";
|
||||
|
||||
pub(crate) const CMD_FLAG: &str = "flags";
|
||||
|
||||
/// Represents the flag commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Add(email::args::Ids<'a>, Flags),
|
||||
Remove(email::args::Ids<'a>, Flags),
|
||||
Set(email::args::Ids<'a>, Flags),
|
||||
}
|
||||
|
||||
/// Represents the flag command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) {
|
||||
debug!("add flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Add(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REMOVE) {
|
||||
info!("remove flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Remove(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
|
||||
debug!("set flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Set(ids, flags))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the flag subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![Command::new(CMD_FLAG)
|
||||
.about("Handles email flags")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
Command::new(CMD_ADD)
|
||||
.about("Adds flags to an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_REMOVE)
|
||||
.aliases(["delete", "del", "d"])
|
||||
.about("Removes flags from an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SET)
|
||||
.aliases(["change", "c"])
|
||||
.about("Sets flags of an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Represents the flags argument.
|
||||
pub fn flags_arg() -> Arg {
|
||||
Arg::new(ARG_FLAGS)
|
||||
.value_name("FLAGS")
|
||||
.help("The flags")
|
||||
.long_help(
|
||||
"The list of flags.
|
||||
It can be one of: seen, answered, flagged, deleted, or draft.
|
||||
Other flags are considered custom.",
|
||||
)
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
.last(true)
|
||||
}
|
||||
|
||||
/// Represents the flags argument parser.
|
||||
pub fn parse_flags_arg(matches: &ArgMatches) -> Flags {
|
||||
Flags::from_iter(
|
||||
matches
|
||||
.get_many::<String>(ARG_FLAGS)
|
||||
.unwrap_or_default()
|
||||
.map(String::as_str)
|
||||
.map(Flag::from),
|
||||
)
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
/// Represents the flag variants.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
|
||||
pub enum Flag {
|
||||
Seen,
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Draft,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&pimalaya_email::email::Flag> for Flag {
|
||||
fn from(flag: &pimalaya_email::email::Flag) -> Self {
|
||||
use pimalaya_email::email::Flag::*;
|
||||
match flag {
|
||||
Seen => Flag::Seen,
|
||||
Answered => Flag::Answered,
|
||||
Flagged => Flag::Flagged,
|
||||
Deleted => Flag::Deleted,
|
||||
Draft => Flag::Draft,
|
||||
Custom(flag) => Flag::Custom(flag.clone()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
use serde::Serialize;
|
||||
use std::{collections::HashSet, ops};
|
||||
|
||||
use crate::Flag;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
||||
pub struct Flags(pub HashSet<Flag>);
|
||||
|
||||
impl ops::Deref for Flags {
|
||||
type Target = HashSet<Flag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pimalaya_email::email::Flags> for Flags {
|
||||
fn from(flags: pimalaya_email::email::Flags) -> Self {
|
||||
Flags(flags.iter().map(Flag::from).collect())
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use pimalaya_email::{backend::Backend, email::Flags};
|
||||
|
||||
use crate::{printer::Printer, IdMapper};
|
||||
|
||||
pub async fn add<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
flags: &Flags,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
backend.add_flags(folder, ids, flags).await?;
|
||||
printer.print("Flag(s) successfully added!")
|
||||
}
|
||||
|
||||
pub async fn set<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
flags: &Flags,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
backend.set_flags(folder, ids, flags).await?;
|
||||
printer.print("Flag(s) successfully set!")
|
||||
}
|
||||
|
||||
pub async fn remove<P: Printer>(
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
flags: &Flags,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids(ids)?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
backend.remove_flags(folder, ids, flags).await?;
|
||||
printer.print("Flag(s) successfully removed!")
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
||||
|
||||
pub mod flag;
|
||||
pub use flag::*;
|
||||
|
||||
pub mod flags;
|
||||
pub use flags::*;
|
|
@ -1,237 +0,0 @@
|
|||
//! Folder CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher
|
||||
//! related to the folder domain.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, Arg, ArgAction, ArgMatches, Command};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_ALL: &str = "all";
|
||||
const ARG_EXCLUDE: &str = "exclude";
|
||||
const ARG_INCLUDE: &str = "include";
|
||||
const ARG_SOURCE: &str = "source";
|
||||
const ARG_TARGET: &str = "target";
|
||||
const CMD_CREATE: &str = "create";
|
||||
const CMD_DELETE: &str = "delete";
|
||||
const CMD_EXPUNGE: &str = "expunge";
|
||||
const CMD_FOLDERS: &str = "folders";
|
||||
const CMD_LIST: &str = "list";
|
||||
|
||||
/// Represents the folder commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
Create,
|
||||
List(table::args::MaxTableWidth),
|
||||
Expunge,
|
||||
Delete,
|
||||
}
|
||||
|
||||
/// Represents the folder command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) {
|
||||
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
|
||||
info!("expunge folder subcommand matched");
|
||||
Some(Cmd::Expunge)
|
||||
} else if let Some(_) = m.subcommand_matches(CMD_CREATE) {
|
||||
debug!("create folder command matched");
|
||||
Some(Cmd::Create)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
debug!("list folders command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else if let Some(_) = m.subcommand_matches(CMD_DELETE) {
|
||||
debug!("delete folder command matched");
|
||||
Some(Cmd::Delete)
|
||||
} else {
|
||||
info!("no folder subcommand matched, falling back to subcommand list");
|
||||
Some(Cmd::List(None))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the folder subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_FOLDERS)
|
||||
.about("Manage folders")
|
||||
.subcommands([
|
||||
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
|
||||
Command::new(CMD_CREATE)
|
||||
.aliases(["add", "new"])
|
||||
.about("Create a new folder"),
|
||||
Command::new(CMD_LIST)
|
||||
.about("List folders")
|
||||
.arg(table::args::max_width()),
|
||||
Command::new(CMD_DELETE)
|
||||
.aliases(["remove", "rm"])
|
||||
.about("Delete a folder with all its emails"),
|
||||
])
|
||||
}
|
||||
|
||||
/// Represents the source folder argument.
|
||||
pub fn source_arg() -> Arg {
|
||||
Arg::new(ARG_SOURCE)
|
||||
.help("Set the source folder")
|
||||
.long("folder")
|
||||
.short('f')
|
||||
.global(true)
|
||||
.value_name("SOURCE")
|
||||
}
|
||||
|
||||
/// Represents the source folder argument parser.
|
||||
pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the all folders argument.
|
||||
pub fn all_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_ALL)
|
||||
.help(help)
|
||||
.long("all-folders")
|
||||
.alias("all")
|
||||
.short('A')
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with(ARG_SOURCE)
|
||||
.conflicts_with(ARG_INCLUDE)
|
||||
.conflicts_with(ARG_EXCLUDE)
|
||||
}
|
||||
|
||||
/// Represents the all folders argument parser.
|
||||
pub fn parse_all_arg(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_ALL)
|
||||
}
|
||||
|
||||
/// Represents the folders to include argument.
|
||||
pub fn include_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_INCLUDE)
|
||||
.help(help)
|
||||
.long("include-folder")
|
||||
.alias("only")
|
||||
.short('F')
|
||||
.value_name("FOLDER")
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.conflicts_with(ARG_SOURCE)
|
||||
.conflicts_with(ARG_ALL)
|
||||
.conflicts_with(ARG_EXCLUDE)
|
||||
}
|
||||
|
||||
/// Represents the folders to include argument parser.
|
||||
pub fn parse_include_arg(m: &ArgMatches) -> HashSet<String> {
|
||||
m.get_many::<String>(ARG_INCLUDE)
|
||||
.unwrap_or_default()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the folders to exclude argument.
|
||||
pub fn exclude_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_EXCLUDE)
|
||||
.help(help)
|
||||
.long("exclude-folder")
|
||||
.alias("except")
|
||||
.short('x')
|
||||
.value_name("FOLDER")
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.conflicts_with(ARG_SOURCE)
|
||||
.conflicts_with(ARG_ALL)
|
||||
.conflicts_with(ARG_INCLUDE)
|
||||
}
|
||||
|
||||
/// Represents the folders to exclude argument parser.
|
||||
pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet<String> {
|
||||
m.get_many::<String>(ARG_EXCLUDE)
|
||||
.unwrap_or_default()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the target folder argument.
|
||||
pub fn target_arg() -> Arg {
|
||||
Arg::new(ARG_TARGET)
|
||||
.help("Specifies the target folder")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the target folder argument parser.
|
||||
pub fn parse_target_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{error::ErrorKind, Command};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds() {
|
||||
let arg = Command::new("himalaya")
|
||||
.subcommand(subcmd())
|
||||
.get_matches_from(&["himalaya", "folders"]);
|
||||
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
|
||||
|
||||
let arg = Command::new("himalaya")
|
||||
.subcommand(subcmd())
|
||||
.get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]);
|
||||
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_source_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
Command::new("himalaya")
|
||||
.arg(source_arg())
|
||||
.get_matches_from(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(None, app.get_one::<String>(ARG_SOURCE).map(String::as_str));
|
||||
|
||||
let app = get_matches_from!["-f", "SOURCE"];
|
||||
assert_eq!(
|
||||
Some("SOURCE"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
|
||||
let app = get_matches_from!["--folder", "SOURCE"];
|
||||
assert_eq!(
|
||||
Some("SOURCE"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_target_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
Command::new("himalaya")
|
||||
.arg(target_arg())
|
||||
.try_get_matches_from_mut(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind());
|
||||
|
||||
let app = get_matches_from!["TARGET"];
|
||||
assert_eq!(
|
||||
Some("TARGET"),
|
||||
app.unwrap()
|
||||
.get_one::<String>(ARG_TARGET)
|
||||
.map(String::as_str)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folder {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl From<&pimalaya_email::folder::Folder> for Folder {
|
||||
fn from(folder: &pimalaya_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())
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
Folder,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Folders(Vec<Folder>);
|
||||
|
||||
impl ops::Deref for Folders {
|
||||
type Target = Vec<Folder>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pimalaya_email::folder::Folders> for Folders {
|
||||
fn from(folders: pimalaya_email::folder::Folders) -> Self {
|
||||
Folders(folders.iter().map(Folder::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Folders {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,288 +0,0 @@
|
|||
//! Folder handling module.
|
||||
//!
|
||||
//! This module gathers all folder actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use dialoguer::Confirm;
|
||||
use pimalaya_email::{account::AccountConfig, backend::Backend};
|
||||
use std::process;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTableOpts, Printer},
|
||||
Folders,
|
||||
};
|
||||
|
||||
pub async fn expunge<P: Printer>(
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
) -> Result<()> {
|
||||
backend.expunge_folder(folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully expunged!"))
|
||||
}
|
||||
|
||||
pub async fn list<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
max_width: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let folders: Folders = backend.list_folders().await?.into();
|
||||
printer.print_table(
|
||||
// TODO: remove Box
|
||||
Box::new(folders),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn create<P: Printer>(
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
) -> Result<()> {
|
||||
backend.add_folder(folder).await?;
|
||||
printer.print("Folder successfully created!")
|
||||
}
|
||||
|
||||
pub async fn delete<P: Printer>(
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
) -> Result<()> {
|
||||
if let Some(false) | None = Confirm::new()
|
||||
.with_prompt(format!("Confirm deletion of folder {folder}?"))
|
||||
.default(false)
|
||||
.report(false)
|
||||
.interact_opt()?
|
||||
{
|
||||
process::exit(0);
|
||||
};
|
||||
|
||||
backend.delete_folder(folder).await?;
|
||||
printer.print("Folder successfully deleted!")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_trait::async_trait;
|
||||
use pimalaya_email::{
|
||||
account::AccountConfig,
|
||||
backend::Backend,
|
||||
email::{Envelope, Envelopes, Flags, Messages},
|
||||
folder::{Folder, Folders},
|
||||
};
|
||||
use std::{any::Any, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::printer::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_should_list_mboxes() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.content = String::default();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> anyhow::Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestBackend;
|
||||
|
||||
#[async_trait]
|
||||
impl Backend for TestBackend {
|
||||
fn name(&self) -> String {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn add_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn list_folders(&mut self) -> pimalaya_email::Result<Folders> {
|
||||
Ok(Folders::from_iter([
|
||||
Folder {
|
||||
name: "INBOX".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
Folder {
|
||||
name: "Sent".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
]))
|
||||
}
|
||||
async fn expunge_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn purge_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn delete_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn get_envelope(&mut self, _: &str, _: &str) -> pimalaya_email::Result<Envelope> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn list_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> pimalaya_email::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn search_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> pimalaya_email::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn add_email(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &[u8],
|
||||
_: &Flags,
|
||||
) -> pimalaya_email::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn get_emails(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
) -> pimalaya_email::Result<Messages> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn preview_emails(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
) -> pimalaya_email::Result<Messages> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn copy_emails(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
) -> pimalaya_email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn move_emails(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
) -> pimalaya_email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> pimalaya_email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn add_flags(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
_: &Flags,
|
||||
) -> pimalaya_email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn set_flags(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
_: &Flags,
|
||||
) -> pimalaya_email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn remove_flags(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
_: &Flags,
|
||||
) -> pimalaya_email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let account_config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
|
||||
assert!(list(&account_config, &mut printer, &mut backend, None)
|
||||
.await
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │DESC \n",
|
||||
"INBOX │desc \n",
|
||||
"Sent │desc \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
pub mod folder;
|
||||
pub use folder::*;
|
||||
|
||||
pub mod folders;
|
||||
pub use folders::*;
|
||||
|
||||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -1,16 +0,0 @@
|
|||
pub mod account;
|
||||
pub mod backend;
|
||||
pub mod email;
|
||||
pub mod envelope;
|
||||
pub mod flag;
|
||||
pub mod folder;
|
||||
pub mod sender;
|
||||
pub mod tpl;
|
||||
|
||||
pub use self::account::{args, handlers, Account, Accounts};
|
||||
pub use self::backend::*;
|
||||
pub use self::email::*;
|
||||
pub use self::envelope::*;
|
||||
pub use self::flag::*;
|
||||
pub use self::folder::*;
|
||||
pub use self::tpl::*;
|
|
@ -1,4 +0,0 @@
|
|||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
pub mod smtp;
|
||||
pub(crate) mod wizard;
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,17 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use pimalaya_email::sender::{SenderConfig, SendmailConfig};
|
||||
|
||||
use crate::config::wizard::THEME;
|
||||
|
||||
pub(crate) fn configure() -> Result<SenderConfig> {
|
||||
let mut config = SendmailConfig::default();
|
||||
|
||||
config.cmd = Input::with_theme(&*THEME)
|
||||
.with_prompt("Sendmail-compatible shell command to send emails")
|
||||
.default(String::from("/usr/bin/msmtp"))
|
||||
.interact()?
|
||||
.into();
|
||||
|
||||
Ok(SenderConfig::Sendmail(config))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -1,221 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use pimalaya_email::{
|
||||
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
|
||||
sender::{SenderConfig, SmtpAuthConfig, SmtpConfig},
|
||||
};
|
||||
use pimalaya_oauth2::{AuthorizationCodeGrant, Client};
|
||||
use pimalaya_secret::Secret;
|
||||
|
||||
use crate::{
|
||||
config::wizard::{prompt_passwd, THEME},
|
||||
wizard_log, wizard_prompt,
|
||||
};
|
||||
|
||||
const SSL_TLS: &str = "SSL/TLS";
|
||||
const STARTTLS: &str = "STARTTLS";
|
||||
const NONE: &str = "None";
|
||||
const PROTOCOLS: &[&str] = &[SSL_TLS, STARTTLS, NONE];
|
||||
|
||||
const PASSWD: &str = "Password";
|
||||
const OAUTH2: &str = "OAuth 2.0";
|
||||
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];
|
||||
|
||||
const XOAUTH2: &str = "XOAUTH2";
|
||||
const OAUTHBEARER: &str = "OAUTHBEARER";
|
||||
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
||||
|
||||
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
|
||||
const KEYRING: &str = "Ask the password, then save it in my system's global keyring";
|
||||
const RAW: &str = "Ask the password, then save it in the configuration file (not safe)";
|
||||
const CMD: &str = "Use a shell command that exposes the password";
|
||||
|
||||
pub(crate) fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
|
||||
let mut config = SmtpConfig::default();
|
||||
|
||||
config.host = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP host")
|
||||
.default(format!("smtp.{}", email.rsplit_once('@').unwrap().1))
|
||||
.interact()?;
|
||||
|
||||
let protocol = Select::with_theme(&*THEME)
|
||||
.with_prompt("SMTP security protocol")
|
||||
.items(PROTOCOLS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let default_port = match protocol {
|
||||
Some(idx) if PROTOCOLS[idx] == SSL_TLS => {
|
||||
config.ssl = Some(true);
|
||||
465
|
||||
}
|
||||
Some(idx) if PROTOCOLS[idx] == STARTTLS => {
|
||||
config.starttls = Some(true);
|
||||
587
|
||||
}
|
||||
_ => 25,
|
||||
};
|
||||
|
||||
config.port = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP port")
|
||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||
.default(default_port.to_string())
|
||||
.interact()
|
||||
.map(|input| input.parse::<u16>().unwrap())?;
|
||||
|
||||
config.login = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP login")
|
||||
.default(email.to_owned())
|
||||
.interact()?;
|
||||
|
||||
let auth = Select::with_theme(&*THEME)
|
||||
.with_prompt("SMTP authentication mechanism")
|
||||
.items(AUTH_MECHANISMS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
config.auth = match auth {
|
||||
Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
|
||||
let secret = Select::with_theme(&*THEME)
|
||||
.with_prompt("SMTP authentication strategy")
|
||||
.items(SECRETS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let config = match secret {
|
||||
Some(idx) if SECRETS[idx] == KEYRING => {
|
||||
Secret::new_keyring_entry(format!("{account_name}-smtp-passwd"))
|
||||
.set_keyring_entry_secret(prompt_passwd("SMTP password")?)?;
|
||||
PasswdConfig::default()
|
||||
}
|
||||
Some(idx) if SECRETS[idx] == RAW => PasswdConfig {
|
||||
passwd: Secret::Raw(prompt_passwd("SMTP password")?),
|
||||
},
|
||||
Some(idx) if SECRETS[idx] == CMD => PasswdConfig {
|
||||
passwd: Secret::new_cmd(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Shell command")
|
||||
.default(format!("pass show {account_name}-smtp-passwd"))
|
||||
.interact()?,
|
||||
),
|
||||
},
|
||||
_ => PasswdConfig::default(),
|
||||
};
|
||||
SmtpAuthConfig::Passwd(config)
|
||||
}
|
||||
Some(idx) if AUTH_MECHANISMS[idx] == OAUTH2 => {
|
||||
let mut config = OAuth2Config::default();
|
||||
|
||||
let method = Select::with_theme(&*THEME)
|
||||
.with_prompt("SMTP OAuth 2.0 mechanism")
|
||||
.items(OAUTH2_MECHANISMS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
config.method = match method {
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||
_ => OAuth2Method::XOAuth2,
|
||||
};
|
||||
|
||||
config.client_id = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP OAuth 2.0 client id")
|
||||
.interact()?;
|
||||
|
||||
let client_secret: String = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP OAuth 2.0 client secret")
|
||||
.interact()?;
|
||||
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))
|
||||
.set_keyring_entry_secret(&client_secret)?;
|
||||
|
||||
config.auth_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP OAuth 2.0 authorization URL")
|
||||
.interact()?;
|
||||
|
||||
config.token_url = Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP OAuth 2.0 token URL")
|
||||
.interact()?;
|
||||
|
||||
config.scopes = OAuth2Scopes::Scope(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("SMTP OAuth 2.0 main scope")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
while Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to add more SMTP OAuth 2.0 scopes?"
|
||||
))
|
||||
.default(false)
|
||||
.interact_opt()?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let mut scopes = match config.scopes {
|
||||
OAuth2Scopes::Scope(scope) => vec![scope],
|
||||
OAuth2Scopes::Scopes(scopes) => scopes,
|
||||
};
|
||||
|
||||
scopes.push(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Additional SMTP OAuth 2.0 scope")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
config.scopes = OAuth2Scopes::Scopes(scopes);
|
||||
}
|
||||
|
||||
config.pkce = Confirm::new()
|
||||
.with_prompt(wizard_prompt!(
|
||||
"Would you like to enable PKCE verification?"
|
||||
))
|
||||
.default(true)
|
||||
.interact_opt()?
|
||||
.unwrap_or(true);
|
||||
|
||||
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
|
||||
|
||||
let client = Client::new(
|
||||
config.client_id.clone(),
|
||||
client_secret,
|
||||
config.auth_url.clone(),
|
||||
config.token_url.clone(),
|
||||
)?
|
||||
.with_redirect_host(config.redirect_host.clone())
|
||||
.with_redirect_port(config.redirect_port)
|
||||
.build()?;
|
||||
|
||||
let mut auth_code_grant = AuthorizationCodeGrant::new()
|
||||
.with_redirect_host(config.redirect_host.clone())
|
||||
.with_redirect_port(config.redirect_port);
|
||||
|
||||
if config.pkce {
|
||||
auth_code_grant = auth_code_grant.with_pkce();
|
||||
}
|
||||
|
||||
for scope in config.scopes.clone() {
|
||||
auth_code_grant = auth_code_grant.with_scope(scope);
|
||||
}
|
||||
|
||||
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
|
||||
|
||||
println!("{}", redirect_url.to_string());
|
||||
println!("");
|
||||
|
||||
let (access_token, refresh_token) =
|
||||
auth_code_grant.wait_for_redirection(&client, csrf_token)?;
|
||||
|
||||
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))
|
||||
.set_keyring_entry_secret(access_token)?;
|
||||
|
||||
if let Some(refresh_token) = &refresh_token {
|
||||
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-refresh-token"))
|
||||
.set_keyring_entry_secret(refresh_token)?;
|
||||
}
|
||||
|
||||
SmtpAuthConfig::OAuth2(config)
|
||||
}
|
||||
_ => SmtpAuthConfig::default(),
|
||||
};
|
||||
|
||||
Ok(SenderConfig::Smtp(config))
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use dialoguer::Select;
|
||||
use pimalaya_email::sender::SenderConfig;
|
||||
|
||||
use crate::config::wizard::THEME;
|
||||
|
||||
use super::sendmail;
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
use super::smtp;
|
||||
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
const SMTP: &str = "SMTP";
|
||||
const SENDMAIL: &str = "Sendmail";
|
||||
const NONE: &str = "None";
|
||||
|
||||
const SENDERS: &[&str] = &[
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
SMTP,
|
||||
SENDMAIL,
|
||||
NONE,
|
||||
];
|
||||
|
||||
pub(crate) fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
|
||||
let sender = Select::with_theme(&*THEME)
|
||||
.with_prompt("Email sender")
|
||||
.items(SENDERS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
match sender {
|
||||
#[cfg(feature = "smtp-sender")]
|
||||
Some(n) if SENDERS[n] == SMTP => smtp::wizard::configure(account_name, email),
|
||||
Some(n) if SENDERS[n] == SENDMAIL => sendmail::wizard::configure(),
|
||||
_ => Ok(SenderConfig::None),
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
//! Module related to email template CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher
|
||||
//! related to email templating.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use log::warn;
|
||||
|
||||
use crate::email;
|
||||
|
||||
const ARG_BODY: &str = "body";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
const ARG_TPL: &str = "template";
|
||||
const CMD_FORWARD: &str = "forward";
|
||||
const CMD_REPLY: &str = "reply";
|
||||
const CMD_SAVE: &str = "save";
|
||||
const CMD_SEND: &str = "send";
|
||||
const CMD_WRITE: &str = "write";
|
||||
|
||||
pub const CMD_TPL: &str = "template";
|
||||
|
||||
pub type RawTpl = String;
|
||||
pub type Headers<'a> = Option<Vec<(&'a str, &'a str)>>;
|
||||
pub type Body<'a> = Option<&'a str>;
|
||||
|
||||
/// Represents the template commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Forward(email::args::Id<'a>, Headers<'a>, Body<'a>),
|
||||
Write(Headers<'a>, Body<'a>),
|
||||
Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>),
|
||||
Save(RawTpl),
|
||||
Send(RawTpl),
|
||||
}
|
||||
|
||||
/// Represents the template command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Forward(id, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let all = email::args::parse_reply_all_flag(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Reply(id, all, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
let raw_tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Save(raw_tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
||||
let raw_tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Send(raw_tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Write(headers, body))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the template subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![Command::new(CMD_TPL)
|
||||
.alias("tpl")
|
||||
.about("Handles email templates")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
Command::new(CMD_FORWARD)
|
||||
.alias("fwd")
|
||||
.about("Generates a template for forwarding an email")
|
||||
.arg(email::args::id_arg())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_REPLY)
|
||||
.about("Generates a template for replying to an email")
|
||||
.arg(email::args::id_arg())
|
||||
.arg(email::args::reply_all_flag())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SAVE)
|
||||
.about("Compiles the template into a valid email then saves it")
|
||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SEND)
|
||||
.about("Compiles the template into a valid email then sends it")
|
||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_WRITE)
|
||||
.aliases(["new", "n"])
|
||||
.about("Generates a template for writing a new email")
|
||||
.args(&args()),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Represents the template arguments.
|
||||
pub fn args() -> Vec<Arg> {
|
||||
vec![
|
||||
Arg::new(ARG_HEADERS)
|
||||
.help("Overrides a specific header")
|
||||
.short('H')
|
||||
.long("header")
|
||||
.value_name("KEY:VAL")
|
||||
.action(ArgAction::Append),
|
||||
Arg::new(ARG_BODY)
|
||||
.help("Overrides the body")
|
||||
.short('B')
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Represents the template headers argument parser.
|
||||
pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> {
|
||||
m.get_many::<String>(ARG_HEADERS).map(|h| {
|
||||
h.filter_map(|h| match h.split_once(':') {
|
||||
Some((key, val)) => Some((key, val.trim())),
|
||||
None => {
|
||||
warn!("invalid raw header {h:?}, skipping it");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Represents the template body argument parser.
|
||||
pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> {
|
||||
matches.get_one::<String>(ARG_BODY).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the raw template argument parser.
|
||||
pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl {
|
||||
matches
|
||||
.get_one::<String>(ARG_TPL)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use atty::Stream;
|
||||
use pimalaya_email::{
|
||||
account::AccountConfig,
|
||||
backend::Backend,
|
||||
email::{Flags, Message, Tpl},
|
||||
sender::Sender,
|
||||
};
|
||||
use std::io::{stdin, BufRead};
|
||||
|
||||
use crate::{printer::Printer, IdMapper};
|
||||
|
||||
pub async fn forward<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
headers: Option<Vec<(&str, &str)>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids([id])?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
|
||||
let tpl: String = backend
|
||||
.get_emails(folder, ids)
|
||||
.await?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_forward_tpl_builder(config)
|
||||
.with_some_headers(headers)
|
||||
.with_some_body(body)
|
||||
.build()
|
||||
.await?
|
||||
.into();
|
||||
|
||||
printer.print(tpl)
|
||||
}
|
||||
|
||||
pub async fn reply<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
all: bool,
|
||||
headers: Option<Vec<(&str, &str)>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let ids = id_mapper.get_ids([id])?;
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
|
||||
let tpl: String = backend
|
||||
.get_emails(folder, ids)
|
||||
.await?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_reply_tpl_builder(config)
|
||||
.with_some_headers(headers)
|
||||
.with_some_body(body)
|
||||
.with_reply_all(all)
|
||||
.build()
|
||||
.await?
|
||||
.into();
|
||||
|
||||
printer.print(tpl)
|
||||
}
|
||||
|
||||
pub async fn save<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
id_mapper: &IdMapper,
|
||||
backend: &mut dyn Backend,
|
||||
folder: &str,
|
||||
tpl: String,
|
||||
) -> Result<()> {
|
||||
let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
})
|
||||
.some_pgp_sign_cmd(config.email_writing_sign_cmd.clone())
|
||||
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.clone())
|
||||
.compile()
|
||||
.await?
|
||||
.write_to_vec()?;
|
||||
|
||||
let id = backend.add_email(folder, &email, &Flags::default()).await?;
|
||||
id_mapper.create_alias(id)?;
|
||||
|
||||
printer.print("Template successfully saved!")
|
||||
}
|
||||
|
||||
pub async fn send<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut dyn Backend,
|
||||
sender: &mut dyn Sender,
|
||||
folder: &str,
|
||||
tpl: String,
|
||||
) -> Result<()> {
|
||||
let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
})
|
||||
.some_pgp_sign_cmd(config.email_writing_sign_cmd.clone())
|
||||
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.clone())
|
||||
.compile()
|
||||
.await?
|
||||
.write_to_vec()?;
|
||||
|
||||
sender.send(&email).await?;
|
||||
|
||||
if config.email_sending_save_copy {
|
||||
backend.add_email(folder, &email, &Flags::default()).await?;
|
||||
}
|
||||
|
||||
printer.print("Template successfully sent!")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
headers: Option<Vec<(&str, &str)>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tpl: String = Message::new_tpl_builder(config)
|
||||
.with_some_headers(headers)
|
||||
.with_some_body(body)
|
||||
.build()
|
||||
.await?
|
||||
.into();
|
||||
|
||||
printer.print(tpl)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
17
src/email/envelope/arg/ids.rs
Normal file
17
src/email/envelope/arg/ids.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use clap::Parser;
|
||||
|
||||
/// The envelope id argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct EnvelopeIdArg {
|
||||
/// The envelope id.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
/// The envelopes ids arguments parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct EnvelopeIdsArgs {
|
||||
/// The list of envelopes ids.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<usize>,
|
||||
}
|
1
src/email/envelope/arg/mod.rs
Normal file
1
src/email/envelope/arg/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod ids;
|
218
src/email/envelope/command/list.rs
Normal file
218
src/email/envelope/command/list.rs
Normal file
|
@ -0,0 +1,218 @@
|
|||
use std::{process::exit, sync::Arc};
|
||||
|
||||
use ariadne::{Color, Label, Report, ReportKind, Source};
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{
|
||||
backend::feature::BackendFeatureSource, 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, config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Search and sort envelopes as a list.
|
||||
///
|
||||
/// This command allows you to list envelopes included in the given
|
||||
/// folder, matching the given query.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct EnvelopeListCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
/// The page number.
|
||||
///
|
||||
/// The page number starts from 1 (which is the default). Giving a
|
||||
/// page number to big will result in a out of bound error.
|
||||
#[arg(long, short, value_name = "NUMBER", default_value = "1")]
|
||||
pub page: usize,
|
||||
|
||||
/// The page size.
|
||||
///
|
||||
/// Determine the amount of envelopes a page should contain.
|
||||
#[arg(long, short = 's', value_name = "NUMBER")]
|
||||
pub page_size: Option<usize>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
/// The maximum width the table should not exceed.
|
||||
///
|
||||
/// This argument will force the table not to exceed the given
|
||||
/// width in pixels. Columns may shrink with ellipsis in order to
|
||||
/// fit the width.
|
||||
#[arg(long = "max-width", short = 'w')]
|
||||
#[arg(name = "table_max_width", value_name = "PIXELS")]
|
||||
pub table_max_width: Option<u16>,
|
||||
|
||||
/// The list envelopes filter and sort query.
|
||||
///
|
||||
/// The query can be a filter query, a sort query or both
|
||||
/// together.
|
||||
///
|
||||
/// A filter query is composed of operators and conditions. There
|
||||
/// is 3 operators and 8 conditions:
|
||||
///
|
||||
/// • not <condition> → filter envelopes that do not match the
|
||||
/// condition
|
||||
///
|
||||
/// • <condition> and <condition> → filter envelopes that match
|
||||
/// both conditions
|
||||
///
|
||||
/// • <condition> or <condition> → filter envelopes that match
|
||||
/// one of the conditions
|
||||
///
|
||||
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
|
||||
/// date
|
||||
///
|
||||
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
|
||||
/// before the given one
|
||||
///
|
||||
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
|
||||
/// after the given one
|
||||
///
|
||||
/// ◦ from <pattern> → filter envelopes with senders matching the
|
||||
/// given pattern
|
||||
///
|
||||
/// ◦ to <pattern> → filter envelopes with recipients matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ subject <pattern> → filter envelopes with subject matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ body <pattern> → filter envelopes with text bodies matching
|
||||
/// the given pattern
|
||||
///
|
||||
/// ◦ flag <flag> → filter envelopes matching the given flag
|
||||
///
|
||||
/// A sort query starts by "order by", and is composed of kinds
|
||||
/// and orders. There is 4 kinds and 2 orders:
|
||||
///
|
||||
/// • date [order] → sort envelopes by date
|
||||
///
|
||||
/// • from [order] → sort envelopes by sender
|
||||
///
|
||||
/// • to [order] → sort envelopes by recipient
|
||||
///
|
||||
/// • subject [order] → sort envelopes by subject
|
||||
///
|
||||
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
|
||||
/// order
|
||||
///
|
||||
/// ◦ <kind> desc → sort envelopes by the given kind in
|
||||
/// descending order
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// subject foo and body bar → filter envelopes containing "foo"
|
||||
/// in their subject and "bar" in their text bodies
|
||||
///
|
||||
/// order by date desc subject → sort envelopes by descending date
|
||||
/// (most recent first), then by ascending subject
|
||||
///
|
||||
/// subject foo and body bar order by date desc subject →
|
||||
/// combination of the 2 previous examples
|
||||
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
|
||||
pub query: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for EnvelopeListCommand {
|
||||
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)
|
||||
}
|
||||
}
|
35
src/email/envelope/command/mod.rs
Normal file
35
src/email/envelope/command/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
pub mod list;
|
||||
pub mod thread;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
|
||||
|
||||
/// 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
|
||||
/// headers from the message itself. This subcommand allows you to
|
||||
/// manage them.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum EnvelopeSubcommand {
|
||||
#[command(alias = "lst")]
|
||||
List(EnvelopeListCommand),
|
||||
|
||||
#[command()]
|
||||
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::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)
|
||||
// }
|
||||
// }
|
48
src/email/envelope/flag/arg/ids_and_flags.rs
Normal file
48
src/email/envelope/flag/arg/ids_and_flags.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use clap::Parser;
|
||||
use email::flag::{Flag, Flags};
|
||||
use tracing::debug;
|
||||
|
||||
/// The ids and/or flags arguments parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct IdsAndFlagsArgs {
|
||||
/// The list of ids and/or flags.
|
||||
///
|
||||
/// Every argument that can be parsed as an integer is considered
|
||||
/// an id, otherwise it is considered as a flag.
|
||||
#[arg(value_name = "ID-OR-FLAG", required = true)]
|
||||
pub ids_and_flags: Vec<IdOrFlag>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum IdOrFlag {
|
||||
Id(usize),
|
||||
Flag(Flag),
|
||||
}
|
||||
|
||||
impl From<&str> for IdOrFlag {
|
||||
fn from(value: &str) -> Self {
|
||||
value.parse::<usize>().map(Self::Id).unwrap_or_else(|err| {
|
||||
let flag = Flag::from(value);
|
||||
debug!("cannot parse {value} as usize, parsing it as flag {flag}");
|
||||
debug!("{err:?}");
|
||||
Self::Flag(flag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec<usize>, Flags) {
|
||||
ids_and_flags.iter().fold(
|
||||
(Vec::default(), Flags::default()),
|
||||
|(mut ids, mut flags), arg| {
|
||||
match arg {
|
||||
IdOrFlag::Id(id) => {
|
||||
ids.push(*id);
|
||||
}
|
||||
IdOrFlag::Flag(flag) => {
|
||||
flags.insert(flag.to_owned());
|
||||
}
|
||||
};
|
||||
(ids, flags)
|
||||
},
|
||||
)
|
||||
}
|
1
src/email/envelope/flag/arg/mod.rs
Normal file
1
src/email/envelope/flag/arg/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod ids_and_flags;
|
64
src/email/envelope/flag/command/add.rs
Normal file
64
src/email/envelope/flag/command/add.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
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::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
config::TomlConfig,
|
||||
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Add flag(s) to the given envelope.
|
||||
///
|
||||
/// This command allows you to attach the given flag(s) to the given
|
||||
/// envelope(s).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FlagAddCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FlagAddCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing add flag(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let 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.out(format!("Flag(s) {flags} successfully added!\n"))
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue