mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-21 08:43:39 +00:00
Compare commits
554 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 | ||
![]() |
5c360da80b | ||
![]() |
34ad1add65 | ||
![]() |
cb1178ee9d | ||
![]() |
679007ba64 | ||
![]() |
4e43b97513 | ||
![]() |
2f4bbcb1db | ||
![]() |
e821fe06d9 | ||
![]() |
cac8280c8c | ||
![]() |
f8ca248bce | ||
![]() |
7a6ebc0cd0 | ||
![]() |
5599a1f5d0 | ||
![]() |
5a17ae7316 | ||
![]() |
7d96ca52fa | ||
![]() |
0f6f3439fb | ||
![]() |
c254f64569 | ||
![]() |
ab1e8b7e45 | ||
![]() |
b5a5b0d42f | ||
![]() |
ff004f0c2a | ||
![]() |
696834c8dc | ||
![]() |
308805db17 | ||
![]() |
dfff9064d7 | ||
![]() |
7aff3bbf9d | ||
![]() |
b800d6e6fc | ||
![]() |
d557d9e1df | ||
![]() |
32b31db175 | ||
![]() |
65ac0c7702 | ||
![]() |
5a2d842cbe | ||
![]() |
5da1148dc9 | ||
![]() |
d814ae904a | ||
![]() |
0ff77b5179 | ||
![]() |
53538e36f9 | ||
![]() |
f8eed6ad14 | ||
![]() |
f4facd1761 | ||
![]() |
54ea9a3302 | ||
![]() |
441ce40e09 | ||
![]() |
728f2555d7 | ||
![]() |
5749bc3a82 | ||
![]() |
5d21433816 | ||
![]() |
b478c545ad | ||
![]() |
9dfdebb396 | ||
![]() |
e6c9a6e90e | ||
![]() |
f026e48733 | ||
![]() |
21f67bc7f5 | ||
![]() |
e271ca4293 | ||
![]() |
ae6fe9a7c1 | ||
![]() |
7844a79009 | ||
![]() |
30599931bc | ||
![]() |
a7e9c560c2 | ||
![]() |
9453f83c94 | ||
![]() |
5d00e0bef0 | ||
![]() |
f66679318d | ||
![]() |
84003f951a | ||
![]() |
9cf5003697 | ||
![]() |
072f488d89 | ||
![]() |
7b3a9e4cc7 | ||
![]() |
69590f6986 | ||
![]() |
d5efd03bcd | ||
![]() |
501c7f18f5 | ||
![]() |
55f5de1803 | ||
![]() |
fb324878fa | ||
![]() |
22fb1b8dee | ||
![]() |
21d8d57f72 | ||
![]() |
5734b30fd1 | ||
![]() |
3631ca714b | ||
![]() |
0ab652b4b6 | ||
![]() |
4b3280cbbb | ||
![]() |
bfb572acbd | ||
![]() |
efd24251e0 | ||
![]() |
beba35d57e | ||
![]() |
19d8296324 | ||
![]() |
7fbe39b8fb | ||
![]() |
3cc1ed7583 | ||
![]() |
509b09d533 | ||
![]() |
694173b534 | ||
![]() |
bda37ca0ed | ||
![]() |
d29b227c4b | ||
![]() |
6a15b742b0 | ||
![]() |
bb8f63e4b0 | ||
![]() |
98929d687b | ||
![]() |
29f2bdd931 | ||
![]() |
9630a6f108 | ||
![]() |
285d9d0521 |
143 changed files with 10877 additions and 5967 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
|
||||
|
|
85
.github/workflows/deployment.yaml
vendored
85
.github/workflows/deployment.yaml
vendored
|
@ -1,85 +0,0 @@
|
|||
name: deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
deploy_github:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: create_release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
os_name: linux
|
||||
- os: macos-latest
|
||||
os_name: macos
|
||||
- os: windows-latest
|
||||
os_name: windows
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Check project
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
- name: Build release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release
|
||||
- name: Compress executable (unix)
|
||||
if: matrix.os_name == 'linux' || matrix.os_name == 'macos'
|
||||
run: tar czf himalaya.tar.gz -C target/release himalaya
|
||||
- name: Compress executable (windows)
|
||||
if: matrix.os_name == 'windows'
|
||||
run: tar czf himalaya.tar.gz -C target/release himalaya.exe
|
||||
- name: Upload release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: himalaya.tar.gz
|
||||
asset_name: himalaya-${{ matrix.os_name }}.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
deploy_crates:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create_release
|
||||
environment: deployment
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v1
|
||||
with:
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
22
.github/workflows/nix-build.yaml
vendored
22
.github/workflows/nix-build.yaml
vendored
|
@ -1,22 +0,0 @@
|
|||
name: nix-build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
nix-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: cachix/install-nix-action@v13
|
||||
with:
|
||||
install_url: https://nixos-nix-install-tests.cachix.org/serve/i6laym9jw3wg9mw6ncyrk6gjx4l34vvx/install
|
||||
install_options: '--tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve'
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- run: nix develop -c rustc --version
|
||||
- run: nix run . -- --version
|
||||
- run: nix-build
|
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 }}
|
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
|
38
.github/workflows/tests.yaml
vendored
38
.github/workflows/tests.yaml
vendored
|
@ -1,38 +0,0 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install libnotmuch
|
||||
run: sudo apt-get install -y libnotmuch-dev
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Start GreenMail testing server
|
||||
run: |
|
||||
docker run \
|
||||
--rm \
|
||||
-d \
|
||||
-e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' \
|
||||
-p 3025:3025 \
|
||||
-p 3110:3110 \
|
||||
-p 3143:3143 \
|
||||
-p 3465:3465 \
|
||||
-p 3993:3993 \
|
||||
-p 3995:3995 \
|
||||
greenmail/standalone:1.6.2
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
# Cargo config directory
|
||||
.cargo/
|
||||
|
||||
# Cargo build directory
|
||||
target/
|
||||
debug/
|
||||
|
|
1023
CHANGELOG.md
1023
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -1,42 +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!
|
||||
|
||||
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
|
||||
## Development
|
||||
|
||||
## New contributor guide
|
||||
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.
|
||||
|
||||
To get an overview of the project, read the [README](README.md). To get more information about the project, read the [wiki](https://github.com/soywod/himalaya/wiki).
|
||||
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
|
||||
|
||||
## Getting started
|
||||
```text
|
||||
rustup update
|
||||
```
|
||||
|
||||
### Issues
|
||||
or install manually the following dependencies:
|
||||
|
||||
#### Create a new issue
|
||||
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
|
||||
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
|
||||
|
||||
If you spot a problem with the docs, [search if an issue already exists](https://github.com/soywod/himalaya/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/soywod/himalaya/issues/new/choose).
|
||||
## Build
|
||||
|
||||
#### Solve an issue
|
||||
```text
|
||||
cargo build
|
||||
```
|
||||
|
||||
Scan through our [existing issues](https://github.com/soywod/himalaya/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
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`.
|
||||
|
||||
### Make Changes
|
||||
Finally, you can build a release with `--release`:
|
||||
|
||||
#### Make changes in the UI
|
||||
```text
|
||||
cargo build --no-default-features --features imap,smtp,keyring --release
|
||||
```
|
||||
|
||||
Click **Make a contribution** at the bottom of any docs page to make small changes such as a typo, sentence fix, or a broken link. This takes you to the `.md` file where you can make your changes and [create a pull request](#pull-request) for a review.
|
||||
## Override dependencies
|
||||
|
||||
#### Make changes locally
|
||||
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`:
|
||||
|
||||
First, follow the instructions on [how to install Himalaya from sources](https://github.com/soywod/himalaya/wiki/Installation:sources). Then, create a working branch and start with your changes!
|
||||
```toml
|
||||
[patch.crates-io]
|
||||
email-lib = { path = "/path/to/email-lib" }
|
||||
```
|
||||
|
||||
### Commit your update
|
||||
If you get the following error:
|
||||
|
||||
Commit the changes once you are happy with them. Commit messages follow the [Angular Convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153), but contain only a subject. The subject can be prefixed with a custom context like `msg: `, `mbox: `, `imap: ` etc.
|
||||
```text
|
||||
note: perhaps two different versions of crate email are being used?
|
||||
```
|
||||
|
||||
> Use imperative, present tense: “change” not “changed” nor
|
||||
> “changes”<br>Don't capitalize first letter<br>No dot (.) at the end
|
||||
|
||||
### Pull Request
|
||||
then you may need to override more Pimalaya's sub-dependencies:
|
||||
|
||||
When you're finished with the changes, create a pull request, also known as a PR.
|
||||
```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).
|
||||
|
|
5138
Cargo.lock
generated
5138
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
102
Cargo.toml
102
Cargo.toml
|
@ -1,62 +1,70 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management."
|
||||
version = "0.6.0"
|
||||
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://github.com/soywod/himalaya"
|
||||
documentation = "https://github.com/soywod/himalaya/wiki"
|
||||
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.deb]
|
||||
priority = "optional"
|
||||
section = "mail"
|
||||
[package.metadata.docs.rs]
|
||||
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
imap-backend = ["imap", "imap-proto"]
|
||||
maildir-backend = ["maildir", "md5"]
|
||||
notmuch-backend = ["himalaya-lib/notmuch-backend", "maildir-backend", "notmuch"]
|
||||
default = ["imap-backend", "maildir-backend"]
|
||||
default = ["imap", "maildir", "smtp", "sendmail", "wizard", "pgp-commands"]
|
||||
imap = ["email-lib/imap", "pimalaya-tui/imap"]
|
||||
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
|
||||
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
|
||||
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
|
||||
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
|
||||
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
|
||||
oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
|
||||
wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
|
||||
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"]
|
||||
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"]
|
||||
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"]
|
||||
|
||||
[build-dependencies]
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
|
||||
|
||||
[dependencies]
|
||||
ammonia = "3.1.2"
|
||||
anyhow = "1.0.44"
|
||||
atty = "0.2.14"
|
||||
chrono = "0.4.19"
|
||||
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
|
||||
convert_case = "0.5.0"
|
||||
env_logger = "0.8.3"
|
||||
erased-serde = "0.3.18"
|
||||
# himalaya-lib = { version = "=0.3.1", git = "https://git.sr.ht/~soywod/himalaya-lib", branch = "develop" }
|
||||
himalaya-lib = "=0.3.1"
|
||||
html-escape = "0.2.9"
|
||||
lettre = { version = "=0.10.0-rc.7", features = ["serde"] }
|
||||
log = "0.4.14"
|
||||
mailparse = "0.13.6"
|
||||
native-tls = "0.2.8"
|
||||
regex = "1.5.4"
|
||||
rfc2047-decoder = "0.1.2"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_json = "1.0.61"
|
||||
shellexpand = "2.1.0"
|
||||
termcolor = "1.1"
|
||||
terminal_size = "0.1.15"
|
||||
toml = "0.5.8"
|
||||
tree_magic = "0.2.3"
|
||||
unicode-width = "0.1.7"
|
||||
url = "2.2.2"
|
||||
ariadne = "0.2"
|
||||
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.4"
|
||||
clap_mangen = "0.2"
|
||||
color-eyre = "0.6"
|
||||
email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
|
||||
mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
|
||||
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.8"
|
||||
tracing = "0.1"
|
||||
url = "2.2"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
# Optional dependencies:
|
||||
[patch.crates-io]
|
||||
imap-codec.git = "https://github.com/duesee/imap-codec"
|
||||
|
||||
imap = { version = "=3.0.0-alpha.4", optional = true }
|
||||
imap-proto = { version = "0.14.3", optional = true }
|
||||
maildir = { version = "0.6.1", optional = true }
|
||||
md5 = { version = "0.7.0", optional = true }
|
||||
notmuch = { version = "0.7.1", optional = true }
|
||||
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
|
||||
|
|
764
README.md
764
README.md
|
@ -1,94 +1,694 @@
|
|||
# 📫 Himalaya
|
||||
<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>
|
||||
|
||||
Command-line interface for email management based on the
|
||||
[himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib).
|
||||
|
||||

|
||||
|
||||
*The project is under active development. Do not use in production
|
||||
before the `v1.0.0`.*
|
||||
|
||||
## Installation
|
||||
|
||||
[](https://formulae.brew.sh/formula/himalaya)
|
||||
[](https://crates.io/crates/himalaya)
|
||||
|
||||
```sh
|
||||
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh
|
||||
```
|
||||
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
|
||||
```
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary)
|
||||
for other installation methods.*
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml
|
||||
# ~/.config/himalaya/config.toml
|
||||
|
||||
display-name = "Test"
|
||||
downloads-dir = "~/downloads"
|
||||
signature = "Regards,"
|
||||
|
||||
[gmail]
|
||||
default = true
|
||||
email = "test@gmail.com"
|
||||
|
||||
backend = "imap" # imap, maildir or notmuch
|
||||
imap-host = "imap.gmail.com"
|
||||
imap-port = 993
|
||||
imap-login = "test@gmail.com"
|
||||
imap-passwd-cmd = "pass show gmail"
|
||||
|
||||
sender = "smtp" # smtp or sendmail
|
||||
smtp-host = "smtp.gmail.com"
|
||||
smtp-port = 465
|
||||
smtp-login = "test@gmail.com"
|
||||
smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
|
||||
```
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file)
|
||||
for all the options.*
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Folder listing
|
||||
- Email listing and searching
|
||||
- Email composition based on `$EDITOR`
|
||||
- Email manipulation (copy/move/delete)
|
||||
- Multi-accounting
|
||||
- Account listing
|
||||
- IMAP, Maildir and Notmuch support
|
||||
- IMAP IDLE mode for real-time notifications
|
||||
- PGP end-to-end encryption
|
||||
- Vim and Emacs plugins
|
||||
- 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)
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Usage:email:list) for
|
||||
all the features.*
|
||||
*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).*
|
||||
|
||||
## Credits
|
||||
## Installation
|
||||
|
||||
- [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 lib
|
||||
<details>
|
||||
<summary>Pre-built binary</summary>
|
||||
|
||||
Himalaya CLI can be installed with the installer:
|
||||
|
||||
*As root:*
|
||||
|
||||
```
|
||||
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | sudo sh
|
||||
```
|
||||
|
||||
*As a regular user:*
|
||||
|
||||
```
|
||||
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh
|
||||
```
|
||||
|
||||
These commands install the latest binary from the GitHub [releases](https://github.com/pimalaya/himalaya/releases) section.
|
||||
|
||||
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.
|
||||
|
||||
*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
|
||||
|
||||
Just run `himalaya`, the wizard will help you to configure your default account.
|
||||
|
||||
Accounts can be (re)configured via the wizard using the command `himalaya account configure <name>`.
|
||||
|
||||
You can also manually edit your own configuration, from scratch:
|
||||
|
||||
- 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
|
||||
|
||||
<details>
|
||||
<summary>Proton Mail (Bridge)</summary>
|
||||
|
||||
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
|
||||
|
||||
- Id order may be reversed or shuffled, but envelopes will still be sorted by date.
|
||||
- SSL/TLS needs to be deactivated manually.
|
||||
- The password to use is the one generated by Proton Bridge, not the one from your Proton Mail account.
|
||||
|
||||
```toml
|
||||
[accounts.proton]
|
||||
email = "example@proton.me"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "127.0.0.1"
|
||||
backend.port = 1143
|
||||
backend.encryption.type = "none"
|
||||
backend.login = "example@proton.me"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "127.0.0.1"
|
||||
message.send.backend.port = 1025
|
||||
message.send.backend.encryption.type = "none"
|
||||
message.send.backend.login = "example@proton.me"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show proton"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "proton-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a proton` will ask for your IMAP password, just paste the one generated previously.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Gmail</summary>
|
||||
|
||||
Google passwords cannot be used directly. There is two ways to authenticate yourself:
|
||||
|
||||
### Using [App Passwords](https://support.google.com/mail/answer/185833)
|
||||
|
||||
This option is the simplest and the fastest. First, be sure that:
|
||||
|
||||
- IMAP is enabled
|
||||
- Two-step authentication is enabled
|
||||
- Less secure app access is enabled
|
||||
|
||||
First create a [dedicated password](https://myaccount.google.com/apppasswords) for Himalaya.
|
||||
|
||||
```toml
|
||||
[accounts.gmail]
|
||||
email = "example@gmail.com"
|
||||
|
||||
folder.aliases.inbox = "INBOX"
|
||||
folder.aliases.sent = "[Gmail]/Sent Mail"
|
||||
folder.aliases.drafts = "[Gmail]/Drafts"
|
||||
folder.aliases.trash = "[Gmail]/Trash"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.gmail.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@gmail.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.gmail.com"
|
||||
message.send.backend.port = 465
|
||||
message.send.backend.login = "example@gmail.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.cmd = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show gmail"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "gmail-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a gmail` will ask for your IMAP password, just paste the one generated previously.
|
||||
|
||||
### Using OAuth 2.0
|
||||
|
||||
This option is the most secure but the hardest to configure. It requires the `oauth2` and `keyring` cargo features.
|
||||
|
||||
First, you need to get your OAuth 2.0 credentials by following [this guide](https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name-.). Once you get your client id and your client secret, you can configure your Himalaya account this way:
|
||||
|
||||
```toml
|
||||
[accounts.gmail]
|
||||
email = "example@gmail.com"
|
||||
|
||||
folder.aliases.inbox = "INBOX"
|
||||
folder.aliases.sent = "[Gmail]/Sent Mail"
|
||||
folder.aliases.drafts = "[Gmail]/Drafts"
|
||||
folder.aliases.trash = "[Gmail]/Trash"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.gmail.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@gmail.com"
|
||||
backend.auth.type = "oauth2"
|
||||
backend.auth.client-id = "*****"
|
||||
backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
|
||||
backend.auth.access-token.keyring = "gmail-oauth2-access-token"
|
||||
backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
|
||||
backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
backend.auth.pkce = true
|
||||
backend.auth.scope = "https://mail.google.com/"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.gmail.com"
|
||||
message.send.backend.port = 465
|
||||
message.send.backend.login = "example@gmail.com"
|
||||
message.send.backend.auth.type = "oauth2"
|
||||
message.send.backend.auth.client-id = "*****"
|
||||
message.send.backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
|
||||
message.send.backend.auth.access-token.keyring = "gmail-oauth2-access-token"
|
||||
message.send.backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
|
||||
message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
|
||||
message.send.backend.auth.pkce = true
|
||||
message.send.backend.auth.scope = "https://mail.google.com/"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a gmail` will complete your OAuth 2.0 setup and ask for your client secret.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Outlook</summary>
|
||||
|
||||
```toml
|
||||
[accounts.outlook]
|
||||
email = "example@outlook.com"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "outlook.office365.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@outlook.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp-mail.outlook.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "example@outlook.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show outlook"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "outlook-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a outlook` will ask for your IMAP password, just paste the one generated previously.
|
||||
|
||||
### Using OAuth 2.0
|
||||
|
||||
This option is the most secure but the hardest to configure. First, you need to get your OAuth 2.0 credentials by following [this guide](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth). Once you get your client id and your client secret, you can configure your Himalaya account this way:
|
||||
|
||||
```toml
|
||||
[accounts.outlook]
|
||||
email = "example@outlook.com"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "outlook.office365.com"
|
||||
backend.port = 993
|
||||
backend.login = "example@outlook.com"
|
||||
backend.auth.type = "oauth2"
|
||||
backend.auth.client-id = "*****"
|
||||
backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
|
||||
backend.auth.access-token.keyring = "outlook-oauth2-access-token"
|
||||
backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
|
||||
backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||
backend.auth.pkce = true
|
||||
backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.mail.outlook.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.starttls = true
|
||||
message.send.backend.login = "example@outlook.com"
|
||||
message.send.backend.auth.type = "oauth2"
|
||||
message.send.backend.auth.client-id = "*****"
|
||||
message.send.backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
|
||||
message.send.backend.auth.access-token.keyring = "outlook-oauth2-access-token"
|
||||
message.send.backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
|
||||
message.send.backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
message.send.backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||
message.send.backend.auth.pkce = true
|
||||
message.send.backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
|
||||
```
|
||||
|
||||
Running `himalaya configure -a outlook` will complete your OAuth 2.0 setup and ask for your client secret.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>iCloud Mail</summary>
|
||||
|
||||
From the [iCloud Mail](https://support.apple.com/en-us/HT202304) support page:
|
||||
|
||||
- IMAP port = `993`.
|
||||
- IMAP login = name of your iCloud Mail email address (for example, `johnappleseed`, not `johnappleseed@icloud.com`)
|
||||
- SMTP port = `587` with `STARTTLS`
|
||||
- SMTP login = full iCloud Mail email address (for example, `johnappleseed@icloud.com`, not `johnappleseed`)
|
||||
|
||||
```toml
|
||||
[accounts.icloud]
|
||||
email = "johnappleseed@icloud.com"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.mail.me.com"
|
||||
backend.port = 993
|
||||
backend.login = "johnappleseed"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "*****"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.mail.me.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "johnappleseed@icloud.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "*****"
|
||||
```
|
||||
|
||||
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
|
||||
|
||||
- Save your password in any password manager that can be queried via the CLI:
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show icloud"
|
||||
```
|
||||
|
||||
- Use the global keyring of your system (requires the `keyring` cargo feature):
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "icloud-example"
|
||||
```
|
||||
|
||||
Running `himalaya configure -a icloud` will ask for your IMAP password, just paste the one generated previously.
|
||||
</details>
|
||||
|
||||
## Other interfaces
|
||||
|
||||
- [pimalaya/himalaya-vim](https://github.com/pimalaya/himalaya-vim), a Vim plugin sitting at the top of Himalaya CLI
|
||||
- [dantecatalfamo/himalaya-emacs](https://github.com/dantecatalfamo/himalaya-emacs), an Emacs plugin sitting at the top of Himalaya CLI
|
||||
- [jns/himalaya-raycast](https://www.raycast.com/jns/himalaya), a Raycast extension for Himalaya CLI
|
||||
- [pimalaya/himalaya-repl](https://github.com/pimalaya/himalaya-repl), an experimental Read-Eval-Print-Loop variant of Himalaya CLI
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary>How different is it from aerc, mutt or alpine?</summary>
|
||||
|
||||
Aerc, mutt and alpine can be categorized as Terminal User Interfaces (TUI). When the program is executed, your terminal is locked into an event loop and you interact with your emails using keybinds.
|
||||
|
||||
Himalaya is also a TUI, but more specifically a Command-Line Interface (CLI). There is no event loop: you interact with your emails using shell commands, in a stateless way.
|
||||
|
||||
Additionaly, Himalaya CLI is based on `email-lib`, which is also part of the Pimalaya project. The aim is not just to propose a new terminal interface, but also to expose Rust tools to deal with emails. Anyone who knows Rust language can build his own email interface, without re-inventing the wheel.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to compose a message?</summary>
|
||||
|
||||
An email message is a list of **headers** (`key: val`) followed by a **body**. They form together a template:
|
||||
|
||||
```eml
|
||||
Header: value
|
||||
Header: value
|
||||
Header: value
|
||||
|
||||
Body
|
||||
```
|
||||
|
||||
***Headers and body must be separated by an empty line.***
|
||||
|
||||
### Headers
|
||||
|
||||
Here a non-exhaustive list of valid email message template headers:
|
||||
|
||||
- `Message-ID`: represents the message identifier (you usually do not need to set up it manually)
|
||||
- `In-Reply-To`: represents the identifier of the replied message
|
||||
- `Date`: represents the date of the message
|
||||
- `Subject`: represents the subject of the message
|
||||
- `From`: represents the address of the sender
|
||||
- `To`: represents the addresses of the receivers
|
||||
- `Reply-To`: represents the address the receiver should reply to instead of the `From` header
|
||||
- `Cc`: represents the addresses of the other receivers (carbon copy)
|
||||
- `Bcc`: represents the addresses of the other hidden receivers (blind carbon copy)
|
||||
|
||||
An address can be:
|
||||
|
||||
- a single email address `user@domain`
|
||||
- a named address `Name <user@domain>`
|
||||
- a quoted named address `"Name" <user@domain>`
|
||||
|
||||
Multiple address are separated by a coma `,`: `user@domain, Name <user@domain>, "Name" <user@domain>`.
|
||||
|
||||
### Plain text body
|
||||
|
||||
Email message template body can be written in plain text. The result will be compiled into a single `text/plain` MIME part:
|
||||
|
||||
```eml
|
||||
From: alice@localhost
|
||||
To: Bob <bob@localhost>
|
||||
Subject: Hello from Himalaya
|
||||
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
### MML body
|
||||
|
||||
Email message template body can also be written in MML. The MIME Meta Language was introduced by the Emacs [`mml`](https://www.gnu.org/software/emacs/manual/html_node/emacs-mime/Composing.html) ELisp module. Pimalaya [ported it](https://github.com/pimalaya/core/tree/master/mml) in Rust.
|
||||
|
||||
A raw email message is structured according to the [MIME](https://www.rfc-editor.org/rfc/rfc2045) standard. This standard produces verbose, non-friendly messages. Here comes MML: it simplifies the way email message body are structured. Thanks to its simple XML-based syntax, it allows you to easily add multiple parts, attach a binary file, or attach inline image to your body without dealing with the MIME standard.
|
||||
|
||||
For instance, this MML template:
|
||||
|
||||
```eml
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: MML simple
|
||||
|
||||
<#multipart type=alternative>
|
||||
This is a plain text part.
|
||||
<#part type=text/enriched>
|
||||
<center>This is a centered enriched part</center>
|
||||
<#/multipart>
|
||||
```
|
||||
|
||||
compiles into the following MIME Message:
|
||||
|
||||
```eml
|
||||
Subject: MML simple
|
||||
To: bob@localhost
|
||||
From: alice@localhost
|
||||
MIME-Version: 1.0
|
||||
Date: Tue, 29 Nov 2022 13:07:01 +0000
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe"
|
||||
|
||||
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is a plain text part.
|
||||
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
|
||||
Content-Type: text/enriched
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<center>This is a centered enriched part</center>
|
||||
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe--
|
||||
```
|
||||
|
||||
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to add attachments to a message?</summary>
|
||||
|
||||
*Read first about the FAQ: How to compose a message?*.
|
||||
|
||||
```eml
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: How to attach stuff
|
||||
|
||||
Regular binary attachment:
|
||||
<#part filename=/path/to/file.pdf><#/part>
|
||||
|
||||
Custom file name:
|
||||
<#part filename=/path/to/file.pdf name=custom.pdf><#/part>
|
||||
|
||||
Inline image:
|
||||
<#part disposition=inline filename=/path/to/image.png><#/part>
|
||||
```
|
||||
|
||||
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to debug Himalaya CLI?</summary>
|
||||
|
||||
The simplest way is to use `--debug` and `--trace` arguments.
|
||||
|
||||
The advanced way is based on environment variables:
|
||||
|
||||
- `RUST_LOG=<level>`: determines the log level filter, can be one of `off`, `error`, `warn`, `info`, `debug` and `trace`.
|
||||
- `RUST_SPANTRACE=1`: enables the spantrace (a span represent periods of time in which a program was executing in a particular context).
|
||||
- `RUST_BACKTRACE=1`: enables the error backtrace.
|
||||
- `RUST_BACKTRACE=full`: enables the full error backtrace, which include source lines where the error originated from.
|
||||
|
||||
Logs are written to the `stderr`, which means that you can redirect them easily to a file:
|
||||
|
||||
```
|
||||
RUST_LOG=debug himalaya 2>/tmp/himalaya.log
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How the wizard discovers IMAP/SMTP configs?</summary>
|
||||
|
||||
All the lookup mechanisms use the email address domain as base for the lookup. It is heavily inspired from the Thunderbird [Autoconfiguration](https://udn.realityripple.com/docs/Mozilla/Thunderbird/Autoconfiguration) protocol. For example, for the email address `test@example.com`, the lookup is performed as (in this order):
|
||||
|
||||
1. check for `autoconfig.example.com`
|
||||
2. look up of `example.com` in the ISPDB (the Thunderbird central database)
|
||||
3. look up `MX example.com` in DNS, and for `mx1.mail.hoster.com`, look up `hoster.com` in the ISPDB
|
||||
4. look up `SRV example.com` in DNS
|
||||
5. try to guess (`imap.example.com`, `smtp.example.com`…)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to disable color output?</summary>
|
||||
|
||||
Simply set the environment variable NO_COLOR=1
|
||||
</details>
|
||||
|
||||
## Sponsoring
|
||||
|
||||
[](https://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://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://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();
|
||||
}
|
651
config.sample.toml
Normal file
651
config.sample.toml
Normal file
|
@ -0,0 +1,651 @@
|
|||
################################################################################
|
||||
###[ Global configuration ]#####################################################
|
||||
################################################################################
|
||||
|
||||
# Default display name for all accounts. It is used to build the full
|
||||
# email address of an account: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# Default signature for all accounts. The signature is put at the
|
||||
# bottom of all messages. It can be a path or a string. Supports TOML
|
||||
# multilines.
|
||||
#
|
||||
#signature = "/path/to/signature/file"
|
||||
#signature = """
|
||||
# Thanks you,
|
||||
# Regards
|
||||
#"""
|
||||
signature = "Regards,\n"
|
||||
|
||||
# Default signature delimiter for all accounts. It delimits the end of
|
||||
# the message body from the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# Default downloads directory path for all accounts. It is mostly used
|
||||
# for downloading attachments. Defaults to the system temporary
|
||||
# directory.
|
||||
#
|
||||
downloads-dir = "~/Downloads"
|
||||
|
||||
# Customizes the charset used to build the accounts listing
|
||||
# table. Defaults to markdown table style.
|
||||
#
|
||||
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
|
||||
#
|
||||
account.list.table.preset = "|| |-||| "
|
||||
|
||||
# Customizes the color of the NAME column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.name-color = "green"
|
||||
|
||||
# Customizes the color of the BACKENDS column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.backends-color = "blue"
|
||||
|
||||
# Customizes the color of the DEFAULT column of the account listing
|
||||
# table.
|
||||
#
|
||||
account.list.table.default-color = "black"
|
||||
|
||||
################################################################################
|
||||
###[ Account configuration ]####################################################
|
||||
################################################################################
|
||||
|
||||
# The account name should be unique.
|
||||
#
|
||||
[accounts.example]
|
||||
|
||||
# Defaultness of the account. The current account will be used by
|
||||
# default in all commands.
|
||||
#
|
||||
default = true
|
||||
|
||||
# The email address associated to the current account.
|
||||
#
|
||||
email = "example@localhost"
|
||||
|
||||
# The display name of the account. This and the email are used to
|
||||
# build the full email address: "Example" <example@localhost>
|
||||
#
|
||||
display-name = "Example"
|
||||
|
||||
# The signature put at the bottom of composed messages. It can be a
|
||||
# path or a string. Supports TOML multilines.
|
||||
#
|
||||
#signature = "/path/to/signature/file"
|
||||
#signature = """
|
||||
# Thanks you,
|
||||
# Regards
|
||||
#"""
|
||||
signature = "Regards,\n"
|
||||
|
||||
# Signature delimiter. It delimits the end of the message body from
|
||||
# the signature.
|
||||
#
|
||||
signature-delim = "-- \n"
|
||||
|
||||
# Downloads directory path. It is mostly used for downloading
|
||||
# attachments. Defaults to the system temporary directory.
|
||||
#
|
||||
downloads-dir = "~/downloads"
|
||||
|
||||
|
||||
|
||||
# Defines aliases for your mailboxes. There are 4 special aliases used
|
||||
# by the tool: inbox, sent, drafts and trash. Other aliases can be
|
||||
# defined as well.
|
||||
#
|
||||
folder.aliases.inbox = "INBOX"
|
||||
folder.aliases.sent = "Sent"
|
||||
folder.aliases.drafts = "Drafts"
|
||||
folder.aliases.trash = "Trash"
|
||||
folder.aliases.a23 = "Archives/2023"
|
||||
|
||||
# Customizes the number of folders to show by page.
|
||||
#
|
||||
folder.list.page-size = 10
|
||||
|
||||
# Customizes the charset used to build the table. Defaults to markdown
|
||||
# table style.
|
||||
#
|
||||
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
|
||||
#
|
||||
folder.list.table.preset = "|| |-||| "
|
||||
|
||||
# Customizes the color of the NAME column of the folder listing table.
|
||||
#
|
||||
folder.list.table.name-color = "blue"
|
||||
|
||||
# Customizes the color of the DESC column of the folder listing table.
|
||||
#
|
||||
folder.list.table.desc-color = "green"
|
||||
|
||||
|
||||
|
||||
# Customizes the number of envelopes to show by page.
|
||||
#
|
||||
envelope.list.page-size = 10
|
||||
|
||||
# Customizes the format of the envelope date.
|
||||
#
|
||||
# See supported formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
|
||||
#
|
||||
envelope.list.datetime-fmt = "%F %R%:z"
|
||||
|
||||
# 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" ]
|
||||
)
|
||||
|
|
143
flake.lock
generated
143
flake.lock
generated
|
@ -1,138 +1,79 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1650374568,
|
||||
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1656928814,
|
||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1662220400,
|
||||
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3",
|
||||
"lastModified": 1732405626,
|
||||
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"owner": "soywod",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1664356419,
|
||||
"narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=",
|
||||
"owner": "NixOS",
|
||||
"lastModified": 1736437047,
|
||||
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1",
|
||||
"rev": "f17b95775191ea44bc426831235d87affb10faba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
"owner": "nixos",
|
||||
"ref": "staging-next",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"pimalaya": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1664356419,
|
||||
"narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1",
|
||||
"lastModified": 1737984647,
|
||||
"narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=",
|
||||
"owner": "pimalaya",
|
||||
"repo": "nix",
|
||||
"rev": "712a481632f4929d24a34cb5762e0ffdc901bd99",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1659102345,
|
||||
"narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"owner": "pimalaya",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay",
|
||||
"utils": "utils"
|
||||
"fenix": "fenix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pimalaya": "pimalaya"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1664334084,
|
||||
"narHash": "sha256-cqP0TzDs3GDRprS6IgVQcWjQ0ynmjQFjYWvp+LE/s6I=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "70eab96a255ae9b4b82b38ea5ac5c8e5b57e0abd",
|
||||
"lastModified": 1732050317,
|
||||
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
|
75
flake.nix
75
flake.nix
|
@ -1,65 +1,26 @@
|
|||
{
|
||||
description = "Command-line interface for email management.";
|
||||
description = "CLI to manage emails";
|
||||
|
||||
inputs = {
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
# FIXME: when #358989 lands on nixos-unstable
|
||||
# https://nixpk.gs/pr-tracker.html?pr=358989
|
||||
nixpkgs.url = "github:nixos/nixpkgs/staging-next";
|
||||
fenix = {
|
||||
# TODO: https://github.com/nix-community/fenix/pull/145
|
||||
# url = "github:nix-community/fenix";
|
||||
url = "github:soywod/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
pimalaya = {
|
||||
url = "github:pimalaya/nix";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }:
|
||||
utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
name = "himalaya";
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
in
|
||||
rec {
|
||||
# nix build
|
||||
defaultPackage = packages.${name};
|
||||
packages = {
|
||||
${name} = naersk.lib.${system}.buildPackage {
|
||||
pname = name;
|
||||
root = ./.;
|
||||
nativeBuildInputs = with pkgs; [ openssl.dev pkg-config ];
|
||||
overrideMain = _: {
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/applications/
|
||||
cp assets/himalaya.desktop $out/share/applications/
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# nix run
|
||||
defaultApp = apps.${name};
|
||||
apps.${name} = utils.lib.mkApp {
|
||||
inherit name;
|
||||
drv = packages.${name};
|
||||
};
|
||||
|
||||
# nix develop
|
||||
devShell = pkgs.mkShell {
|
||||
inputsFrom = builtins.attrValues self.packages.${system};
|
||||
nativeBuildInputs = with pkgs; [
|
||||
# Nix LSP + formatter
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
|
||||
# Rust env
|
||||
(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
|
||||
cargo-watch
|
||||
rust-analyzer
|
||||
|
||||
# Notmuch
|
||||
notmuch
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
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,2 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "1.82.0"
|
||||
profile = "default"
|
||||
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;
|
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::{self, App, Arg, ArgMatches, Shell, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
type OptionShell<'a> = Option<&'a str>;
|
||||
|
||||
/// Completion commands.
|
||||
pub enum Command<'a> {
|
||||
/// Generate completion script for the given shell slice.
|
||||
Generate(OptionShell<'a>),
|
||||
}
|
||||
|
||||
/// Completion command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
info!("entering completion command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("completion") {
|
||||
info!("completion command matched");
|
||||
let shell = m.value_of("shell");
|
||||
debug!("shell: {:?}", shell);
|
||||
return Ok(Some(Command::Generate(shell)));
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Completion subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("completion")
|
||||
.aliases(&["completions", "compl", "compe", "comp"])
|
||||
.about("Generates the completion script for the given shell")
|
||||
.args(&[Arg::with_name("shell")
|
||||
.possible_values(&Shell::variants()[..])
|
||||
.required(true)])]
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
//! Module related to completion handling.
|
||||
//!
|
||||
//! This module gathers all completion commands.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{App, Shell};
|
||||
use log::{debug, info};
|
||||
use std::{io, str::FromStr};
|
||||
|
||||
/// Generates completion script from the given [`clap::App`] for the given shell slice.
|
||||
pub fn generate<'a>(mut app: App<'a, 'a>, shell: Option<&'a str>) -> Result<()> {
|
||||
info!("entering generate completion handler");
|
||||
|
||||
let shell = Shell::from_str(shell.unwrap_or_default())
|
||||
.map_err(|err| anyhow!(err))
|
||||
.context("cannot parse shell")?;
|
||||
debug!("shell: {}", shell);
|
||||
|
||||
app.gen_completions_to("himalaya", shell, &mut io::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,20 +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<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_CONFIG)
|
||||
.long("config")
|
||||
.short("c")
|
||||
.help("Forces a specific config file path")
|
||||
.value_name("PATH")
|
||||
}
|
||||
|
||||
/// Represents the config file path argument parser.
|
||||
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> {
|
||||
matches.value_of(ARG_CONFIG)
|
||||
}
|
|
@ -1,572 +0,0 @@
|
|||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
|
||||
use log::{debug, trace};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use toml;
|
||||
|
||||
use crate::{account::DeserializedAccountConfig, config::prelude::*};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[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_reading_headers: Option<Vec<String>>,
|
||||
#[serde(default, with = "email_text_plain_format")]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_reading_decrypt_cmd: Option<String>,
|
||||
pub email_writing_encrypt_cmd: Option<String>,
|
||||
#[serde(default, with = "email_hooks")]
|
||||
pub email_hooks: Option<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> {
|
||||
trace!(">> parse config from path");
|
||||
debug!("path: {:?}", path);
|
||||
|
||||
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
let config: Self = toml::from_str(&content).context("cannot parse config file")?;
|
||||
|
||||
if config.accounts.is_empty() {
|
||||
return Err(anyhow!("config file must contain at least one account"));
|
||||
}
|
||||
|
||||
trace!("config: {:?}", config);
|
||||
trace!("<< parse config from path");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from XDG_CONFIG_HOME
|
||||
/// environment variable.
|
||||
fn path_from_xdg() -> Result<PathBuf> {
|
||||
let path = env::var("XDG_CONFIG_HOME").context("cannot read env var XDG_CONFIG_HOME")?;
|
||||
let path = PathBuf::from(path).join("himalaya").join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from HOME environment
|
||||
/// variable.
|
||||
fn path_from_xdg_alt() -> Result<PathBuf> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
|
||||
let path = PathBuf::from(path)
|
||||
.join(".config")
|
||||
.join("himalaya")
|
||||
.join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the .himalayarc config file path from HOME
|
||||
/// environment variable.
|
||||
fn path_from_home() -> Result<PathBuf> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
|
||||
let path = PathBuf::from(path).join(".himalayarc");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the config file path.
|
||||
pub fn path() -> Result<PathBuf> {
|
||||
Self::path_from_xdg()
|
||||
.or_else(|_| Self::path_from_xdg_alt())
|
||||
.or_else(|_| Self::path_from_home())
|
||||
}
|
||||
|
||||
pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> {
|
||||
let (account_config, backend_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(_, account)| {
|
||||
if account.is_default() {
|
||||
Some(account)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
|
||||
}?
|
||||
.to_configs(self);
|
||||
|
||||
Ok((account_config, backend_config))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{EmailSender, SendmailConfig, SmtpConfig};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya_lib::ImapConfig;
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use himalaya_lib::MaildirConfig;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::NotmuchConfig;
|
||||
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::account::DeserializedBaseAccountConfig;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use crate::account::DeserializedImapAccountConfig;
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use crate::account::DeserializedMaildirAccountConfig;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use crate::account::DeserializedNotmuchAccountConfig;
|
||||
|
||||
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_backend_field() {
|
||||
let config = make_config("[account]");
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `backend` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_invalid_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
backend = \"bad\"",
|
||||
);
|
||||
|
||||
assert!(config
|
||||
.unwrap_err()
|
||||
.root_cause()
|
||||
.to_string()
|
||||
.starts_with("unknown variant `bad`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_email_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
backend = \"none\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `email` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imap_account_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-host` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-port` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[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_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-login` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[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_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-passwd-cmd` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_maildir_missing_root_dir_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `maildir-root-dir` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[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_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `notmuch-db-path` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `sender` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_invalid_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"bad\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"unknown variant `bad`, expected one of `none`, `smtp`, `sendmail` at line 1 column 1",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-host` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-port` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[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_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-login` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_missing_passwd_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-passwd-cmd` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_sendmail_sender_missing_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"sendmail\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `sendmail-cmd` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_smtp_sender_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"smtp\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"
|
||||
smtp-passwd-cmd = \"echo password\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::None(DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
email_sender: EmailSender::Smtp(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 25,
|
||||
login: "login".into(),
|
||||
passwd_cmd: "echo password".into(),
|
||||
..SmtpConfig::default()
|
||||
}),
|
||||
..DeserializedBaseAccountConfig::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::None(DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
email_sender: EmailSender::Sendmail(SendmailConfig {
|
||||
cmd: "echo send".into(),
|
||||
}),
|
||||
..DeserializedBaseAccountConfig::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-passwd-cmd = \"echo password\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 993,
|
||||
login: "login".into(),
|
||||
passwd_cmd: "echo password".into(),
|
||||
..ImapConfig::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::Maildir(DeserializedMaildirAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: MaildirConfig {
|
||||
root_dir: "/tmp/maildir".into(),
|
||||
}
|
||||
})
|
||||
)]),
|
||||
..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::Notmuch(DeserializedNotmuchAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: NotmuchConfig {
|
||||
db_path: "/tmp/notmuch.db".into(),
|
||||
}
|
||||
})
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod prelude;
|
||||
|
||||
pub use config::*;
|
|
@ -1,139 +0,0 @@
|
|||
use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig};
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya_lib::ImapConfig;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use himalaya_lib::MaildirConfig;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::NotmuchConfig;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "SmtpConfig")]
|
||||
struct SmtpConfigDef {
|
||||
#[serde(rename = "smtp-host")]
|
||||
pub host: String,
|
||||
#[serde(rename = "smtp-port")]
|
||||
pub port: u16,
|
||||
#[serde(rename = "smtp-starttls")]
|
||||
pub starttls: Option<bool>,
|
||||
#[serde(rename = "smtp-insecure")]
|
||||
pub insecure: Option<bool>,
|
||||
#[serde(rename = "smtp-login")]
|
||||
pub login: String,
|
||||
#[serde(rename = "smtp-passwd-cmd")]
|
||||
pub passwd_cmd: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "ImapConfig")]
|
||||
pub struct ImapConfigDef {
|
||||
#[serde(rename = "imap-host")]
|
||||
pub host: String,
|
||||
#[serde(rename = "imap-port")]
|
||||
pub port: u16,
|
||||
#[serde(rename = "imap-starttls")]
|
||||
pub starttls: Option<bool>,
|
||||
#[serde(rename = "imap-insecure")]
|
||||
pub insecure: Option<bool>,
|
||||
#[serde(rename = "imap-login")]
|
||||
pub login: String,
|
||||
#[serde(rename = "imap-passwd-cmd")]
|
||||
pub passwd_cmd: String,
|
||||
#[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 = "maildir-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "MaildirConfig")]
|
||||
pub struct MaildirConfigDef {
|
||||
#[serde(rename = "maildir-root-dir")]
|
||||
pub root_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "NotmuchConfig")]
|
||||
pub struct NotmuchConfigDef {
|
||||
#[serde(rename = "notmuch-db-path")]
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
|
||||
enum EmailTextPlainFormatDef {
|
||||
Auto,
|
||||
Flowed,
|
||||
Fixed(usize),
|
||||
}
|
||||
|
||||
pub mod email_text_plain_format {
|
||||
use himalaya_lib::EmailTextPlainFormat;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::EmailTextPlainFormatDef;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailTextPlainFormat>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper(#[serde(with = "EmailTextPlainFormatDef")] EmailTextPlainFormat);
|
||||
|
||||
let helper = Option::deserialize(deserializer)?;
|
||||
Ok(helper.map(|Helper(external)| external))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")]
|
||||
pub enum EmailSenderDef {
|
||||
None,
|
||||
#[serde(with = "SmtpConfigDef")]
|
||||
Smtp(SmtpConfig),
|
||||
#[serde(with = "SendmailConfigDef")]
|
||||
Sendmail(SendmailConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "SendmailConfig")]
|
||||
pub struct SendmailConfigDef {
|
||||
#[serde(rename = "sendmail-cmd")]
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
/// Represents the email hooks. Useful for doing extra email
|
||||
/// processing before or after sending it.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailHooks")]
|
||||
struct EmailHooksDef {
|
||||
/// Represents the hook called just before sending an email.
|
||||
pub pre_send: Option<String>,
|
||||
}
|
||||
|
||||
pub mod email_hooks {
|
||||
use himalaya_lib::EmailHooks;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::EmailHooksDef;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailHooks>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper(#[serde(with = "EmailHooksDef")] EmailHooks);
|
||||
|
||||
let helper = Option::deserialize(deserializer)?;
|
||||
Ok(helper.map(|Helper(external)| external))
|
||||
}
|
||||
}
|
|
@ -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 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 {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => {
|
||||
Account::new(name, "imap", config.base.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => {
|
||||
Account::new(name, "maildir", config.base.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => {
|
||||
Account::new(name, "notmuch", config.base.default.unwrap_or_default())
|
||||
}
|
||||
DeserializedAccountConfig::None(..) => Account::new(name, "none", false),
|
||||
})
|
||||
.collect();
|
||||
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
Self(accounts)
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
//! This module provides arguments related to the user account config.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use log::info;
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_ACCOUNT: &str = "account";
|
||||
const CMD_ACCOUNTS: &str = "accounts";
|
||||
|
||||
/// Represents the account commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
/// Represents the list accounts command.
|
||||
List(table::args::MaxTableWidth),
|
||||
}
|
||||
|
||||
/// Represents the account command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
|
||||
info!("accounts command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the account subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_ACCOUNTS)
|
||||
.aliases(&["account", "acc", "a"])
|
||||
.about("Lists accounts")
|
||||
.arg(table::args::max_width())]
|
||||
}
|
||||
|
||||
/// Represents the user account name argument. This argument allows
|
||||
/// the user to select a different account than the default one.
|
||||
pub fn arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ACCOUNT)
|
||||
.long("account")
|
||||
.short("a")
|
||||
.help("Selects a specific account")
|
||||
.value_name("STRING")
|
||||
}
|
||||
|
||||
/// Represents the user account name argument parser.
|
||||
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> {
|
||||
matches.value_of(ARG_ACCOUNT)
|
||||
}
|
|
@ -1,215 +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.
|
||||
|
||||
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailSender, EmailTextPlainFormat};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya_lib::ImapConfig;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use himalaya_lib::MaildirConfig;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::NotmuchConfig;
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::{prelude::*, DeserializedConfig};
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(tag = "backend", rename_all = "snake_case")]
|
||||
pub enum DeserializedAccountConfig {
|
||||
None(DeserializedBaseAccountConfig),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Imap(DeserializedImapAccountConfig),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Maildir(DeserializedMaildirAccountConfig),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Notmuch(DeserializedNotmuchAccountConfig),
|
||||
}
|
||||
|
||||
impl DeserializedAccountConfig {
|
||||
pub fn to_configs(&self, global_config: &DeserializedConfig) -> (AccountConfig, BackendConfig) {
|
||||
match self {
|
||||
DeserializedAccountConfig::None(config) => {
|
||||
(config.to_account_config(global_config), BackendConfig::None)
|
||||
}
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Imap(&config.backend),
|
||||
),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Maildir(&config.backend),
|
||||
),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Notmuch(&config.backend),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
match self {
|
||||
DeserializedAccountConfig::None(config) => config.default.unwrap_or_default(),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => config.base.default.unwrap_or_default(),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => config.base.default.unwrap_or_default(),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => config.base.default.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedBaseAccountConfig {
|
||||
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_reading_headers: Option<Vec<String>>,
|
||||
#[serde(default, with = "email_text_plain_format")]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_reading_decrypt_cmd: Option<String>,
|
||||
pub email_writing_encrypt_cmd: Option<String>,
|
||||
#[serde(flatten, with = "EmailSenderDef")]
|
||||
pub email_sender: EmailSender,
|
||||
#[serde(default, with = "email_hooks")]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
}
|
||||
|
||||
impl DeserializedBaseAccountConfig {
|
||||
pub fn to_account_config(&self, 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 {
|
||||
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_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
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
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_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_sender: self.email_sender.to_owned(),
|
||||
email_hooks: EmailHooks {
|
||||
pre_send: self
|
||||
.email_hooks
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.map(|hook| hook.pre_send)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_hooks
|
||||
.as_ref()
|
||||
.map(|hook| hook.pre_send.as_ref().map(ToOwned::to_owned))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub struct DeserializedImapAccountConfig {
|
||||
#[serde(flatten)]
|
||||
pub base: DeserializedBaseAccountConfig,
|
||||
#[serde(flatten, with = "ImapConfigDef")]
|
||||
pub backend: ImapConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub struct DeserializedMaildirAccountConfig {
|
||||
#[serde(flatten)]
|
||||
pub base: DeserializedBaseAccountConfig,
|
||||
#[serde(flatten, with = "MaildirConfigDef")]
|
||||
pub backend: MaildirConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub struct DeserializedNotmuchAccountConfig {
|
||||
#[serde(flatten)]
|
||||
pub base: DeserializedBaseAccountConfig,
|
||||
#[serde(flatten, with = "NotmuchConfigDef")]
|
||||
pub backend: NotmuchConfig,
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
//! Account handlers module.
|
||||
//!
|
||||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::AccountConfig;
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
config::DeserializedConfig,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
Accounts,
|
||||
};
|
||||
|
||||
/// Lists all accounts.
|
||||
pub fn list<'a, P: Printer>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
deserialized_config: &DeserializedConfig,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!(">> account list 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(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{AccountConfig, ImapConfig};
|
||||
use std::{collections::HashMap, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
account::{
|
||||
DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedImapAccountConfig,
|
||||
},
|
||||
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_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<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::Imap(DeserializedImapAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
default: Some(true),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: ImapConfig::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,9 +0,0 @@
|
|||
pub mod account;
|
||||
pub mod accounts;
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
|
||||
pub use account::*;
|
||||
pub use accounts::*;
|
||||
pub use config::*;
|
|
@ -1,489 +0,0 @@
|
|||
//! Module related to email CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to email.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::email::TplOverride;
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::{email, flag, folder, tpl, ui::table};
|
||||
|
||||
const ARG_ATTACHMENTS: &str = "attachment";
|
||||
const ARG_CRITERIA: &str = "criterion";
|
||||
const ARG_ENCRYPT: &str = "encrypt";
|
||||
const ARG_HEADERS: &str = "header";
|
||||
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_DEL: &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";
|
||||
|
||||
type Criteria = String;
|
||||
type Encrypt = bool;
|
||||
type Folder<'a> = &'a str;
|
||||
type Page = usize;
|
||||
type PageSize = usize;
|
||||
type Query = String;
|
||||
type Raw = bool;
|
||||
type RawEmail<'a> = &'a str;
|
||||
type TextMime<'a> = &'a str;
|
||||
|
||||
pub(crate) type All = bool;
|
||||
pub(crate) type Attachments<'a> = Vec<&'a str>;
|
||||
pub(crate) type Headers<'a> = Vec<&'a str>;
|
||||
pub(crate) type Id<'a> = &'a str;
|
||||
pub(crate) type Ids<'a> = &'a str;
|
||||
|
||||
/// Represents the email commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Attachments(Id<'a>),
|
||||
Copy(Id<'a>, Folder<'a>),
|
||||
Delete(Ids<'a>),
|
||||
Forward(Id<'a>, Attachments<'a>, Encrypt),
|
||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Id<'a>, Folder<'a>),
|
||||
Read(Id<'a>, TextMime<'a>, Raw, Headers<'a>),
|
||||
Reply(Id<'a>, All, Attachments<'a>, Encrypt),
|
||||
Save(RawEmail<'a>),
|
||||
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawEmail<'a>),
|
||||
Sort(
|
||||
Criteria,
|
||||
Query,
|
||||
table::args::MaxTableWidth,
|
||||
Option<PageSize>,
|
||||
Page,
|
||||
),
|
||||
Write(TplOverride<'a>, Attachments<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag::args::Cmd<'a>>),
|
||||
Tpl(Option<tpl::args::Cmd<'a>>),
|
||||
}
|
||||
|
||||
/// Email command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
trace!("matches: {:?}", m);
|
||||
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
|
||||
debug!("attachments command matched");
|
||||
let id = parse_id_arg(m);
|
||||
Cmd::Attachments(id)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
|
||||
debug!("copy command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Copy(id, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DEL) {
|
||||
debug!("delete command matched");
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Delete(ids)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
debug!("forward command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let attachments = parse_attachments_arg(m);
|
||||
let encrypt = parse_encrypt_flag(m);
|
||||
Cmd::Forward(id, attachments, encrypt)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
debug!("list command matched");
|
||||
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) {
|
||||
debug!("move command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Move(id, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
|
||||
debug!("read command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let mime = parse_mime_type_arg(m);
|
||||
let raw = parse_raw_flag(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
Cmd::Read(id, mime, raw, headers)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
debug!("reply command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let all = parse_reply_all_flag(m);
|
||||
let attachments = parse_attachments_arg(m);
|
||||
let encrypt = parse_encrypt_flag(m);
|
||||
Cmd::Reply(id, all, attachments, encrypt)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
debug!("save command matched");
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Save(email)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
|
||||
debug!("search command matched");
|
||||
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) {
|
||||
debug!("sort command matched");
|
||||
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) {
|
||||
debug!("send command matched");
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Send(email)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
debug!("write command matched");
|
||||
let attachments = parse_attachments_arg(m);
|
||||
let encrypt = parse_encrypt_flag(m);
|
||||
let tpl = tpl::args::parse_override_arg(m);
|
||||
Cmd::Write(tpl, attachments, encrypt)
|
||||
} 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(flag::args::CMD_FLAG) {
|
||||
Cmd::Flag(flag::args::matches(m)?)
|
||||
} else {
|
||||
debug!("default list command matched");
|
||||
Cmd::List(None, None, 0)
|
||||
};
|
||||
|
||||
Ok(Some(cmd))
|
||||
}
|
||||
|
||||
/// Represents the email subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![
|
||||
flag::args::subcmds(),
|
||||
tpl::args::subcmds(),
|
||||
vec![
|
||||
SubCommand::with_name(CMD_ATTACHMENTS)
|
||||
.aliases(&["attachment", "attach", "att", "at", "a"])
|
||||
.about("Downloads all attachments of the targeted email")
|
||||
.arg(email::args::id_arg()),
|
||||
SubCommand::with_name(CMD_LIST)
|
||||
.aliases(&["lst", "l"])
|
||||
.about("Lists all emails")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width()),
|
||||
SubCommand::with_name(CMD_SEARCH)
|
||||
.aliases(&["s", "query", "q"])
|
||||
.about("Lists emails matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(query_arg()),
|
||||
SubCommand::with_name(CMD_SORT)
|
||||
.about("Sorts emails 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()),
|
||||
SubCommand::with_name(CMD_WRITE)
|
||||
.about("Writes a new email")
|
||||
.aliases(&["w", "new", "n"])
|
||||
.args(&tpl::args::args())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_flag()),
|
||||
SubCommand::with_name(CMD_SEND)
|
||||
.about("Sends a raw email")
|
||||
.arg(raw_arg()),
|
||||
SubCommand::with_name(CMD_SAVE)
|
||||
.about("Saves a raw email")
|
||||
.arg(raw_arg()),
|
||||
SubCommand::with_name(CMD_READ)
|
||||
.about("Reads text bodies of a email")
|
||||
.arg(id_arg())
|
||||
.arg(mime_type_arg())
|
||||
.arg(raw_flag())
|
||||
.arg(headers_arg()),
|
||||
SubCommand::with_name(CMD_REPLY)
|
||||
.aliases(&["rep", "r"])
|
||||
.about("Answers to an email")
|
||||
.arg(id_arg())
|
||||
.arg(reply_all_flag())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_flag()),
|
||||
SubCommand::with_name(CMD_FORWARD)
|
||||
.aliases(&["fwd", "f"])
|
||||
.about("Forwards an email")
|
||||
.arg(id_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_flag()),
|
||||
SubCommand::with_name(CMD_COPY)
|
||||
.aliases(&["cp", "c"])
|
||||
.about("Copies an email to the targeted folder")
|
||||
.arg(id_arg())
|
||||
.arg(folder::args::target_arg()),
|
||||
SubCommand::with_name(CMD_MOVE)
|
||||
.aliases(&["mv"])
|
||||
.about("Moves an email to the targeted folder")
|
||||
.arg(id_arg())
|
||||
.arg(folder::args::target_arg()),
|
||||
SubCommand::with_name(CMD_DEL)
|
||||
.aliases(&["del", "d", "remove", "rm"])
|
||||
.about("Deletes an email")
|
||||
.arg(ids_arg()),
|
||||
],
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
/// Represents the email id argument.
|
||||
pub fn id_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ID)
|
||||
.help("Specifies the target email")
|
||||
.value_name("ID")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email id argument parser.
|
||||
pub fn parse_id_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_ID).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email sort criteria argument.
|
||||
pub fn criteria_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_CRITERIA)
|
||||
.long("criterion")
|
||||
.short("c")
|
||||
.help("Email sorting preferences")
|
||||
.value_name("CRITERION:ORDER")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
.possible_values(&[
|
||||
"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<'a>(matches: &'a ArgMatches<'a>) -> String {
|
||||
matches
|
||||
.values_of(ARG_CRITERIA)
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// Represents the email ids argument.
|
||||
pub fn ids_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_IDS)
|
||||
.help("Specifies the target email(s)")
|
||||
.long_help("Specifies a range of emails. The range follows the RFC3501 format.")
|
||||
.value_name("IDS")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email ids argument parser.
|
||||
pub fn parse_ids_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(email::args::ARG_IDS).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email reply all argument.
|
||||
pub fn reply_all_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_REPLY_ALL)
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
}
|
||||
|
||||
/// Represents the email reply all argument parser.
|
||||
pub fn parse_reply_all_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_REPLY_ALL)
|
||||
}
|
||||
|
||||
/// Represents the page size argument.
|
||||
fn page_size_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_PAGE_SIZE)
|
||||
.help("Page size")
|
||||
.short("s")
|
||||
.long("size")
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the page size argument parser.
|
||||
fn parse_page_size_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<usize> {
|
||||
matches.value_of(ARG_PAGE_SIZE).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Represents the page argument.
|
||||
fn page_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_PAGE)
|
||||
.help("Page number")
|
||||
.short("p")
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
/// Represents the page argument parser.
|
||||
fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize {
|
||||
matches
|
||||
.value_of(ARG_PAGE)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email attachments argument.
|
||||
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ATTACHMENTS)
|
||||
.help("Adds attachment to the email")
|
||||
.short("a")
|
||||
.long("attachment")
|
||||
.value_name("PATH")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Represents the email attachments argument parser.
|
||||
pub fn parse_attachments_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> {
|
||||
matches
|
||||
.values_of(ARG_ATTACHMENTS)
|
||||
.unwrap_or_default()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the email headers argument.
|
||||
pub fn headers_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_HEADERS)
|
||||
.help("Shows additional headers with the email")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("STRING")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Represents the email headers argument parser.
|
||||
pub fn parse_headers_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> {
|
||||
matches.values_of(ARG_HEADERS).unwrap_or_default().collect()
|
||||
}
|
||||
|
||||
/// Represents the raw flag.
|
||||
pub fn raw_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_RAW)
|
||||
.help("Reads a raw email")
|
||||
.long("raw")
|
||||
.short("r")
|
||||
}
|
||||
|
||||
/// Represents the raw flag parser.
|
||||
pub fn parse_raw_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_RAW)
|
||||
}
|
||||
|
||||
/// Represents the email raw argument.
|
||||
pub fn raw_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_RAW).raw(true)
|
||||
}
|
||||
|
||||
/// Represents the email raw argument parser.
|
||||
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_RAW).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email encrypt flag.
|
||||
pub fn encrypt_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ENCRYPT)
|
||||
.help("Encrypts the email")
|
||||
.short("e")
|
||||
.long("encrypt")
|
||||
}
|
||||
|
||||
/// Represents the email encrypt flag parser.
|
||||
pub fn parse_encrypt_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_ENCRYPT)
|
||||
}
|
||||
|
||||
/// Represents the email MIME type argument.
|
||||
pub fn mime_type_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_MIME_TYPE)
|
||||
.help("MIME type to use")
|
||||
.short("t")
|
||||
.long("mime-type")
|
||||
.value_name("MIME")
|
||||
.possible_values(&["plain", "html"])
|
||||
.default_value("plain")
|
||||
}
|
||||
|
||||
/// Represents the email MIME type argument parser.
|
||||
pub fn parse_mime_type_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_MIME_TYPE).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email query argument.
|
||||
pub fn query_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_QUERY)
|
||||
.long_help("The query system depends on the backend, see the wiki for more details")
|
||||
.value_name("QUERY")
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email query argument parser.
|
||||
pub fn parse_query_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
|
||||
matches
|
||||
.values_of(ARG_QUERY)
|
||||
.unwrap_or_default()
|
||||
.fold((false, vec![]), |(escape, mut cmds), cmd| {
|
||||
match (cmd, 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,392 +0,0 @@
|
|||
//! Module related to message handling.
|
||||
//!
|
||||
//! This module gathers all message commands.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{
|
||||
AccountConfig, Backend, Email, Part, Parts, Sender, TextPlainPart, TplOverride,
|
||||
};
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::addrparse;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Downloads all message attachments to the user account downloads directory.
|
||||
pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let attachments = backend.email_get(mbox, seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
|
||||
if attachments_len == 0 {
|
||||
return printer.print_struct(format!("No attachment found for message {}", seq));
|
||||
}
|
||||
|
||||
printer.print_str(format!(
|
||||
"{} attachment(s) found for message {}",
|
||||
attachments_len, seq
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let file_path = config.get_download_file_path(&attachment.filename)?;
|
||||
printer.print_str(format!("Downloading {:?}…", file_path))?;
|
||||
fs::write(&file_path, &attachment.content)
|
||||
.context(format!("cannot download attachment {:?}", file_path))?;
|
||||
}
|
||||
|
||||
printer.print_struct("Done!")
|
||||
}
|
||||
|
||||
/// Copy a message from a folder to another.
|
||||
pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.email_copy(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
"Message {} successfully copied to folder {}",
|
||||
seq, mbox_dst
|
||||
))
|
||||
}
|
||||
|
||||
/// Delete messages matching the given sequence range.
|
||||
pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.email_delete(mbox, seq)?;
|
||||
printer.print_struct(format!("Message(s) {} successfully deleted", seq))
|
||||
}
|
||||
|
||||
/// Forward the given message UID from the selected folder.
|
||||
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
seq: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let msg = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List paginated messages from the selected folder.
|
||||
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.envelope_list(mbox, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
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 fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
url: &Url,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering mailto command handler");
|
||||
|
||||
let to = addrparse(url.path())?;
|
||||
let mut cc = Vec::new();
|
||||
let mut bcc = Vec::new();
|
||||
let mut subject = Cow::default();
|
||||
let mut body = Cow::default();
|
||||
|
||||
for (key, val) in url.query_pairs() {
|
||||
match key.as_bytes() {
|
||||
b"cc" => {
|
||||
cc.push(val.to_string());
|
||||
}
|
||||
b"bcc" => {
|
||||
bcc.push(val.to_string());
|
||||
}
|
||||
b"subject" => {
|
||||
subject = val;
|
||||
}
|
||||
b"body" => {
|
||||
body = val;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let msg = Email {
|
||||
from: Some(vec![config.address()?].into()),
|
||||
to: if to.is_empty() { None } else { Some(to) },
|
||||
cc: if cc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addrparse(&cc.join(","))?)
|
||||
},
|
||||
bcc: if bcc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addrparse(&bcc.join(","))?)
|
||||
},
|
||||
subject: subject.into(),
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: body.into(),
|
||||
})]),
|
||||
..Email::default()
|
||||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
editor::edit_msg_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a message from a folder to another.
|
||||
pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.email_move(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully moved to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
}
|
||||
|
||||
/// Read a message by its sequence number.
|
||||
pub fn read<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
text_mime: &str,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let msg = backend.email_get(mbox, seq)?;
|
||||
|
||||
printer.print_struct(if raw {
|
||||
// Emails don't always have valid utf8. Using "lossy" to display what we can.
|
||||
String::from_utf8_lossy(&msg.raw).into_owned()
|
||||
} else {
|
||||
msg.to_readable_string(text_mime, headers, config)?
|
||||
})
|
||||
}
|
||||
|
||||
/// Reply to the given message UID.
|
||||
pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let msg = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
backend.flags_add(mbox, seq, "replied")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves a raw message to the targetted folder.
|
||||
pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
raw_msg: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
debug!("folder: {}", mbox);
|
||||
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
debug!("is tty: {}", is_tty);
|
||||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
backend.email_add(mbox, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paginate messages from the selected folder matching the specified
|
||||
/// query.
|
||||
pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.envelope_search(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Paginates messages from the selected folder matching the specified
|
||||
/// query, sorted by the given criteria.
|
||||
pub fn sort<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
sort: String,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.envelope_search(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a raw message.
|
||||
pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
raw_msg: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering send message handler");
|
||||
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
debug!("is tty: {}", is_tty);
|
||||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let sent_folder = config.folder_alias("sent")?;
|
||||
debug!("sent folder: {:?}", sent_folder);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
trace!("raw message: {:?}", raw_msg);
|
||||
let msg = Email::from_tpl(&raw_msg)?;
|
||||
sender.send(&config, &msg)?;
|
||||
backend.email_add(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose a new message.
|
||||
pub fn write<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
tpl: TplOverride,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let msg = Email::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, tpl, config, printer, backend, sender)?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -1,30 +0,0 @@
|
|||
use himalaya_lib::{Envelope, Flag};
|
||||
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
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("SENDER").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let flags = self.flags.to_symbols_string();
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = self.date.as_deref().unwrap_or_default();
|
||||
|
||||
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,16 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::Envelopes;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
impl PrintTable for Envelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod envelope;
|
||||
pub mod envelopes;
|
||||
|
||||
pub use envelope::*;
|
||||
pub use envelopes::*;
|
|
@ -1,99 +0,0 @@
|
|||
//! Email flag CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher
|
||||
//! related to the email flag domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::email;
|
||||
|
||||
const ARG_FLAGS: &str = "flag";
|
||||
|
||||
const CMD_ADD: &str = "add";
|
||||
const CMD_DEL: &str = "remove";
|
||||
const CMD_SET: &str = "set";
|
||||
|
||||
pub(crate) const CMD_FLAG: &str = "flag";
|
||||
|
||||
type Flags = String;
|
||||
|
||||
/// Represents the flag commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Add(email::args::Ids<'a>, Flags),
|
||||
Set(email::args::Ids<'a>, Flags),
|
||||
Del(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 subcommand matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags: String = parse_flags_arg(m);
|
||||
Some(Cmd::Add(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
|
||||
debug!("set subcommand matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags: String = parse_flags_arg(m);
|
||||
Some(Cmd::Set(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DEL) {
|
||||
info!("remove subcommand matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags: String = parse_flags_arg(m);
|
||||
Some(Cmd::Del(ids, flags))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the flag subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_FLAG)
|
||||
.aliases(&["flags", "flg"])
|
||||
.about("Handles email flags")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_ADD)
|
||||
.aliases(&["a"])
|
||||
.about("Adds email flags")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_SET)
|
||||
.aliases(&["s", "change", "c"])
|
||||
.about("Sets email flags")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_DEL)
|
||||
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
|
||||
.about("Removes email flags")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Represents the flags argument.
|
||||
pub fn flags_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_FLAGS)
|
||||
.long_help("Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
|
||||
.value_name("FLAGS…")
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the flags argument parser.
|
||||
pub fn parse_flags_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
|
||||
matches
|
||||
.values_of(ARG_FLAGS)
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
//! Message flag handling module.
|
||||
//!
|
||||
//! This module gathers all flag actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::backend::Backend;
|
||||
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Adds flags to all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn add<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.flags_add(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully added to message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
||||
|
||||
/// Removes flags from all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn remove<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.flags_delete(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully removed from message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
||||
|
||||
/// Replaces flags of all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn set<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.flags_set(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully set for message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -1,153 +0,0 @@
|
|||
//! Folder CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher
|
||||
//! related to the folder domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use log::debug;
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_SOURCE: &str = "source";
|
||||
const ARG_TARGET: &str = "target";
|
||||
const CMD_FOLDERS: &str = "folders";
|
||||
|
||||
/// Represents the folder commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
List(table::args::MaxTableWidth),
|
||||
}
|
||||
|
||||
/// Represents the folder command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) {
|
||||
debug!("folders command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents folder subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_FOLDERS)
|
||||
.aliases(&[
|
||||
"folder",
|
||||
"fold",
|
||||
"fo",
|
||||
"mailboxes",
|
||||
"mailbox",
|
||||
"mboxes",
|
||||
"mbox",
|
||||
"mb",
|
||||
"m",
|
||||
])
|
||||
.about("Lists folders")
|
||||
.arg(table::args::max_width())]
|
||||
}
|
||||
|
||||
/// Represents the source folder argument.
|
||||
pub fn source_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_SOURCE)
|
||||
.short("f")
|
||||
.long("folder")
|
||||
.help("Specifies the source folder")
|
||||
.value_name("SOURCE")
|
||||
.default_value("inbox")
|
||||
}
|
||||
|
||||
/// Represents the source folder argument parser.
|
||||
pub fn parse_source_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_SOURCE).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the target folder argument.
|
||||
pub fn target_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_TARGET)
|
||||
.help("Specifies the target folder")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the target folder argument parser.
|
||||
pub fn parse_target_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_TARGET).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{App, ErrorKind};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds() {
|
||||
let arg = App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", "folders"]);
|
||||
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
|
||||
|
||||
let arg = App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", "folders", "--max-width", "20"]);
|
||||
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_aliases() {
|
||||
macro_rules! get_matches_from {
|
||||
($alias:expr) => {
|
||||
App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", $alias])
|
||||
.subcommand_name()
|
||||
};
|
||||
}
|
||||
|
||||
assert_eq!(Some("folders"), get_matches_from!["folders"]);
|
||||
assert_eq!(Some("folders"), get_matches_from!["folder"]);
|
||||
assert_eq!(Some("folders"), get_matches_from!["fold"]);
|
||||
assert_eq!(Some("folders"), get_matches_from!["fo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_source_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
App::new("himalaya")
|
||||
.arg(source_arg())
|
||||
.get_matches_from(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(Some("inbox"), app.value_of("source"));
|
||||
|
||||
let app = get_matches_from!["-f", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("source"));
|
||||
|
||||
let app = get_matches_from!["--folder", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("source"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_target_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
App::new("himalaya")
|
||||
.arg(target_arg())
|
||||
.get_matches_from_safe(&["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().value_of("target"));
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
use himalaya_lib::folder::Folder;
|
||||
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
impl Table for Folder {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("DELIM").bold().underline().white())
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("DESC").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.delim).white())
|
||||
.cell(Cell::new(&self.name).blue())
|
||||
.cell(Cell::new(&self.desc).green())
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::folder::Folders;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
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,185 +0,0 @@
|
|||
//! Folder handling module.
|
||||
//!
|
||||
//! This module gathers all folder actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::{AccountConfig, Backend};
|
||||
use log::trace;
|
||||
|
||||
use crate::printer::{PrintTableOpts, Printer};
|
||||
|
||||
/// Lists all folders.
|
||||
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let folders = backend.folder_list()?;
|
||||
trace!("folders: {:?}", folders);
|
||||
printer.print_table(
|
||||
// TODO: remove Box
|
||||
Box::new(folders),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders};
|
||||
use std::{fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::printer::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
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_str<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestBackend;
|
||||
|
||||
impl<'a> Backend<'a> for TestBackend {
|
||||
fn folder_add(&mut self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn folder_list(&mut self) -> backend::Result<Folders> {
|
||||
Ok(Folders(vec![
|
||||
Folder {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
Folder {
|
||||
delim: "/".into(),
|
||||
name: "Sent".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
]))
|
||||
}
|
||||
fn folder_delete(&mut self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn envelope_list(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn envelope_search(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_get(&mut self, _: &str, _: &str) -> backend::Result<Email> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn as_any(&self) -> &(dyn std::any::Any + 'a) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
let account_config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
|
||||
assert!(list(None, &account_config, &mut printer, &mut backend).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"DELIM │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,66 +0,0 @@
|
|||
//! Module related to IMAP CLI.
|
||||
//!
|
||||
//! This module provides subcommands and a command matcher related to IMAP.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, ArgMatches};
|
||||
use log::{debug, info};
|
||||
|
||||
type Keepalive = u64;
|
||||
|
||||
/// IMAP commands.
|
||||
pub enum Command {
|
||||
/// 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<Command>> {
|
||||
info!("entering imap command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("notify") {
|
||||
info!("notify command matched");
|
||||
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Command::Notify(keepalive)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("watch") {
|
||||
info!("watch command matched");
|
||||
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Command::Watch(keepalive)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// IMAP subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![
|
||||
clap::SubCommand::with_name("notify")
|
||||
.about("Notifies when new messages arrive in the given folder")
|
||||
.aliases(&["idle"])
|
||||
.arg(
|
||||
clap::Arg::with_name("keepalive")
|
||||
.help("Specifies the keepalive duration")
|
||||
.short("k")
|
||||
.long("keepalive")
|
||||
.value_name("SECS")
|
||||
.default_value("500"),
|
||||
),
|
||||
clap::SubCommand::with_name("watch")
|
||||
.about("Watches IMAP server changes")
|
||||
.arg(
|
||||
clap::Arg::with_name("keepalive")
|
||||
.help("Specifies the keepalive duration")
|
||||
.short("k")
|
||||
.long("keepalive")
|
||||
.value_name("SECS")
|
||||
.default_value("500"),
|
||||
),
|
||||
]
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
//! Module related to IMAP handling.
|
||||
//!
|
||||
//! This module gathers all IMAP handlers triggered by the CLI.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::ImapBackend;
|
||||
|
||||
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.notify(keepalive, mbox).context("cannot imap notify")
|
||||
}
|
||||
|
||||
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.watch(keepalive, mbox).context("cannot imap watch")
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -1,17 +0,0 @@
|
|||
pub mod account;
|
||||
pub mod email;
|
||||
pub mod envelope;
|
||||
pub mod flag;
|
||||
pub mod folder;
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub mod imap;
|
||||
pub mod tpl;
|
||||
|
||||
pub use self::account::{args, handlers, Account, Accounts};
|
||||
pub use self::email::*;
|
||||
pub use self::envelope::*;
|
||||
pub use self::flag::*;
|
||||
pub use self::folder::*;
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub use self::imap::*;
|
||||
pub use self::tpl::*;
|
|
@ -1,185 +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::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::email::TplOverride;
|
||||
use log::debug;
|
||||
|
||||
use crate::email;
|
||||
|
||||
const ARG_BCC: &str = "bcc";
|
||||
const ARG_BODY: &str = "body";
|
||||
const ARG_CC: &str = "cc";
|
||||
const ARG_FROM: &str = "from";
|
||||
const ARG_HEADERS: &str = "header";
|
||||
const ARG_SIGNATURE: &str = "signature";
|
||||
const ARG_SUBJECT: &str = "subject";
|
||||
const ARG_TO: &str = "to";
|
||||
const ARG_TPL: &str = "template";
|
||||
const CMD_FORWARD: &str = "forward";
|
||||
const CMD_NEW: &str = "new";
|
||||
const CMD_REPLY: &str = "reply";
|
||||
const CMD_SAVE: &str = "save";
|
||||
const CMD_SEND: &str = "send";
|
||||
|
||||
pub(crate) const CMD_TPL: &str = "template";
|
||||
|
||||
type Tpl<'a> = &'a str;
|
||||
|
||||
/// Represents the template commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Forward(email::args::Id<'a>, TplOverride<'a>),
|
||||
New(TplOverride<'a>),
|
||||
Reply(email::args::Id<'a>, email::args::All, TplOverride<'a>),
|
||||
Save(email::args::Attachments<'a>, Tpl<'a>),
|
||||
Send(email::args::Attachments<'a>, Tpl<'a>),
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
debug!("forward subcommand matched");
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let tpl = parse_override_arg(m);
|
||||
Some(Cmd::Forward(id, tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_NEW) {
|
||||
debug!("new subcommand matched");
|
||||
let tpl = parse_override_arg(m);
|
||||
Some(Cmd::New(tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
debug!("reply subcommand matched");
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let all = email::args::parse_reply_all_flag(m);
|
||||
let tpl = parse_override_arg(m);
|
||||
Some(Cmd::Reply(id, all, tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
debug!("save subcommand matched");
|
||||
let attachments = email::args::parse_attachments_arg(m);
|
||||
let tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Save(attachments, tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
||||
debug!("send subcommand matched");
|
||||
let attachments = email::args::parse_attachments_arg(m);
|
||||
let tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Send(attachments, tpl))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the template subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_TPL)
|
||||
.aliases(&["tpl"])
|
||||
.about("Handles email templates")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_NEW)
|
||||
.aliases(&["n"])
|
||||
.about("Generates a template for a new email")
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_REPLY)
|
||||
.aliases(&["rep", "re", "r"])
|
||||
.about("Generates a template for replying to an email")
|
||||
.arg(email::args::id_arg())
|
||||
.arg(email::args::reply_all_flag())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_FORWARD)
|
||||
.aliases(&["fwd", "fw", "f"])
|
||||
.about("Generates a template for forwarding an email")
|
||||
.arg(email::args::id_arg())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_SAVE)
|
||||
.about("Saves an email based on the given template")
|
||||
.arg(&email::args::attachments_arg())
|
||||
.arg(Arg::with_name(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_SEND)
|
||||
.about("Sends an email based on the given template")
|
||||
.arg(&email::args::attachments_arg())
|
||||
.arg(Arg::with_name(ARG_TPL).raw(true)),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Represents the template arguments.
|
||||
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
|
||||
vec![
|
||||
Arg::with_name(ARG_SUBJECT)
|
||||
.help("Overrides the Subject header")
|
||||
.short("s")
|
||||
.long("subject")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name(ARG_FROM)
|
||||
.help("Overrides the From header")
|
||||
.short("f")
|
||||
.long("from")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_TO)
|
||||
.help("Overrides the To header")
|
||||
.short("t")
|
||||
.long("to")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_CC)
|
||||
.help("Overrides the Cc header")
|
||||
.short("c")
|
||||
.long("cc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_BCC)
|
||||
.help("Overrides the Bcc header")
|
||||
.short("b")
|
||||
.long("bcc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_HEADERS)
|
||||
.help("Overrides a specific header")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("KEY:VAL")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_BODY)
|
||||
.help("Overrides the body")
|
||||
.short("B")
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name(ARG_SIGNATURE)
|
||||
.help("Overrides the signature")
|
||||
.short("S")
|
||||
.long("signature")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Represents the template override argument parser.
|
||||
pub fn parse_override_arg<'a>(matches: &'a ArgMatches<'a>) -> TplOverride {
|
||||
TplOverride {
|
||||
subject: matches.value_of(ARG_SUBJECT),
|
||||
from: matches.values_of(ARG_FROM).map(Iterator::collect),
|
||||
to: matches.values_of(ARG_TO).map(Iterator::collect),
|
||||
cc: matches.values_of(ARG_CC).map(Iterator::collect),
|
||||
bcc: matches.values_of(ARG_BCC).map(Iterator::collect),
|
||||
headers: matches.values_of(ARG_HEADERS).map(Iterator::collect),
|
||||
body: matches.value_of(ARG_BODY),
|
||||
signature: matches.value_of(ARG_SIGNATURE),
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw template argument parser.
|
||||
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_TPL).unwrap_or_default()
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
//! Module related to message template handling.
|
||||
//!
|
||||
//! This module gathers all message template commands.
|
||||
|
||||
use anyhow::Result;
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{AccountConfig, Backend, Email, Sender, TplOverride};
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Generate a new message template.
|
||||
pub fn new<'a, P: Printer>(
|
||||
opts: TplOverride<'a>,
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
) -> Result<()> {
|
||||
let tpl = Email::default().to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a reply message template.
|
||||
pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
opts: TplOverride<'_>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a forward message template.
|
||||
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
opts: TplOverride<'_>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Saves a message based on a template.
|
||||
pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
let msg = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let raw_msg = msg.into_sendable_msg(config)?.formatted();
|
||||
backend.email_add(mbox, &raw_msg, "seen")?;
|
||||
printer.print_struct("Template successfully saved")
|
||||
}
|
||||
|
||||
/// Sends a message based on a template.
|
||||
pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
mbox: &str,
|
||||
account: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
let msg = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let sent_msg = sender.send(account, &msg)?;
|
||||
backend.email_add(mbox, &sent_msg, "seen")?;
|
||||
printer.print_struct("Template successfully sent")
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
42
src/email/envelope/flag/command/mod.rs
Normal file
42
src/email/envelope/flag/command/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
mod add;
|
||||
mod remove;
|
||||
mod set;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
|
||||
|
||||
/// Add, change and remove your envelopes flags.
|
||||
///
|
||||
/// A flag is a tag associated to an envelope. Existing flags are
|
||||
/// seen, answered, flagged, deleted, draft. Other flags are
|
||||
/// considered custom, which are not always supported.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum FlagSubcommand {
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(alias = "create")]
|
||||
Add(FlagAddCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(aliases = ["update", "change", "replace"])]
|
||||
Set(FlagSetCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(aliases = ["rm", "delete", "del"])]
|
||||
Remove(FlagRemoveCommand),
|
||||
}
|
||||
|
||||
impl FlagSubcommand {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Set(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Remove(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
64
src/email/envelope/flag/command/remove.rs
Normal file
64
src/email/envelope/flag/command/remove.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,
|
||||
};
|
||||
|
||||
/// Remove flag(s) from a given envelope.
|
||||
///
|
||||
/// This command allows you to remove the given flag(s) from the given
|
||||
/// envelope(s).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FlagRemoveCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FlagRemoveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing remove 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_remove_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.remove_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.out(format!("Flag(s) {flags} successfully removed!\n"))
|
||||
}
|
||||
}
|
64
src/email/envelope/flag/command/set.rs
Normal file
64
src/email/envelope/flag/command/set.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,
|
||||
};
|
||||
|
||||
/// Replace flag(s) of a given envelope.
|
||||
///
|
||||
/// This command allows you to replace existing flags of the given
|
||||
/// envelope(s) with the given flag(s).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FlagSetCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub args: IdsAndFlagsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl FlagSetCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing set 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_set_flags(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.set_flags(folder, &ids, &flags).await?;
|
||||
|
||||
printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
|
||||
}
|
||||
}
|
2
src/email/envelope/flag/mod.rs
Normal file
2
src/email/envelope/flag/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
3
src/email/envelope/mod.rs
Normal file
3
src/email/envelope/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod flag;
|
25
src/email/message/arg/body.rs
Normal file
25
src/email/message/arg/body.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use clap::Parser;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// The raw message body argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageRawBodyArg {
|
||||
/// Prefill the template with a custom body.
|
||||
#[arg(trailing_var_arg = true)]
|
||||
#[arg(name = "body_raw", value_name = "BODY")]
|
||||
pub raw: Vec<String>,
|
||||
}
|
||||
|
||||
impl MessageRawBodyArg {
|
||||
pub fn raw(self) -> String {
|
||||
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MessageRawBodyArg {
|
||||
type Target = Vec<String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
}
|
||||
}
|
20
src/email/message/arg/header.rs
Normal file
20
src/email/message/arg/header.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use clap::Parser;
|
||||
|
||||
/// The envelope id argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct HeaderRawArgs {
|
||||
/// Prefill the template with custom headers.
|
||||
///
|
||||
/// A raw header should follow the pattern KEY:VAL.
|
||||
#[arg(long = "header", short = 'H', required = false)]
|
||||
#[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)]
|
||||
pub raw: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> {
|
||||
if let Some((key, val)) = raw_header.split_once(':') {
|
||||
Ok((key.trim().to_owned(), val.trim().to_owned()))
|
||||
} else {
|
||||
Err(format!("cannot parse raw header {raw_header:?}"))
|
||||
}
|
||||
}
|
20
src/email/message/arg/mod.rs
Normal file
20
src/email/message/arg/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use clap::Parser;
|
||||
|
||||
pub mod body;
|
||||
pub mod header;
|
||||
pub mod reply;
|
||||
|
||||
/// The raw message argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageRawArg {
|
||||
/// The raw message, including headers and body.
|
||||
#[arg(trailing_var_arg = true)]
|
||||
#[arg(name = "message_raw", value_name = "MESSAGE")]
|
||||
pub raw: Vec<String>,
|
||||
}
|
||||
|
||||
impl MessageRawArg {
|
||||
pub fn raw(self) -> String {
|
||||
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
|
||||
}
|
||||
}
|
12
src/email/message/arg/reply.rs
Normal file
12
src/email/message/arg/reply.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use clap::Parser;
|
||||
|
||||
/// The reply to all argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageReplyAllArg {
|
||||
/// Reply to all recipients.
|
||||
///
|
||||
/// This argument will add all recipients for the To and Cc
|
||||
/// headers.
|
||||
#[arg(long, short = 'A')]
|
||||
pub all: bool,
|
||||
}
|
105
src/email/message/attachment/command/download.rs
Normal file
105
src/email/message/attachment/command/download.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use clap::Parser;
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Download all attachments found in the given message.
|
||||
///
|
||||
/// This command allows you to download all attachments found for the
|
||||
/// given message to your downloads directory.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AttachmentDownloadCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl AttachmentDownloadCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing download attachment(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let emails = backend.get_messages(folder, ids).await?;
|
||||
|
||||
let mut emails_count = 0;
|
||||
let mut attachments_count = 0;
|
||||
|
||||
let mut ids = ids.iter();
|
||||
for email in emails.to_vec() {
|
||||
let id = ids.next().unwrap();
|
||||
let attachments = email.attachments()?;
|
||||
|
||||
if attachments.is_empty() {
|
||||
printer.log(format!("No attachment found for message {id}!\n"))?;
|
||||
continue;
|
||||
} else {
|
||||
emails_count += 1;
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"{} attachment(s) found for message {id}!\n",
|
||||
attachments.len()
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let filename: PathBuf = attachment
|
||||
.filename
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||
.into();
|
||||
let filepath = account_config.get_download_file_path(&filename)?;
|
||||
printer.log(format!("Downloading {:?}…\n", filepath))?;
|
||||
fs::write(&filepath, &attachment.body)
|
||||
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
|
||||
attachments_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
match attachments_count {
|
||||
0 => printer.out("No attachment found!\n"),
|
||||
1 => printer.out("Downloaded 1 attachment!\n"),
|
||||
n => printer.out(format!(
|
||||
"Downloaded {} attachment(s) from {} messages(s)!\n",
|
||||
n, emails_count,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
28
src/email/message/attachment/command/mod.rs
Normal file
28
src/email/message/attachment/command/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
mod download;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::download::AttachmentDownloadCommand;
|
||||
|
||||
/// Download your message attachments.
|
||||
///
|
||||
/// A message body can be composed of multiple MIME parts. An
|
||||
/// attachment is the representation of a binary part of a message
|
||||
/// body.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AttachmentSubcommand {
|
||||
#[command(arg_required_else_help = true, alias = "dl")]
|
||||
Download(AttachmentDownloadCommand),
|
||||
}
|
||||
|
||||
impl AttachmentSubcommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Download(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
1
src/email/message/attachment/mod.rs
Normal file
1
src/email/message/attachment/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod command;
|
69
src/email/message/command/copy.rs
Normal file
69
src/email/message/command/copy.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
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,
|
||||
envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
|
||||
};
|
||||
|
||||
/// Copy the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageCopyCommand {
|
||||
#[command(flatten)]
|
||||
pub source_folder: SourceFolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub target_folder: TargetFolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageCopyCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing copy message(s) command");
|
||||
|
||||
let source = &self.source_folder.name;
|
||||
let target = &self.target_folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_copy_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.copy_messages(source, target, ids).await?;
|
||||
|
||||
printer.out(format!(
|
||||
"Message(s) successfully copied from {source} to {target}!\n"
|
||||
))
|
||||
}
|
||||
}
|
65
src/email/message/command/delete.rs
Normal file
65
src/email/message/command/delete.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
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, envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Mark as deleted the message associated to the given envelope id(s).
|
||||
///
|
||||
/// This command does not really delete the message: if the given
|
||||
/// folder points to the trash folder, it adds the "deleted" flag to
|
||||
/// its envelope, otherwise it moves it to the trash folder. Only the
|
||||
/// expunge folder command truly deletes messages.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageDeleteCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageDeleteCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing delete message(s) command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_delete_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.delete_messages(folder, ids).await?;
|
||||
|
||||
printer.out(format!("Message(s) successfully removed from {folder}!\n"))
|
||||
}
|
||||
}
|
103
src/email/message/command/edit.rs
Normal file
103
src/email/message/command/edit.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Edit the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to edit the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
/// edition process finishes, you can choose between saving or sending
|
||||
/// the final message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageEditCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// List of headers that should be visible at the top of the
|
||||
/// message.
|
||||
///
|
||||
/// If a given header is not found in the message, it will not be
|
||||
/// visible. If no header is given, defaults to the one set up in
|
||||
/// your TOML configuration file.
|
||||
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||
pub headers: Vec<String>,
|
||||
|
||||
/// Edit the message on place.
|
||||
///
|
||||
/// If set, the original message being edited will be removed at
|
||||
/// the end of the command. Useful when you need, for example, to
|
||||
/// edit a draft, send it then remove it from the Drafts folder.
|
||||
#[arg(long, short = 'p')]
|
||||
pub on_place: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageEditCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing edit message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
.with_delete_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message"))?
|
||||
.to_read_tpl(&account_config, |mut tpl| {
|
||||
if !self.headers.is_empty() {
|
||||
tpl = tpl.with_show_only_headers(&self.headers);
|
||||
}
|
||||
|
||||
tpl
|
||||
})
|
||||
.await?;
|
||||
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
|
||||
|
||||
if self.on_place {
|
||||
backend.delete_messages(folder, &[id]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
155
src/email/message/command/export.rs
Normal file
155
src/email/message/command/export.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use std::{
|
||||
env::temp_dir,
|
||||
fs,
|
||||
io::{stdout, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
};
|
||||
|
||||
/// Export the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to export a message. A message can be
|
||||
/// fully exported in one single file, or exported in multiple files
|
||||
/// (one per MIME part found in the message). This is useful, for
|
||||
/// example, to read a HTML message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageExportCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
/// Export the full raw message as one unique .eml file.
|
||||
///
|
||||
/// The raw message represents the headers and the body as it is
|
||||
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||
/// useful for debugging faulty messages, but also for
|
||||
/// saving/sending/transfering messages.
|
||||
#[arg(long, short = 'F')]
|
||||
pub full: bool,
|
||||
|
||||
/// Try to open the exported message, when applicable.
|
||||
///
|
||||
/// This argument only works with full message export, or when
|
||||
/// HTML or plain text is present in the export.
|
||||
#[arg(long, short = 'O')]
|
||||
pub open: bool,
|
||||
|
||||
/// Where the message should be exported to.
|
||||
///
|
||||
/// The destination should point to a valid directory. If `--full`
|
||||
/// is given, it can also point to a .eml file.
|
||||
#[arg(long, short, alias = "dest")]
|
||||
pub destination: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageExportCommand {
|
||||
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing export message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
let id = &self.envelope.id;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_get_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let msgs = backend.get_messages(folder, &[*id]).await?;
|
||||
let msg = msgs.first().ok_or(eyre!("cannot find message {id}"))?;
|
||||
|
||||
if self.full {
|
||||
let bytes = msg.raw()?;
|
||||
|
||||
match self.destination {
|
||||
Some(mut dest) if dest.is_dir() => {
|
||||
dest.push(format!("{id}.eml"));
|
||||
fs::write(&dest, bytes)?;
|
||||
let dest = dest.display();
|
||||
println!("Message {id} successfully exported at {dest}!");
|
||||
}
|
||||
Some(dest) => {
|
||||
fs::write(&dest, bytes)?;
|
||||
let dest = dest.display();
|
||||
println!("Message {id} successfully exported at {dest}!");
|
||||
}
|
||||
None => {
|
||||
stdout().write_all(bytes)?;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let dest = match self.destination {
|
||||
Some(dest) if dest.is_dir() => {
|
||||
let dest = msg.download_parts(&dest)?;
|
||||
let d = dest.display();
|
||||
println!("Message {id} successfully exported in {d}!");
|
||||
dest
|
||||
}
|
||||
Some(dest) if dest.is_file() => {
|
||||
let dest = dest.parent().unwrap_or(&dest);
|
||||
let dest = msg.download_parts(&dest)?;
|
||||
let d = dest.display();
|
||||
println!("Message {id} successfully exported in {d}!");
|
||||
dest
|
||||
}
|
||||
Some(dest) => {
|
||||
return Err(eyre!("Destination {} does not exist!", dest.display()));
|
||||
}
|
||||
None => {
|
||||
let dest = temp_dir();
|
||||
let dest = msg.download_parts(&dest)?;
|
||||
let d = dest.display();
|
||||
println!("Message {id} successfully exported in {d}!");
|
||||
dest
|
||||
}
|
||||
};
|
||||
|
||||
if self.open {
|
||||
let index_html = dest.join("index.html");
|
||||
if index_html.exists() {
|
||||
return Ok(open::that(index_html)?);
|
||||
}
|
||||
|
||||
let plain_txt = dest.join("plain.txt");
|
||||
if plain_txt.exists() {
|
||||
return Ok(open::that(plain_txt)?);
|
||||
}
|
||||
|
||||
println!("--open was passed but nothing to open, ignoring");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
84
src/email/message/command/forward.rs
Normal file
84
src/email/message/command/forward.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdArg,
|
||||
folder::arg::name::FolderNameOptionalFlag,
|
||||
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||
};
|
||||
|
||||
/// Forward the message associated to the given envelope id.
|
||||
///
|
||||
/// This command allows you to forward the given message using the
|
||||
/// editor defined in your environment variable $EDITOR. When the
|
||||
/// edition process finishes, you can choose between saving or sending
|
||||
/// the final message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageForwardCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelope: EnvelopeIdArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub headers: HeaderRawArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub body: MessageRawBodyArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageForwardCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing forward message command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let id = self.envelope.id;
|
||||
let tpl = backend
|
||||
.get_messages(folder, &[id])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or(eyre!("cannot find message"))?
|
||||
.to_forward_tpl_builder(account_config.clone())
|
||||
.with_headers(self.headers.raw)
|
||||
.with_body(self.body.raw())
|
||||
.build()
|
||||
.await?;
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
}
|
98
src/email/message/command/mailto.rs
Normal file
98
src/email/message/command/mailto.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::{backend::BackendBuilder, editor},
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
|
||||
|
||||
/// Parse and edit a message from the given mailto URL string.
|
||||
///
|
||||
/// This command allows you to edit a message from the mailto format
|
||||
/// using the editor defined in your environment variable
|
||||
/// $EDITOR. When the edition process finishes, you can choose between
|
||||
/// saving or sending the final message.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageMailtoCommand {
|
||||
/// The mailto url.
|
||||
#[arg()]
|
||||
pub url: Url,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageMailtoCommand {
|
||||
pub fn new(url: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: Url::parse(url)?,
|
||||
account: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing mailto message command");
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let account_config = Arc::new(account_config);
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
account_config.clone(),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_add_message(BackendFeatureSource::Context)
|
||||
.with_send_message(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut msg = Vec::<u8>::new();
|
||||
let mut body = Vec::<u8>::new();
|
||||
|
||||
msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n");
|
||||
|
||||
for (key, val) in self.url.query_pairs() {
|
||||
if key.eq_ignore_ascii_case("body") {
|
||||
body.extend(val.as_bytes());
|
||||
} else {
|
||||
msg.extend(key.as_bytes());
|
||||
msg.extend(b": ");
|
||||
msg.extend(val.as_bytes());
|
||||
msg.extend(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
msg.extend(b"\r\n");
|
||||
msg.extend(body);
|
||||
|
||||
if let Some(sig) = account_config.find_full_signature() {
|
||||
msg.extend(b"\r\n");
|
||||
msg.extend(sig.as_bytes());
|
||||
}
|
||||
|
||||
let tpl = account_config
|
||||
.generate_tpl_interpreter()
|
||||
.with_show_only_headers(account_config.get_message_write_headers())
|
||||
.build()
|
||||
.from_bytes(msg)
|
||||
.await?
|
||||
.into();
|
||||
|
||||
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
|
||||
}
|
||||
}
|
94
src/email/message/command/mod.rs
Normal file
94
src/email/message/command/mod.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod edit;
|
||||
pub mod export;
|
||||
pub mod forward;
|
||||
pub mod mailto;
|
||||
pub mod r#move;
|
||||
pub mod read;
|
||||
pub mod reply;
|
||||
pub mod save;
|
||||
pub mod send;
|
||||
pub mod thread;
|
||||
pub mod write;
|
||||
|
||||
use clap::Subcommand;
|
||||
use color_eyre::Result;
|
||||
use pimalaya_tui::terminal::cli::printer::Printer;
|
||||
|
||||
use crate::config::TomlConfig;
|
||||
|
||||
use self::{
|
||||
copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand,
|
||||
export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand,
|
||||
r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand,
|
||||
save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand,
|
||||
write::MessageWriteCommand,
|
||||
};
|
||||
|
||||
/// Read, write, send, copy, move and delete your messages.
|
||||
///
|
||||
/// A message is the content of an email. It is composed of headers
|
||||
/// (located at the top of the message) and a body (located at the
|
||||
/// bottom of the message). Both are separated by two new lines. This
|
||||
/// subcommand allows you to manage them.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum MessageSubcommand {
|
||||
#[command(arg_required_else_help = true)]
|
||||
Read(MessageReadCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Export(MessageExportCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Thread(MessageThreadCommand),
|
||||
|
||||
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||
Write(MessageWriteCommand),
|
||||
|
||||
Reply(MessageReplyCommand),
|
||||
|
||||
#[command(aliases = ["fwd", "fd"])]
|
||||
Forward(MessageForwardCommand),
|
||||
|
||||
Edit(MessageEditCommand),
|
||||
|
||||
Mailto(MessageMailtoCommand),
|
||||
|
||||
Save(MessageSaveCommand),
|
||||
|
||||
Send(MessageSendCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(aliases = ["cpy", "cp"])]
|
||||
Copy(MessageCopyCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(alias = "mv")]
|
||||
Move(MessageMoveCommand),
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
#[command(aliases = ["remove", "rm"])]
|
||||
Delete(MessageDeleteCommand),
|
||||
}
|
||||
|
||||
impl MessageSubcommand {
|
||||
#[allow(unused)]
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::Read(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Export(cmd) => cmd.execute(config).await,
|
||||
Self::Thread(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Write(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Reply(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Forward(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Edit(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Mailto(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Save(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Send(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Copy(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Move(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Delete(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
70
src/email/message/command/move.rs
Normal file
70
src/email/message/command/move.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use email::{backend::feature::BackendFeatureSource, config::Config};
|
||||
use pimalaya_tui::{
|
||||
himalaya::backend::BackendBuilder,
|
||||
terminal::{cli::printer::Printer, config::TomlConfig as _},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[allow(unused)]
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
config::TomlConfig,
|
||||
envelope::arg::ids::EnvelopeIdsArgs,
|
||||
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
|
||||
};
|
||||
|
||||
/// Move the message associated to the given envelope id(s) to the
|
||||
/// given target folder.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageMoveCommand {
|
||||
#[command(flatten)]
|
||||
pub source_folder: SourceFolderNameOptionalFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub target_folder: TargetFolderNameArg,
|
||||
|
||||
#[command(flatten)]
|
||||
pub envelopes: EnvelopeIdsArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
}
|
||||
|
||||
impl MessageMoveCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing move message(s) command");
|
||||
|
||||
let source = &self.source_folder.name;
|
||||
let target = &self.target_folder.name;
|
||||
let ids = &self.envelopes.ids;
|
||||
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
|
||||
c.account(name).ok()
|
||||
})?;
|
||||
|
||||
let backend = BackendBuilder::new(
|
||||
Arc::new(toml_account_config),
|
||||
Arc::new(account_config),
|
||||
|builder| {
|
||||
builder
|
||||
.without_features()
|
||||
.with_move_messages(BackendFeatureSource::Context)
|
||||
},
|
||||
)
|
||||
.without_sending_backend()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
backend.move_messages(source, target, ids).await?;
|
||||
|
||||
printer.out(format!(
|
||||
"Message(s) successfully moved from {source} to {target}!\n"
|
||||
))
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue