mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-20 08:13:39 +00:00
Compare commits
597 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 | ||
![]() |
cd4575eb5e | ||
![]() |
15e8a0f08f | ||
![]() |
82133b30d9 | ||
![]() |
8125a55bbe | ||
![]() |
4fe5d246f1 | ||
![]() |
cdc0e0aa6a | ||
![]() |
29aa383147 | ||
![]() |
1dcdfa8afa | ||
![]() |
7777eca667 | ||
![]() |
dda90809cb | ||
![]() |
abb9f4172b | ||
![]() |
329af51534 | ||
![]() |
3feccc3225 | ||
![]() |
44b980c329 | ||
![]() |
a3686c1c44 | ||
![]() |
82b7dfb97f | ||
![]() |
672666734b | ||
![]() |
ceebf643c4 | ||
![]() |
7b9cfc4512 | ||
![]() |
29c731336f | ||
![]() |
9bcd659af2 | ||
![]() |
1e4dc0cb5a | ||
![]() |
c0e002ea1b | ||
![]() |
a5c4fdaac6 | ||
![]() |
3b2991ae56 | ||
![]() |
3c5379b24d | ||
![]() |
8f667def0c | ||
![]() |
ca67780341 | ||
![]() |
e1c92d3f57 | ||
![]() |
7c01f88006 | ||
![]() |
a0461d84ba | ||
![]() |
3f5feed0ff | ||
![]() |
0e98def513 | ||
![]() |
cc918e0eee | ||
![]() |
b6643be03f | ||
![]() |
bed5a3856b | ||
![]() |
0696f36f05 | ||
![]() |
ba8ef9adf6 | ||
![]() |
5a2d7fa6b5 | ||
![]() |
4d91a5d74e | ||
![]() |
b7157573f2 | ||
![]() |
6d154abcb5 | ||
![]() |
0ddcce22e6 |
186 changed files with 11005 additions and 11804 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
|
45
.gitignore
vendored
45
.gitignore
vendored
|
@ -1,10 +1,49 @@
|
|||
# Cargo config directory
|
||||
.cargo/
|
||||
|
||||
# Cargo build directory
|
||||
/target
|
||||
target/
|
||||
debug/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Nix build directory
|
||||
/result
|
||||
/result-lib
|
||||
result
|
||||
result-*
|
||||
|
||||
# Direnv
|
||||
/.envrc
|
||||
/.direnv
|
||||
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
.idea/
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
## Others
|
||||
.metadata/
|
||||
|
|
982
CHANGELOG.md
982
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).
|
||||
|
|
5161
Cargo.lock
generated
5161
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
72
Cargo.toml
72
Cargo.toml
|
@ -1,2 +1,70 @@
|
|||
[workspace]
|
||||
members = ["lib", "cli"]
|
||||
[package]
|
||||
name = "himalaya"
|
||||
description = "CLI to manage emails"
|
||||
version = "1.1.0"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
categories = ["command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "maildir", "smtp"]
|
||||
homepage = "https://pimalaya.org/"
|
||||
documentation = "https://github.com/pimalaya/himalaya/"
|
||||
repository = "https://github.com/pimalaya/himalaya/"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["imap", "maildir", "smtp", "sendmail", "wizard", "pgp-commands"]
|
||||
imap = ["email-lib/imap", "pimalaya-tui/imap"]
|
||||
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
|
||||
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
|
||||
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
|
||||
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
|
||||
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
|
||||
oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
|
||||
wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
|
||||
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"]
|
||||
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"]
|
||||
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"]
|
||||
|
||||
[build-dependencies]
|
||||
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
|
||||
|
||||
[dependencies]
|
||||
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"] }
|
||||
|
||||
[patch.crates-io]
|
||||
imap-codec.git = "https://github.com/duesee/imap-codec"
|
||||
|
||||
email-lib.git = "https://github.com/pimalaya/core"
|
||||
imap-client.git = "https://github.com/pimalaya/imap-client"
|
||||
keyring-lib.git = "https://github.com/pimalaya/core"
|
||||
mml-lib.git = "https://github.com/pimalaya/core"
|
||||
oauth-lib.git = "https://github.com/pimalaya/core"
|
||||
pgp-lib.git = "https://github.com/pimalaya/core"
|
||||
pimalaya-tui.git = "https://github.com/pimalaya/tui"
|
||||
process-lib.git = "https://github.com/pimalaya/core"
|
||||
secret-lib.git = "https://github.com/pimalaya/core"
|
||||
|
|
45
LICENSE
45
LICENSE
|
@ -1,32 +1,21 @@
|
|||
Copyright (c) 2020-2021, soywod (Clément DOUIN) <clement.douin@posteo.net>
|
||||
MIT License
|
||||
|
||||
All rights reserved.
|
||||
Copyright (c) 2022-2024 soywod <clement.douin@posteo.net>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. All advertising materials mentioning features or use of this software must
|
||||
display the following acknowledgement:
|
||||
This product includes software developed by Clément DOUIN.
|
||||
|
||||
4. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
776
README.md
776
README.md
|
@ -1,106 +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
|
||||
|
||||
*The project is under active development. Do not use in production
|
||||
before the `v1.0.0`.*
|
||||
|
||||

|
||||
|
||||
## Motivation
|
||||
|
||||
Bringing emails to the terminal is a *pain*. First, because they are
|
||||
sensitive data. Secondly, the existing TUIs
|
||||
([Mutt](http://www.mutt.org/), [NeoMutt](https://neomutt.org/),
|
||||
[Alpine](https://alpine.x10host.com/),
|
||||
[aerc](https://aerc-mail.org/)…) are really hard to configure. They
|
||||
require time and patience.
|
||||
|
||||
The aim of Himalaya is to extract the email logic into a simple (yet
|
||||
solid) CLI API that can be used directly from the terminal, from
|
||||
scripts, from UIs… Possibilities are endless!
|
||||
|
||||
## 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
|
||||
|
||||
name = "Your full name"
|
||||
downloads-dir = "/abs/path/to/downloads"
|
||||
signature = """
|
||||
Cordialement,
|
||||
Regards,
|
||||
"""
|
||||
|
||||
[gmail]
|
||||
default = true
|
||||
email = "your.email@gmail.com"
|
||||
|
||||
imap-host = "imap.gmail.com"
|
||||
imap-port = 993
|
||||
imap-login = "your.email@gmail.com"
|
||||
imap-passwd-cmd = "pass show gmail"
|
||||
|
||||
smtp-host = "smtp.gmail.com"
|
||||
smtp-port = 465
|
||||
smtp-login = "your.email@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
|
||||
|
||||
- Mailbox 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:msg: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).*
|
||||
|
||||
## Installation
|
||||
|
||||
<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/)
|
||||
|
||||
## Credits
|
||||
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:
|
||||
|
||||
- [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
|
||||
- [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();
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management"
|
||||
version = "0.5.10"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2018"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||
keywords = ["cli", "mail", "email", "client", "imap"]
|
||||
homepage = "https://github.com/soywod/himalaya/wiki"
|
||||
documentation = "https://github.com/soywod/himalaya/wiki"
|
||||
repository = "https://github.com/soywod/himalaya"
|
||||
|
||||
[package.metadata.deb]
|
||||
priority = "optional"
|
||||
section = "mail"
|
||||
|
||||
[features]
|
||||
imap-backend = ["imap", "imap-proto"]
|
||||
maildir-backend = ["maildir", "md5"]
|
||||
notmuch-backend = ["notmuch", "maildir-backend"]
|
||||
default = ["imap-backend", "maildir-backend"]
|
||||
|
||||
[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"
|
||||
html-escape = "0.2.9"
|
||||
lettre = { version = "0.10.0-rc.1", 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"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
# Optional dependencies:
|
||||
|
||||
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 }
|
|
@ -1,47 +0,0 @@
|
|||
//! Backend module.
|
||||
//!
|
||||
//! This module exposes the backend trait, which can be used to create
|
||||
//! custom backend implementations.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
pub trait Backend<'a> {
|
||||
fn connect(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_mbox(&mut self, mbox: &str) -> Result<()>;
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>>;
|
||||
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>>;
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>>;
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>>;
|
||||
fn get_msg(&mut self, mbox: &str, id: &str) -> Result<Msg>;
|
||||
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
|
||||
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
|
||||
fn del_msg(&mut self, mbox: &str, ids: &str) -> Result<()>;
|
||||
fn add_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
|
||||
fn set_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
|
||||
fn del_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
|
||||
|
||||
fn disconnect(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::OpenOptions,
|
||||
io::{BufRead, BufReader, Write},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdMapper {
|
||||
path: PathBuf,
|
||||
map: HashMap<String, String>,
|
||||
short_hash_len: usize,
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn new(dir: &Path) -> Result<Self> {
|
||||
let mut mapper = Self::default();
|
||||
mapper.path = dir.join(".himalaya-id-map");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&mapper.path)
|
||||
.context("cannot open id hash map file")?;
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let line =
|
||||
line.context("cannot read line from maildir envelopes id mapper cache file")?;
|
||||
if mapper.short_hash_len == 0 {
|
||||
mapper.short_hash_len = 2.max(line.parse().unwrap_or(2));
|
||||
} else {
|
||||
let (hash, id) = line.split_once(' ').ok_or_else(|| {
|
||||
anyhow!(
|
||||
"cannot parse line {:?} from maildir envelopes id mapper cache file",
|
||||
line
|
||||
)
|
||||
})?;
|
||||
mapper.insert(hash.to_owned(), id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mapper)
|
||||
}
|
||||
|
||||
pub fn find(&self, short_hash: &str) -> Result<String> {
|
||||
let matching_hashes: Vec<_> = self
|
||||
.keys()
|
||||
.filter(|hash| hash.starts_with(short_hash))
|
||||
.collect();
|
||||
if matching_hashes.len() == 0 {
|
||||
Err(anyhow!(
|
||||
"cannot find maildir message id from short hash {:?}",
|
||||
short_hash,
|
||||
))
|
||||
} else if matching_hashes.len() > 1 {
|
||||
Err(anyhow!(
|
||||
"the short hash {:?} matches more than one hash: {}",
|
||||
short_hash,
|
||||
matching_hashes
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
.context(format!(
|
||||
"cannot find maildir message id from short hash {:?}",
|
||||
short_hash
|
||||
)))
|
||||
} else {
|
||||
Ok(self.get(matching_hashes[0]).unwrap().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, lines: Vec<(String, String)>) -> Result<usize> {
|
||||
self.extend(lines);
|
||||
|
||||
let mut entries = String::new();
|
||||
let mut short_hash_len = self.short_hash_len;
|
||||
|
||||
for (hash, id) in self.iter() {
|
||||
loop {
|
||||
let short_hash = &hash[0..short_hash_len];
|
||||
let conflict_found = self
|
||||
.map
|
||||
.keys()
|
||||
.find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash)
|
||||
.is_some();
|
||||
if short_hash_len > 32 || !conflict_found {
|
||||
break;
|
||||
}
|
||||
short_hash_len += 1;
|
||||
}
|
||||
entries.push_str(&format!("{} {}\n", hash, id));
|
||||
}
|
||||
|
||||
self.short_hash_len = short_hash_len;
|
||||
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&self.path)
|
||||
.context("cannot open maildir id hash map cache")?
|
||||
.write(format!("{}\n{}", short_hash_len, entries).as_bytes())
|
||||
.context("cannot write maildir id hash map cache")?;
|
||||
|
||||
Ok(short_hash_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IdMapper {
|
||||
type Target = HashMap<String, String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for IdMapper {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
|
@ -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 mailbox")
|
||||
.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,415 +0,0 @@
|
|||
//! IMAP backend module.
|
||||
//!
|
||||
//! This module contains the definition of the IMAP backend.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, log_enabled, trace, Level};
|
||||
use native_tls::{TlsConnector, TlsStream};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::{TryFrom, TryInto},
|
||||
net::TcpStream,
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backends::{
|
||||
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
|
||||
},
|
||||
config::{AccountConfig, ImapBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
output::run_cmd,
|
||||
};
|
||||
|
||||
use super::ImapFlags;
|
||||
|
||||
type ImapSess = imap::Session<TlsStream<TcpStream>>;
|
||||
|
||||
pub struct ImapBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
imap_config: &'a ImapBackendConfig,
|
||||
sess: Option<ImapSess>,
|
||||
}
|
||||
|
||||
impl<'a> ImapBackend<'a> {
|
||||
pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self {
|
||||
Self {
|
||||
account_config,
|
||||
imap_config,
|
||||
sess: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sess(&mut self) -> Result<&mut ImapSess> {
|
||||
if self.sess.is_none() {
|
||||
debug!("create TLS builder");
|
||||
debug!("insecure: {}", self.imap_config.imap_insecure);
|
||||
let builder = TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(self.imap_config.imap_insecure)
|
||||
.danger_accept_invalid_hostnames(self.imap_config.imap_insecure)
|
||||
.build()
|
||||
.context("cannot create TLS connector")?;
|
||||
|
||||
debug!("create client");
|
||||
debug!("host: {}", self.imap_config.imap_host);
|
||||
debug!("port: {}", self.imap_config.imap_port);
|
||||
debug!("starttls: {}", self.imap_config.imap_starttls);
|
||||
let mut client_builder =
|
||||
imap::ClientBuilder::new(&self.imap_config.imap_host, self.imap_config.imap_port);
|
||||
if self.imap_config.imap_starttls {
|
||||
client_builder.starttls();
|
||||
}
|
||||
let client = client_builder
|
||||
.connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?))
|
||||
.context("cannot connect to IMAP server")?;
|
||||
|
||||
debug!("create session");
|
||||
debug!("login: {}", self.imap_config.imap_login);
|
||||
debug!("passwd cmd: {}", self.imap_config.imap_passwd_cmd);
|
||||
let mut sess = client
|
||||
.login(
|
||||
&self.imap_config.imap_login,
|
||||
&self.imap_config.imap_passwd()?,
|
||||
)
|
||||
.map_err(|res| res.0)
|
||||
.context("cannot login to IMAP server")?;
|
||||
sess.debug = log_enabled!(Level::Trace);
|
||||
self.sess = Some(sess);
|
||||
}
|
||||
|
||||
match self.sess {
|
||||
Some(ref mut sess) => Ok(sess),
|
||||
None => Err(anyhow!("cannot get IMAP session")),
|
||||
}
|
||||
}
|
||||
|
||||
fn search_new_msgs(&mut self, query: &str) -> Result<Vec<u32>> {
|
||||
let uids: Vec<u32> = self
|
||||
.sess()?
|
||||
.uid_search(query)
|
||||
.context("cannot search new messages")?
|
||||
.into_iter()
|
||||
.collect();
|
||||
debug!("found {} new messages", uids.len());
|
||||
trace!("uids: {:?}", uids);
|
||||
|
||||
Ok(uids)
|
||||
}
|
||||
|
||||
pub fn notify(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
|
||||
debug!("notify");
|
||||
|
||||
debug!("examine mailbox {:?}", mbox);
|
||||
self.sess()?
|
||||
.examine(mbox)
|
||||
.context(format!("cannot examine mailbox {}", mbox))?;
|
||||
|
||||
debug!("init messages hashset");
|
||||
let mut msgs_set: HashSet<u32> = self
|
||||
.search_new_msgs(&self.account_config.notify_query)?
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
trace!("messages hashset: {:?}", msgs_set);
|
||||
|
||||
loop {
|
||||
debug!("begin loop");
|
||||
self.sess()?
|
||||
.idle()
|
||||
.and_then(|mut idle| {
|
||||
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
|
||||
idle.wait_keepalive_while(|res| {
|
||||
// TODO: handle response
|
||||
trace!("idle response: {:?}", res);
|
||||
false
|
||||
})
|
||||
})
|
||||
.context("cannot start the idle mode")?;
|
||||
|
||||
let uids: Vec<u32> = self
|
||||
.search_new_msgs(&self.account_config.notify_query)?
|
||||
.into_iter()
|
||||
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
|
||||
.collect();
|
||||
debug!("found {} new messages not in hashset", uids.len());
|
||||
trace!("messages hashet: {:?}", msgs_set);
|
||||
|
||||
if !uids.is_empty() {
|
||||
let uids = uids
|
||||
.iter()
|
||||
.map(|uid| uid.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.uid_fetch(uids, "(UID ENVELOPE)")
|
||||
.context("cannot fetch new messages enveloppe")?;
|
||||
|
||||
for fetch in fetches.iter() {
|
||||
let msg = ImapEnvelope::try_from(fetch)?;
|
||||
let uid = fetch.uid.ok_or_else(|| {
|
||||
anyhow!("cannot retrieve message {}'s UID", fetch.message)
|
||||
})?;
|
||||
|
||||
let from = msg.sender.to_owned().into();
|
||||
self.account_config.run_notify_cmd(&msg.subject, &from)?;
|
||||
|
||||
debug!("notify message: {}", uid);
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
debug!("insert message {} in hashset", uid);
|
||||
msgs_set.insert(uid);
|
||||
trace!("messages hashset: {:?}", msgs_set);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("end loop");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
|
||||
debug!("examine mailbox: {}", mbox);
|
||||
|
||||
self.sess()?
|
||||
.examine(mbox)
|
||||
.context(format!("cannot examine mailbox `{}`", mbox))?;
|
||||
|
||||
loop {
|
||||
debug!("begin loop");
|
||||
self.sess()?
|
||||
.idle()
|
||||
.and_then(|mut idle| {
|
||||
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
|
||||
idle.wait_keepalive_while(|res| {
|
||||
// TODO: handle response
|
||||
trace!("idle response: {:?}", res);
|
||||
false
|
||||
})
|
||||
})
|
||||
.context("cannot start the idle mode")?;
|
||||
|
||||
let cmds = self.account_config.watch_cmds.clone();
|
||||
thread::spawn(move || {
|
||||
debug!("batch execution of {} cmd(s)", cmds.len());
|
||||
cmds.iter().for_each(|cmd| {
|
||||
debug!("running command {:?}…", cmd);
|
||||
let res = run_cmd(cmd);
|
||||
debug!("{:?}", res);
|
||||
})
|
||||
});
|
||||
|
||||
debug!("end loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Backend<'a> for ImapBackend<'a> {
|
||||
fn add_mbox(&mut self, mbox: &str) -> Result<()> {
|
||||
self.sess()?
|
||||
.create(mbox)
|
||||
.context(format!("cannot create imap mailbox {:?}", mbox))
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
let mboxes: ImapMboxes = self
|
||||
.sess()?
|
||||
.list(Some(""), Some("*"))
|
||||
.context("cannot list mailboxes")?
|
||||
.into();
|
||||
Ok(Box::new(mboxes))
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, mbox: &str) -> Result<()> {
|
||||
self.sess()?
|
||||
.delete(mbox)
|
||||
.context(format!("cannot delete imap mailbox {:?}", mbox))
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.exists as usize;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
if last_seq == 0 {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
}
|
||||
|
||||
let range = if page_size > 0 {
|
||||
let cursor = page * page_size;
|
||||
let begin = 1.max(last_seq - cursor);
|
||||
let end = begin - begin.min(page_size) + 1;
|
||||
format!("{}:{}", end, begin)
|
||||
} else {
|
||||
String::from("1:*")
|
||||
};
|
||||
debug!("range: {:?}", range);
|
||||
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(format!("cannot fetch messages within range {:?}", range))?;
|
||||
let envelopes: ImapEnvelopes = fetches.try_into()?;
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.exists;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
if last_seq == 0 {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
}
|
||||
|
||||
let begin = page * page_size;
|
||||
let end = begin + (page_size - 1);
|
||||
let seqs: Vec<String> = if sort.is_empty() {
|
||||
self.sess()?
|
||||
.search(query)
|
||||
.context(format!(
|
||||
"cannot find envelopes in {:?} with query {:?}",
|
||||
mbox, query
|
||||
))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect()
|
||||
} else {
|
||||
let sort: SortCriteria = sort.try_into()?;
|
||||
let charset = imap::extensions::sort::SortCharset::Utf8;
|
||||
self.sess()?
|
||||
.sort(&sort, charset, query)
|
||||
.context(format!(
|
||||
"cannot find envelopes in {:?} with query {:?}",
|
||||
mbox, query
|
||||
))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect()
|
||||
};
|
||||
if seqs.is_empty() {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
}
|
||||
|
||||
let range = seqs[begin..end.min(seqs.len())].join(",");
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(format!("cannot fetch messages within range {:?}", range))?;
|
||||
let envelopes: ImapEnvelopes = fetches.try_into()?;
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
self.sess()?
|
||||
.append(mbox, msg)
|
||||
.flags(<ImapFlags as Into<Vec<imap::types::Flag<'a>>>>::into(flags))
|
||||
.finish()
|
||||
.context(format!("cannot append message to {:?}", mbox))?;
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.exists;
|
||||
Ok(Box::new(last_seq))
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, mbox: &str, seq: &str) -> Result<Msg> {
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(seq, "(FLAGS INTERNALDATE BODY[])")
|
||||
.context(format!("cannot fetch messages {:?}", seq))?;
|
||||
let fetch = fetches
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find message {:?}", seq))?;
|
||||
let msg_raw = fetch.body().unwrap_or_default().to_owned();
|
||||
let mut msg = Msg::from_parsed_mail(
|
||||
mailparse::parse_mail(&msg_raw).context("cannot parse message")?,
|
||||
self.account_config,
|
||||
)?;
|
||||
msg.raw = msg_raw;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
|
||||
let msg = self.get_msg(&mbox_src, seq)?.raw;
|
||||
println!("raw: {:?}", String::from_utf8(msg.to_vec()).unwrap());
|
||||
self.add_msg(&mbox_dst, &msg, "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
|
||||
let msg = self.get_msg(mbox_src, seq)?.raw;
|
||||
self.add_flags(mbox_src, seq, "seen deleted")?;
|
||||
self.add_msg(&mbox_dst, &msg, "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_msg(&mut self, mbox: &str, seq: &str) -> Result<()> {
|
||||
self.add_flags(mbox, seq, "deleted")
|
||||
}
|
||||
|
||||
fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
self.sess()?
|
||||
.store(seq_range, format!("+FLAGS ({})", flags))
|
||||
.context(format!("cannot add flags {:?}", &flags))?;
|
||||
self.sess()?
|
||||
.expunge()
|
||||
.context(format!("cannot expunge mailbox {:?}", mbox))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
self.sess()?
|
||||
.store(seq_range, format!("FLAGS ({})", flags))
|
||||
.context(format!("cannot set flags {:?}", &flags))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
self.sess()?
|
||||
.store(seq_range, format!("-FLAGS ({})", flags))
|
||||
.context(format!("cannot remove flags {:?}", &flags))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> Result<()> {
|
||||
if let Some(ref mut sess) = self.sess {
|
||||
debug!("logout from IMAP server");
|
||||
sess.logout().context("cannot logout from IMAP server")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
//! IMAP envelope module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the envelope.
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use std::{convert::TryFrom, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use super::{ImapFlag, ImapFlags};
|
||||
|
||||
/// Represents a list of IMAP envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct ImapEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<ImapEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for ImapEnvelopes {
|
||||
type Target = Vec<ImapEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for ImapEnvelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// impl Envelopes for ImapEnvelopes {
|
||||
// //
|
||||
// }
|
||||
|
||||
/// Represents the IMAP envelope. The envelope is just a message
|
||||
/// subset, and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct ImapEnvelope {
|
||||
/// Represents the sequence number of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
||||
pub id: u32,
|
||||
|
||||
/// Represents the flags attached to the message.
|
||||
pub flags: ImapFlags,
|
||||
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
|
||||
/// Represents the internal date of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
|
||||
pub date: Option<String>,
|
||||
}
|
||||
|
||||
impl Table for ImapEnvelope {
|
||||
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(&ImapFlag::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())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw envelopes returned by the `imap` crate.
|
||||
pub type RawImapEnvelopes = imap::types::ZeroCopy<Vec<RawImapEnvelope>>;
|
||||
|
||||
impl TryFrom<RawImapEnvelopes> for ImapEnvelopes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_envelopes: RawImapEnvelopes) -> Result<Self, Self::Error> {
|
||||
let mut envelopes = vec![];
|
||||
for raw_envelope in raw_envelopes.iter().rev() {
|
||||
envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?);
|
||||
}
|
||||
Ok(Self { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw envelope returned by the `imap` crate.
|
||||
pub type RawImapEnvelope = imap::types::Fetch;
|
||||
|
||||
impl TryFrom<&RawImapEnvelope> for ImapEnvelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fetch: &RawImapEnvelope) -> Result<ImapEnvelope> {
|
||||
let envelope = fetch
|
||||
.envelope()
|
||||
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
|
||||
|
||||
// Get the sequence number
|
||||
let id = fetch.message;
|
||||
|
||||
// Get the flags
|
||||
let flags = ImapFlags::try_from(fetch.flags())?;
|
||||
|
||||
// Get the subject
|
||||
let subject = envelope
|
||||
.subject
|
||||
.as_ref()
|
||||
.map(|subj| {
|
||||
rfc2047_decoder::decode(subj).context(format!(
|
||||
"cannot decode subject of message {}",
|
||||
fetch.message
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| Ok(String::default()))?;
|
||||
|
||||
// Get the sender
|
||||
let sender = envelope
|
||||
.sender
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.get(0))
|
||||
.or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0)))
|
||||
.ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?;
|
||||
let sender = if let Some(ref name) = sender.name {
|
||||
rfc2047_decoder::decode(&name.to_vec()).context(format!(
|
||||
"cannot decode sender's name of message {}",
|
||||
fetch.message,
|
||||
))?
|
||||
} else {
|
||||
let mbox = sender
|
||||
.mailbox
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message))
|
||||
.and_then(|mbox| {
|
||||
rfc2047_decoder::decode(&mbox.to_vec()).context(format!(
|
||||
"cannot decode sender's mailbox of message {}",
|
||||
fetch.message,
|
||||
))
|
||||
})?;
|
||||
let host = sender
|
||||
.host
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message))
|
||||
.and_then(|host| {
|
||||
rfc2047_decoder::decode(&host.to_vec()).context(format!(
|
||||
"cannot decode sender's host of message {}",
|
||||
fetch.message,
|
||||
))
|
||||
})?;
|
||||
format!("{}@{}", mbox, host)
|
||||
};
|
||||
|
||||
// Get the internal date
|
||||
let date = fetch
|
||||
.internal_date()
|
||||
.map(|date| date.naive_local().to_string());
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
flags,
|
||||
subject,
|
||||
sender,
|
||||
date,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the imap flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum ImapFlag {
|
||||
Seen,
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Draft,
|
||||
Recent,
|
||||
MayCreate,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&str> for ImapFlag {
|
||||
fn from(flag_str: &str) -> Self {
|
||||
match flag_str {
|
||||
"seen" => ImapFlag::Seen,
|
||||
"answered" => ImapFlag::Answered,
|
||||
"flagged" => ImapFlag::Flagged,
|
||||
"deleted" => ImapFlag::Deleted,
|
||||
"draft" => ImapFlag::Draft,
|
||||
"recent" => ImapFlag::Recent,
|
||||
"maycreate" | "may-create" => ImapFlag::MayCreate,
|
||||
flag_str => ImapFlag::Custom(flag_str.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&imap::types::Flag<'_>> for ImapFlag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flag: &imap::types::Flag<'_>) -> Result<Self, Self::Error> {
|
||||
Ok(match flag {
|
||||
imap::types::Flag::Seen => ImapFlag::Seen,
|
||||
imap::types::Flag::Answered => ImapFlag::Answered,
|
||||
imap::types::Flag::Flagged => ImapFlag::Flagged,
|
||||
imap::types::Flag::Deleted => ImapFlag::Deleted,
|
||||
imap::types::Flag::Draft => ImapFlag::Draft,
|
||||
imap::types::Flag::Recent => ImapFlag::Recent,
|
||||
imap::types::Flag::MayCreate => ImapFlag::MayCreate,
|
||||
imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()),
|
||||
_ => return Err(anyhow!("cannot parse imap flag")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the imap flags.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct ImapFlags(pub Vec<ImapFlag>);
|
||||
|
||||
impl ImapFlags {
|
||||
/// Builds a symbols string
|
||||
pub fn to_symbols_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if self.contains(&ImapFlag::Seen) {
|
||||
" "
|
||||
} else {
|
||||
"✷"
|
||||
});
|
||||
flags.push_str(if self.contains(&ImapFlag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&ImapFlag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ImapFlags {
|
||||
type Target = Vec<ImapFlag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ImapFlags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut glue = "";
|
||||
|
||||
for flag in &self.0 {
|
||||
write!(f, "{}", glue)?;
|
||||
match flag {
|
||||
ImapFlag::Seen => write!(f, "\\Seen")?,
|
||||
ImapFlag::Answered => write!(f, "\\Answered")?,
|
||||
ImapFlag::Flagged => write!(f, "\\Flagged")?,
|
||||
ImapFlag::Deleted => write!(f, "\\Deleted")?,
|
||||
ImapFlag::Draft => write!(f, "\\Draft")?,
|
||||
ImapFlag::Recent => write!(f, "\\Recent")?,
|
||||
ImapFlag::MayCreate => write!(f, "\\MayCreate")?,
|
||||
ImapFlag::Custom(custom) => write!(f, "{}", custom)?,
|
||||
}
|
||||
glue = " ";
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<Vec<imap::types::Flag<'a>>> for ImapFlags {
|
||||
fn into(self) -> Vec<imap::types::Flag<'a>> {
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|flag| match flag {
|
||||
ImapFlag::Seen => imap::types::Flag::Seen,
|
||||
ImapFlag::Answered => imap::types::Flag::Answered,
|
||||
ImapFlag::Flagged => imap::types::Flag::Flagged,
|
||||
ImapFlag::Deleted => imap::types::Flag::Deleted,
|
||||
ImapFlag::Draft => imap::types::Flag::Draft,
|
||||
ImapFlag::Recent => imap::types::Flag::Recent,
|
||||
ImapFlag::MayCreate => imap::types::Flag::MayCreate,
|
||||
ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ImapFlags {
|
||||
fn from(flags_str: &str) -> Self {
|
||||
ImapFlags(
|
||||
flags_str
|
||||
.split_whitespace()
|
||||
.map(|flag_str| flag_str.trim().into())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flags: &[imap::types::Flag<'_>]) -> Result<Self, Self::Error> {
|
||||
let mut f = vec![];
|
||||
for flag in flags {
|
||||
f.push(flag.try_into()?);
|
||||
}
|
||||
Ok(Self(f))
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//! Module related to IMAP handling.
|
||||
//!
|
||||
//! This module gathers all IMAP handlers triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::backends::ImapBackend;
|
||||
|
||||
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.notify(keepalive, mbox)
|
||||
}
|
||||
|
||||
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.watch(keepalive, mbox)
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
//! IMAP mailbox module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the mailbox.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::mbox::Mboxes;
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use super::ImapMboxAttrs;
|
||||
|
||||
/// Represents a list of IMAP mailboxes.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ImapMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<ImapMbox>,
|
||||
}
|
||||
|
||||
impl Deref for ImapMboxes {
|
||||
type Target = Vec<ImapMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for ImapMboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for ImapMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the IMAP mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct ImapMbox {
|
||||
/// Represents the mailbox hierarchie delimiter.
|
||||
pub delim: String,
|
||||
|
||||
/// Represents the mailbox name.
|
||||
pub name: String,
|
||||
|
||||
/// Represents the mailbox attributes.
|
||||
pub attrs: ImapMboxAttrs,
|
||||
}
|
||||
|
||||
impl ImapMbox {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImapMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for ImapMbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("DELIM").bold().underline().white())
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(
|
||||
Cell::new("ATTRIBUTES")
|
||||
.shrinkable()
|
||||
.bold()
|
||||
.underline()
|
||||
.white(),
|
||||
)
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.delim).white())
|
||||
.cell(Cell::new(&self.name).green())
|
||||
.cell(Cell::new(&self.attrs.to_string()).shrinkable().blue())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backends::ImapMboxAttr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_create_new_mbox() {
|
||||
assert_eq!(ImapMbox::default(), ImapMbox::new(""));
|
||||
assert_eq!(
|
||||
ImapMbox {
|
||||
name: "INBOX".into(),
|
||||
..ImapMbox::default()
|
||||
},
|
||||
ImapMbox::new("INBOX")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_display_mbox() {
|
||||
let default_mbox = ImapMbox::default();
|
||||
assert_eq!("", default_mbox.to_string());
|
||||
|
||||
let new_mbox = ImapMbox::new("INBOX");
|
||||
assert_eq!("INBOX", new_mbox.to_string());
|
||||
|
||||
let full_mbox = ImapMbox {
|
||||
delim: ".".into(),
|
||||
name: "Sent".into(),
|
||||
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||
};
|
||||
assert_eq!("Sent", full_mbox.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw mailboxes returned by the `imap` crate.
|
||||
pub type RawImapMboxes = imap::types::ZeroCopy<Vec<RawImapMbox>>;
|
||||
|
||||
impl<'a> From<RawImapMboxes> for ImapMboxes {
|
||||
fn from(raw_mboxes: RawImapMboxes) -> Self {
|
||||
Self {
|
||||
mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw mailbox returned by the `imap` crate.
|
||||
pub type RawImapMbox = imap::types::Name;
|
||||
|
||||
impl<'a> From<&'a RawImapMbox> for ImapMbox {
|
||||
fn from(raw_mbox: &'a RawImapMbox) -> Self {
|
||||
Self {
|
||||
delim: raw_mbox.delimiter().unwrap_or_default().into(),
|
||||
name: raw_mbox.name().into(),
|
||||
attrs: raw_mbox.attributes().into(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
//! IMAP mailbox attribute module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the mailbox attribute.
|
||||
|
||||
/// Represents the raw mailbox attribute returned by the `imap` crate.
|
||||
pub use imap::types::NameAttribute as RawImapMboxAttr;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the attributes of the mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct ImapMboxAttrs(pub Vec<ImapMboxAttr>);
|
||||
|
||||
impl Deref for ImapMboxAttrs {
|
||||
type Target = Vec<ImapMboxAttr>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImapMboxAttrs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut glue = "";
|
||||
for attr in self.iter() {
|
||||
write!(f, "{}{}", glue, attr)?;
|
||||
glue = ", ";
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
|
||||
pub enum ImapMboxAttr {
|
||||
NoInferiors,
|
||||
NoSelect,
|
||||
Marked,
|
||||
Unmarked,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// Makes the attribute displayable.
|
||||
impl Display for ImapMboxAttr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"),
|
||||
ImapMboxAttr::NoSelect => write!(f, "NoSelect"),
|
||||
ImapMboxAttr::Marked => write!(f, "Marked"),
|
||||
ImapMboxAttr::Unmarked => write!(f, "Unmarked"),
|
||||
ImapMboxAttr::Custom(custom) => write!(f, "{}", custom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_display_attrs() {
|
||||
macro_rules! attrs_from {
|
||||
($($attr:expr),*) => {
|
||||
ImapMboxAttrs(vec![$($attr,)*]).to_string()
|
||||
};
|
||||
}
|
||||
|
||||
let empty_attr = attrs_from![];
|
||||
let single_attr = attrs_from![ImapMboxAttr::NoInferiors];
|
||||
let multiple_attrs = attrs_from![
|
||||
ImapMboxAttr::Custom("AttrCustom".into()),
|
||||
ImapMboxAttr::NoInferiors
|
||||
];
|
||||
|
||||
assert_eq!("", empty_attr);
|
||||
assert_eq!("NoInferiors", single_attr);
|
||||
assert!(multiple_attrs.contains("NoInferiors"));
|
||||
assert!(multiple_attrs.contains("AttrCustom"));
|
||||
assert!(multiple_attrs.contains(","));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_display_attr() {
|
||||
macro_rules! attr_from {
|
||||
($attr:ident) => {
|
||||
ImapMboxAttr::$attr.to_string()
|
||||
};
|
||||
($custom:literal) => {
|
||||
ImapMboxAttr::Custom($custom.into()).to_string()
|
||||
};
|
||||
}
|
||||
|
||||
assert_eq!("NoInferiors", attr_from![NoInferiors]);
|
||||
assert_eq!("NoSelect", attr_from![NoSelect]);
|
||||
assert_eq!("Marked", attr_from![Marked]);
|
||||
assert_eq!("Unmarked", attr_from![Unmarked]);
|
||||
assert_eq!("CustomAttr", attr_from!["CustomAttr"]);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs {
|
||||
fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self {
|
||||
Self(raw_attrs.iter().map(ImapMboxAttr::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr {
|
||||
fn from(attr: &'a RawImapMboxAttr<'a>) -> Self {
|
||||
match attr {
|
||||
RawImapMboxAttr::NoInferiors => Self::NoInferiors,
|
||||
RawImapMboxAttr::NoSelect => Self::NoSelect,
|
||||
RawImapMboxAttr::Marked => Self::Marked,
|
||||
RawImapMboxAttr::Unmarked => Self::Unmarked,
|
||||
RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
//! Message sort criteria module.
|
||||
//!
|
||||
//! This module regroups everything related to deserialization of
|
||||
//! message sort criteria.
|
||||
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{convert::TryFrom, ops::Deref};
|
||||
|
||||
/// Represents the message sort criteria. It is just a wrapper around
|
||||
/// the `imap::extensions::sort::SortCriterion`.
|
||||
pub struct SortCriteria<'a>(Vec<imap::extensions::sort::SortCriterion<'a>>);
|
||||
|
||||
impl<'a> Deref for SortCriteria<'a> {
|
||||
type Target = Vec<imap::extensions::sort::SortCriterion<'a>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for SortCriteria<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(criteria_str: &'a str) -> Result<Self, Self::Error> {
|
||||
let mut criteria = vec![];
|
||||
for criterion_str in criteria_str.split(" ") {
|
||||
criteria.push(match criterion_str.trim() {
|
||||
"arrival:asc" | "arrival" => Ok(imap::extensions::sort::SortCriterion::Arrival),
|
||||
"arrival:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::Arrival,
|
||||
)),
|
||||
"cc:asc" | "cc" => Ok(imap::extensions::sort::SortCriterion::Cc),
|
||||
"cc:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::Cc,
|
||||
)),
|
||||
"date:asc" | "date" => Ok(imap::extensions::sort::SortCriterion::Date),
|
||||
"date:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::Date,
|
||||
)),
|
||||
"from:asc" | "from" => Ok(imap::extensions::sort::SortCriterion::From),
|
||||
"from:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::From,
|
||||
)),
|
||||
"size:asc" | "size" => Ok(imap::extensions::sort::SortCriterion::Size),
|
||||
"size:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::Size,
|
||||
)),
|
||||
"subject:asc" | "subject" => Ok(imap::extensions::sort::SortCriterion::Subject),
|
||||
"subject:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::Subject,
|
||||
)),
|
||||
"to:asc" | "to" => Ok(imap::extensions::sort::SortCriterion::To),
|
||||
"to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::To,
|
||||
)),
|
||||
_ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)),
|
||||
}?);
|
||||
}
|
||||
Ok(Self(criteria))
|
||||
}
|
||||
}
|
|
@ -1,493 +0,0 @@
|
|||
//! Maildir backend module.
|
||||
//!
|
||||
//! This module contains the definition of the maildir backend and its
|
||||
//! traits implementation.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
use std::{convert::TryInto, env, fs, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
|
||||
config::{AccountConfig, MaildirBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
/// Represents the maildir backend.
|
||||
pub struct MaildirBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
mdir: maildir::Maildir,
|
||||
}
|
||||
|
||||
impl<'a> MaildirBackend<'a> {
|
||||
pub fn new(
|
||||
account_config: &'a AccountConfig,
|
||||
maildir_config: &'a MaildirBackendConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
account_config,
|
||||
mdir: maildir_config.maildir_dir.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result<PathBuf> {
|
||||
if mdir_path.is_dir() {
|
||||
Ok(mdir_path)
|
||||
} else {
|
||||
Err(anyhow!("cannot read maildir directory {:?}", mdir_path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a maildir instance from a string slice.
|
||||
pub fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> {
|
||||
let dir = self.account_config.get_mbox_alias(dir)?;
|
||||
|
||||
// If the dir points to the inbox folder, creates a maildir
|
||||
// instance from the root folder.
|
||||
if &dir == "inbox" {
|
||||
return self
|
||||
.validate_mdir_path(self.mdir.path().to_owned())
|
||||
.map(maildir::Maildir::from);
|
||||
}
|
||||
|
||||
// If the dir is a valid maildir path, creates a maildir
|
||||
// instance from it. First checks for absolute path,
|
||||
self.validate_mdir_path((&dir).into())
|
||||
// then for relative path to `maildir-dir`,
|
||||
.or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir)))
|
||||
// and finally for relative path to the current directory.
|
||||
.or_else(|_| self.validate_mdir_path(env::current_dir()?.join(&dir)))
|
||||
.or_else(|_| {
|
||||
// Otherwise creates a maildir instance from a maildir
|
||||
// subdirectory by adding a "." in front of the name
|
||||
// as described in the [spec].
|
||||
//
|
||||
// [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
|
||||
self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir)))
|
||||
})
|
||||
.map(maildir::Maildir::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Backend<'a> for MaildirBackend<'a> {
|
||||
fn add_mbox(&mut self, subdir: &str) -> Result<()> {
|
||||
info!(">> add maildir subdir");
|
||||
debug!("subdir: {:?}", subdir);
|
||||
|
||||
let path = self.mdir.path().join(format!(".{}", subdir));
|
||||
trace!("subdir path: {:?}", path);
|
||||
|
||||
fs::create_dir(&path)
|
||||
.with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?;
|
||||
|
||||
info!("<< add maildir subdir");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
info!(">> get maildir dirs");
|
||||
|
||||
let dirs: MaildirMboxes =
|
||||
self.mdir.list_subdirs().try_into().with_context(|| {
|
||||
format!("cannot parse maildir dirs from {:?}", self.mdir.path())
|
||||
})?;
|
||||
trace!("dirs: {:?}", dirs);
|
||||
|
||||
info!("<< get maildir dirs");
|
||||
Ok(Box::new(dirs))
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, dir: &str) -> Result<()> {
|
||||
info!(">> delete maildir dir");
|
||||
debug!("dir: {:?}", dir);
|
||||
|
||||
let path = self.mdir.path().join(format!(".{}", dir));
|
||||
trace!("dir path: {:?}", path);
|
||||
|
||||
fs::remove_dir_all(&path)
|
||||
.with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?;
|
||||
|
||||
info!("<< delete maildir dir");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
dir: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> get maildir envelopes");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
|
||||
// Reads envelopes from the "cur" folder of the selected
|
||||
// maildir.
|
||||
let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| {
|
||||
format!("cannot parse maildir envelopes from {:?}", self.mdir.path())
|
||||
})?;
|
||||
debug!("envelopes len: {:?}", envelopes.len());
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
// Calculates pagination boundaries.
|
||||
let page_begin = page * page_size;
|
||||
debug!("page begin: {:?}", page_begin);
|
||||
if page_begin > envelopes.len() {
|
||||
return Err(anyhow!(
|
||||
"cannot get maildir envelopes at page {:?} (out of bounds)",
|
||||
page_begin + 1,
|
||||
));
|
||||
}
|
||||
let page_end = envelopes.len().min(page_begin + page_size);
|
||||
debug!("page end: {:?}", page_end);
|
||||
|
||||
// Sorts envelopes by most recent date.
|
||||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
|
||||
// Applies pagination boundaries.
|
||||
envelopes.envelopes = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
// represents the minimum hash length possible to avoid
|
||||
// conflicts.
|
||||
let short_hash_len = {
|
||||
let mut mapper = IdMapper::new(mdir.path())?;
|
||||
let entries = envelopes
|
||||
.iter()
|
||||
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
|
||||
.collect();
|
||||
mapper.append(entries)?
|
||||
};
|
||||
debug!("short hash length: {:?}", short_hash_len);
|
||||
|
||||
// Shorten envelopes hash.
|
||||
envelopes
|
||||
.iter_mut()
|
||||
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
|
||||
|
||||
info!("<< get maildir envelopes");
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
_dir: &str,
|
||||
_query: &str,
|
||||
_sort: &str,
|
||||
_page_size: usize,
|
||||
_page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> search maildir envelopes");
|
||||
info!("<< search maildir envelopes");
|
||||
Err(anyhow!(
|
||||
"cannot find maildir envelopes: feature not implemented"
|
||||
))
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
|
||||
info!(">> add maildir message");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
let id = mdir
|
||||
.store_cur_with_flags(msg, &flags.to_string())
|
||||
.with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?;
|
||||
debug!("id: {:?}", id);
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
debug!("hash: {:?}", hash);
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
let mut mapper = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?;
|
||||
mapper
|
||||
.append(vec![(hash.clone(), id.clone())])
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot append hash {:?} with id {:?} to id mapper",
|
||||
hash, id
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< add maildir message");
|
||||
Ok(Box::new(hash))
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result<Msg> {
|
||||
info!(">> get maildir message");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let id = IdMapper::new(mdir.path())?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let mut mail_entry = mdir.find(&id).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"cannot find maildir message by id {:?} at {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
let parsed_mail = mail_entry.parsed().with_context(|| {
|
||||
format!("cannot parse maildir message {:?} at {:?}", id, mdir.path())
|
||||
})?;
|
||||
let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).with_context(|| {
|
||||
format!("cannot parse maildir message {:?} at {:?}", id, mdir.path())
|
||||
})?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< get maildir message");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> copy maildir message");
|
||||
debug!("source dir: {:?}", dir_src);
|
||||
debug!("destination dir: {:?}", dir_dst);
|
||||
|
||||
let mdir_src = self
|
||||
.get_mdir_from_dir(dir_src)
|
||||
.with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?;
|
||||
let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| {
|
||||
format!("cannot get destination maildir instance from {:?}", dir_dst)
|
||||
})?;
|
||||
let id = IdMapper::new(mdir_src.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir_src.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
|
||||
mdir_src.copy_to(&id, &mdir_dst).with_context(|| {
|
||||
format!(
|
||||
"cannot copy message {:?} from maildir {:?} to maildir {:?}",
|
||||
id,
|
||||
mdir_src.path(),
|
||||
mdir_dst.path()
|
||||
)
|
||||
})?;
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| {
|
||||
format!("cannot create id mapper instance for {:?}", mdir_dst.path())
|
||||
})?;
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
mapper
|
||||
.append(vec![(hash.clone(), id.clone())])
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot append hash {:?} with id {:?} to id mapper",
|
||||
hash, id
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< copy maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> move maildir message");
|
||||
debug!("source dir: {:?}", dir_src);
|
||||
debug!("destination dir: {:?}", dir_dst);
|
||||
|
||||
let mdir_src = self
|
||||
.get_mdir_from_dir(dir_src)
|
||||
.with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?;
|
||||
let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| {
|
||||
format!("cannot get destination maildir instance from {:?}", dir_dst)
|
||||
})?;
|
||||
let id = IdMapper::new(mdir_src.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir_src.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
|
||||
mdir_src.move_to(&id, &mdir_dst).with_context(|| {
|
||||
format!(
|
||||
"cannot move message {:?} from maildir {:?} to maildir {:?}",
|
||||
id,
|
||||
mdir_src.path(),
|
||||
mdir_dst.path()
|
||||
)
|
||||
})?;
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| {
|
||||
format!("cannot create id mapper instance for {:?}", mdir_dst.path())
|
||||
})?;
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
mapper
|
||||
.append(vec![(hash.clone(), id.clone())])
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot append hash {:?} with id {:?} to id mapper",
|
||||
hash, id
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< move maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> delete maildir message");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.delete(&id).with_context(|| {
|
||||
format!(
|
||||
"cannot delete message {:?} from maildir {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< delete maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
|
||||
info!(">> add maildir message flags");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
debug!("flags: {:?}", flags);
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.add_flags(&id, &flags.to_string())
|
||||
.with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?;
|
||||
|
||||
info!("<< add maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
|
||||
info!(">> set maildir message flags");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
debug!("flags: {:?}", flags);
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.set_flags(&id, &flags.to_string())
|
||||
.with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?;
|
||||
|
||||
info!("<< set maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
|
||||
info!(">> delete maildir message flags");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
debug!("flags: {:?}", flags);
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.remove_flags(&id, &flags.to_string())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot delete flags {:?} to maildir message {:?}",
|
||||
flags, id
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< delete maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
//! Maildir mailbox module.
|
||||
//!
|
||||
//! This module provides Maildir types and conversion utilities
|
||||
//! related to the envelope
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::DateTime;
|
||||
use log::trace;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backends::{MaildirFlag, MaildirFlags},
|
||||
msg::{from_slice_to_addrs, Addr},
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct MaildirEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<MaildirEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for MaildirEnvelopes {
|
||||
type Target = Vec<MaildirEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for MaildirEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for MaildirEnvelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// impl Envelopes for MaildirEnvelopes {
|
||||
// //
|
||||
// }
|
||||
|
||||
/// Represents the envelope. The envelope is just a message subset,
|
||||
/// and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct MaildirEnvelope {
|
||||
/// Represents the id of the message.
|
||||
pub id: String,
|
||||
|
||||
/// Represents the MD5 hash of the message id.
|
||||
pub hash: String,
|
||||
|
||||
/// Represents the flags of the message.
|
||||
pub flags: MaildirFlags,
|
||||
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
|
||||
/// Represents the date of the message.
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for MaildirEnvelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("HASH").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 hash = self.hash.clone();
|
||||
let unseen = !self.flags.contains(&MaildirFlag::Seen);
|
||||
let flags = self.flags.to_symbols_string();
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = &self.date;
|
||||
Row::new()
|
||||
.cell(Cell::new(hash).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw envelopees returned by the `maildir` crate.
|
||||
pub type RawMaildirEnvelopes = maildir::MailEntries;
|
||||
|
||||
impl<'a> TryFrom<RawMaildirEnvelopes> for MaildirEnvelopes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mail_entries: RawMaildirEnvelopes) -> Result<Self, Self::Error> {
|
||||
let mut envelopes = vec![];
|
||||
for entry in mail_entries {
|
||||
let envelope: MaildirEnvelope = entry
|
||||
.context("cannot decode maildir mail entry")?
|
||||
.try_into()
|
||||
.context("cannot parse maildir mail entry")?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
|
||||
Ok(MaildirEnvelopes { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw envelope returned by the `maildir` crate.
|
||||
pub type RawMaildirEnvelope = maildir::MailEntry;
|
||||
|
||||
impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
|
||||
trace!(">> build envelope from maildir parsed mail");
|
||||
|
||||
let mut envelope = Self::default();
|
||||
|
||||
envelope.id = mail_entry.id().into();
|
||||
envelope.hash = format!("{:x}", md5::compute(&envelope.id));
|
||||
envelope.flags = (&mail_entry)
|
||||
.try_into()
|
||||
.context("cannot parse maildir flags")?;
|
||||
|
||||
let parsed_mail = mail_entry
|
||||
.parsed()
|
||||
.context("cannot parse maildir mail entry")?;
|
||||
|
||||
trace!(">> parse headers");
|
||||
for h in parsed_mail.get_headers() {
|
||||
let k = h.get_key();
|
||||
trace!("header key: {:?}", k);
|
||||
|
||||
let v = rfc2047_decoder::decode(h.get_value_raw())
|
||||
.context(format!("cannot decode value from header {:?}", k))?;
|
||||
trace!("header value: {:?}", v);
|
||||
|
||||
match k.to_lowercase().as_str() {
|
||||
"date" => {
|
||||
envelope.date =
|
||||
DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0)
|
||||
.context(format!("cannot parse maildir message date {:?}", v))?
|
||||
.naive_local()
|
||||
.to_string();
|
||||
}
|
||||
"subject" => {
|
||||
envelope.subject = v.into();
|
||||
}
|
||||
"from" => {
|
||||
envelope.sender = from_slice_to_addrs(v)
|
||||
.context(format!("cannot parse header {:?}", k))?
|
||||
.and_then(|senders| {
|
||||
if senders.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(senders)
|
||||
}
|
||||
})
|
||||
.map(|senders| match &senders[0] {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
|
||||
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
|
||||
}
|
||||
Addr::Group(mailparse::GroupInfo { group_name, .. }) => {
|
||||
group_name.to_owned()
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find sender"))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
trace!("<< parse headers");
|
||||
|
||||
trace!("envelope: {:?}", envelope);
|
||||
trace!("<< build envelope from maildir parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the maildir flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum MaildirFlag {
|
||||
Passed,
|
||||
Replied,
|
||||
Seen,
|
||||
Trashed,
|
||||
Draft,
|
||||
Flagged,
|
||||
Custom(char),
|
||||
}
|
||||
|
||||
/// Represents the maildir flags.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct MaildirFlags(pub Vec<MaildirFlag>);
|
||||
|
||||
impl MaildirFlags {
|
||||
/// Builds a symbols string
|
||||
pub fn to_symbols_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if self.contains(&MaildirFlag::Seen) {
|
||||
" "
|
||||
} else {
|
||||
"✷"
|
||||
});
|
||||
flags.push_str(if self.contains(&MaildirFlag::Replied) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&MaildirFlag::Passed) {
|
||||
"↗"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&MaildirFlag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MaildirFlags {
|
||||
type Target = Vec<MaildirFlag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for MaildirFlags {
|
||||
fn to_string(&self) -> String {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|flag| {
|
||||
let flag_char: char = flag.into();
|
||||
flag_char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for MaildirFlags {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flags_str: &str) -> Result<Self, Self::Error> {
|
||||
let mut flags = vec![];
|
||||
for flag_str in flags_str.split_whitespace() {
|
||||
flags.push(flag_str.trim().try_into()?);
|
||||
}
|
||||
Ok(MaildirFlags(flags))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&maildir::MailEntry> for MaildirFlags {
|
||||
fn from(mail_entry: &maildir::MailEntry) -> Self {
|
||||
let mut flags = vec![];
|
||||
for c in mail_entry.flags().chars() {
|
||||
flags.push(match c {
|
||||
'P' => MaildirFlag::Passed,
|
||||
'R' => MaildirFlag::Replied,
|
||||
'S' => MaildirFlag::Seen,
|
||||
'T' => MaildirFlag::Trashed,
|
||||
'D' => MaildirFlag::Draft,
|
||||
'F' => MaildirFlag::Flagged,
|
||||
custom => MaildirFlag::Custom(custom),
|
||||
})
|
||||
}
|
||||
Self(flags)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<char> for &MaildirFlag {
|
||||
fn into(self) -> char {
|
||||
match self {
|
||||
MaildirFlag::Passed => 'P',
|
||||
MaildirFlag::Replied => 'R',
|
||||
MaildirFlag::Seen => 'S',
|
||||
MaildirFlag::Trashed => 'T',
|
||||
MaildirFlag::Draft => 'D',
|
||||
MaildirFlag::Flagged => 'F',
|
||||
MaildirFlag::Custom(custom) => *custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for MaildirFlag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flag_str: &str) -> Result<Self, Self::Error> {
|
||||
match flag_str {
|
||||
"passed" => Ok(MaildirFlag::Passed),
|
||||
"replied" => Ok(MaildirFlag::Replied),
|
||||
"seen" => Ok(MaildirFlag::Seen),
|
||||
"trashed" => Ok(MaildirFlag::Trashed),
|
||||
"draft" => Ok(MaildirFlag::Draft),
|
||||
"flagged" => Ok(MaildirFlag::Flagged),
|
||||
flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
//! Maildir mailbox module.
|
||||
//!
|
||||
//! This module provides Maildir types and conversion utilities
|
||||
//! related to the mailbox
|
||||
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ffi::OsStr,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mbox::Mboxes,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of Maildir mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct MaildirMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<MaildirMbox>,
|
||||
}
|
||||
|
||||
impl Deref for MaildirMboxes {
|
||||
type Target = Vec<MaildirMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for MaildirMboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for MaildirMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct MaildirMbox {
|
||||
/// Represents the mailbox name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl MaildirMbox {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self { name: name.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MaildirMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for MaildirMbox {
|
||||
fn head() -> Row {
|
||||
Row::new().cell(Cell::new("SUBDIR").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new().cell(Cell::new(&self.name).green())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_create_new_mbox() {
|
||||
assert_eq!(MaildirMbox::default(), MaildirMbox::new(""));
|
||||
assert_eq!(
|
||||
MaildirMbox {
|
||||
name: "INBOX".into(),
|
||||
..MaildirMbox::default()
|
||||
},
|
||||
MaildirMbox::new("INBOX")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_display_mbox() {
|
||||
let default_mbox = MaildirMbox::default();
|
||||
assert_eq!("", default_mbox.to_string());
|
||||
|
||||
let new_mbox = MaildirMbox::new("INBOX");
|
||||
assert_eq!("INBOX", new_mbox.to_string());
|
||||
|
||||
let full_mbox = MaildirMbox {
|
||||
name: "Sent".into(),
|
||||
};
|
||||
assert_eq!("Sent", full_mbox.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw mailboxes returned by the `maildir` crate.
|
||||
pub type RawMaildirMboxes = maildir::MaildirEntries;
|
||||
|
||||
impl TryFrom<RawMaildirMboxes> for MaildirMboxes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mail_entries: RawMaildirMboxes) -> Result<Self, Self::Error> {
|
||||
let mut mboxes = vec![];
|
||||
for entry in mail_entries {
|
||||
mboxes.push(entry?.try_into()?);
|
||||
}
|
||||
Ok(MaildirMboxes { mboxes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw mailbox returned by the `maildir` crate.
|
||||
pub type RawMaildirMbox = maildir::Maildir;
|
||||
|
||||
impl TryFrom<RawMaildirMbox> for MaildirMbox {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mail_entry: RawMaildirMbox) -> Result<Self, Self::Error> {
|
||||
let subdir_name = mail_entry.path().file_name();
|
||||
Ok(Self {
|
||||
name: subdir_name
|
||||
.and_then(OsStr::to_str)
|
||||
.and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) })
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"cannot parse maildir subdirectory name from path {:?}",
|
||||
subdir_name,
|
||||
)
|
||||
})?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,453 +0,0 @@
|
|||
use std::{convert::TryInto, fs};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
|
||||
config::{AccountConfig, NotmuchBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
/// Represents the Notmuch backend.
|
||||
pub struct NotmuchBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
notmuch_config: &'a NotmuchBackendConfig,
|
||||
pub mdir: &'a mut MaildirBackend<'a>,
|
||||
db: notmuch::Database,
|
||||
}
|
||||
|
||||
impl<'a> NotmuchBackend<'a> {
|
||||
pub fn new(
|
||||
account_config: &'a AccountConfig,
|
||||
notmuch_config: &'a NotmuchBackendConfig,
|
||||
mdir: &'a mut MaildirBackend<'a>,
|
||||
) -> Result<NotmuchBackend<'a>> {
|
||||
info!(">> create new notmuch backend");
|
||||
|
||||
let backend = Self {
|
||||
account_config,
|
||||
notmuch_config,
|
||||
mdir,
|
||||
db: notmuch::Database::open(
|
||||
notmuch_config.notmuch_database_dir.clone(),
|
||||
notmuch::DatabaseMode::ReadWrite,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot open notmuch database at {:?}",
|
||||
notmuch_config.notmuch_database_dir
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
info!("<< create new notmuch backend");
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
fn _search_envelopes(
|
||||
&mut self,
|
||||
query: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
// Gets envelopes matching the given Notmuch query.
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let mut envelopes: NotmuchEnvelopes = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?;
|
||||
debug!("envelopes len: {:?}", envelopes.len());
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
// Calculates pagination boundaries.
|
||||
let page_begin = page * page_size;
|
||||
debug!("page begin: {:?}", page_begin);
|
||||
if page_begin > envelopes.len() {
|
||||
return Err(anyhow!(
|
||||
"cannot get notmuch envelopes at page {:?} (out of bounds)",
|
||||
page_begin + 1,
|
||||
));
|
||||
}
|
||||
let page_end = envelopes.len().min(page_begin + page_size);
|
||||
debug!("page end: {:?}", page_end);
|
||||
|
||||
// Sorts envelopes by most recent date.
|
||||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
|
||||
// Applies pagination boundaries.
|
||||
envelopes.envelopes = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
// represents the minimum hash length possible to avoid
|
||||
// conflicts.
|
||||
let short_hash_len = {
|
||||
let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?;
|
||||
let entries = envelopes
|
||||
.iter()
|
||||
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
|
||||
.collect();
|
||||
mapper.append(entries)?
|
||||
};
|
||||
debug!("short hash length: {:?}", short_hash_len);
|
||||
|
||||
// Shorten envelopes hash.
|
||||
envelopes
|
||||
.iter_mut()
|
||||
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
|
||||
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
||||
fn add_mbox(&mut self, _mbox: &str) -> Result<()> {
|
||||
info!(">> add notmuch mailbox");
|
||||
info!("<< add notmuch mailbox");
|
||||
Err(anyhow!(
|
||||
"cannot add notmuch mailbox: feature not implemented"
|
||||
))
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
info!(">> get notmuch virtual mailboxes");
|
||||
|
||||
let mut mboxes: Vec<_> = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.iter()
|
||||
.map(|(k, v)| NotmuchMbox::new(k, v))
|
||||
.collect();
|
||||
trace!("virtual mailboxes: {:?}", mboxes);
|
||||
mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
|
||||
info!("<< get notmuch virtual mailboxes");
|
||||
Ok(Box::new(NotmuchMboxes { mboxes }))
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, _mbox: &str) -> Result<()> {
|
||||
info!(">> delete notmuch mailbox");
|
||||
info!("<< delete notmuch mailbox");
|
||||
Err(anyhow!(
|
||||
"cannot delete notmuch mailbox: feature not implemented"
|
||||
))
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
virt_mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> get notmuch envelopes");
|
||||
debug!("virtual mailbox: {:?}", virt_mbox);
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let query = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.get(virt_mbox)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("all");
|
||||
debug!("query: {:?}", query);
|
||||
let envelopes = self._search_envelopes(query, page_size, page)?;
|
||||
|
||||
info!("<< get notmuch envelopes");
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
virt_mbox: &str,
|
||||
query: &str,
|
||||
_sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> search notmuch envelopes");
|
||||
debug!("virtual mailbox: {:?}", virt_mbox);
|
||||
debug!("query: {:?}", query);
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let query = if query.is_empty() {
|
||||
self.account_config
|
||||
.mailboxes
|
||||
.get(virt_mbox)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("all")
|
||||
} else {
|
||||
query
|
||||
};
|
||||
debug!("final query: {:?}", query);
|
||||
let envelopes = self._search_envelopes(query, page_size, page)?;
|
||||
|
||||
info!("<< search notmuch envelopes");
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result<Box<dyn ToString>> {
|
||||
info!(">> add notmuch envelopes");
|
||||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
|
||||
// Adds the message to the maildir folder and gets its hash.
|
||||
let hash = self
|
||||
.mdir
|
||||
.add_msg("", msg, "seen")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot add notmuch message to maildir {:?}",
|
||||
self.notmuch_config.notmuch_database_dir
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
debug!("hash: {:?}", hash);
|
||||
|
||||
// Retrieves the file path of the added message by its maildir
|
||||
// identifier.
|
||||
let mut mapper = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?;
|
||||
let id = mapper
|
||||
.find(&hash)
|
||||
.with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?;
|
||||
debug!("id: {:?}", id);
|
||||
let file_path = dir.join("cur").join(format!("{}:2,S", id));
|
||||
debug!("file path: {:?}", file_path);
|
||||
|
||||
// Adds the message to the notmuch database by indexing it.
|
||||
let id = self
|
||||
.db
|
||||
.index_file(&file_path, None)
|
||||
.with_context(|| format!("cannot index notmuch message from file {:?}", file_path))?
|
||||
.id()
|
||||
.to_string();
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
mapper
|
||||
.append(vec![(hash.clone(), id.clone())])
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot append hash {:?} with id {:?} to id mapper",
|
||||
hash, id
|
||||
)
|
||||
})?;
|
||||
|
||||
// Attaches tags to the notmuch message.
|
||||
self.add_flags("", &hash, tags)
|
||||
.with_context(|| format!("cannot add flags to notmuch message {:?}", id))?;
|
||||
|
||||
info!("<< add notmuch envelopes");
|
||||
Ok(Box::new(hash))
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, _: &str, short_hash: &str) -> Result<Msg> {
|
||||
info!(">> add notmuch envelopes");
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let msg_file_path = self
|
||||
.db
|
||||
.find_message(&id)
|
||||
.with_context(|| format!("cannot find notmuch message {:?}", id))?
|
||||
.ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))?
|
||||
.filename()
|
||||
.to_owned();
|
||||
debug!("message file path: {:?}", msg_file_path);
|
||||
let raw_msg = fs::read(&msg_file_path).with_context(|| {
|
||||
format!("cannot read notmuch message from file {:?}", msg_file_path)
|
||||
})?;
|
||||
let msg = mailparse::parse_mail(&raw_msg)
|
||||
.with_context(|| format!("cannot parse raw notmuch message {:?}", id))?;
|
||||
let msg = Msg::from_parsed_mail(msg, &self.account_config)
|
||||
.with_context(|| format!("cannot parse notmuch message {:?}", id))?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< get notmuch message");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> {
|
||||
info!(">> copy notmuch message");
|
||||
info!("<< copy notmuch message");
|
||||
Err(anyhow!(
|
||||
"cannot copy notmuch message: feature not implemented"
|
||||
))
|
||||
}
|
||||
|
||||
fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> {
|
||||
info!(">> move notmuch message");
|
||||
info!("<< move notmuch message");
|
||||
Err(anyhow!(
|
||||
"cannot move notmuch message: feature not implemented"
|
||||
))
|
||||
}
|
||||
|
||||
fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> delete notmuch message");
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let msg_file_path = self
|
||||
.db
|
||||
.find_message(&id)
|
||||
.with_context(|| format!("cannot find notmuch message {:?}", id))?
|
||||
.ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))?
|
||||
.filename()
|
||||
.to_owned();
|
||||
debug!("message file path: {:?}", msg_file_path);
|
||||
self.db
|
||||
.remove_message(msg_file_path)
|
||||
.with_context(|| format!("cannot delete notmuch message {:?}", id))?;
|
||||
|
||||
info!("<< delete notmuch message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
|
||||
info!(">> add notmuch message flags");
|
||||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
let tags: Vec<_> = tags.split_whitespace().collect();
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
for msg in msgs {
|
||||
for tag in tags.iter() {
|
||||
msg.add_tag(*tag).with_context(|| {
|
||||
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
info!("<< add notmuch message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
|
||||
info!(">> set notmuch message flags");
|
||||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
let tags: Vec<_> = tags.split_whitespace().collect();
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
for msg in msgs {
|
||||
msg.remove_all_tags().with_context(|| {
|
||||
format!("cannot remove all tags from notmuch message {:?}", msg.id())
|
||||
})?;
|
||||
for tag in tags.iter() {
|
||||
msg.add_tag(*tag).with_context(|| {
|
||||
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
info!("<< set notmuch message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
|
||||
info!(">> delete notmuch message flags");
|
||||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
let tags: Vec<_> = tags.split_whitespace().collect();
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
for msg in msgs {
|
||||
for tag in tags.iter() {
|
||||
msg.remove_tag(*tag).with_context(|| {
|
||||
format!(
|
||||
"cannot delete tag {:?} from notmuch message {:?}",
|
||||
tag,
|
||||
msg.id()
|
||||
)
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
info!("<< delete notmuch message flags");
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
//! Notmuch mailbox module.
|
||||
//!
|
||||
//! This module provides Notmuch types and conversion utilities
|
||||
//! related to the envelope
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::DateTime;
|
||||
use log::{info, trace};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
msg::{from_slice_to_addrs, Addr},
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<NotmuchEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for NotmuchEnvelopes {
|
||||
type Target = Vec<NotmuchEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NotmuchEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchEnvelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the envelope. The envelope is just a message subset,
|
||||
/// and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct NotmuchEnvelope {
|
||||
/// Represents the id of the message.
|
||||
pub id: String,
|
||||
|
||||
/// Represents the MD5 hash of the message id.
|
||||
pub hash: String,
|
||||
|
||||
/// Represents the tags of the message.
|
||||
pub flags: Vec<String>,
|
||||
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
|
||||
/// Represents the date of the message.
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for NotmuchEnvelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("HASH").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 hash = self.hash.to_string();
|
||||
let unseen = !self.flags.contains(&String::from("unread"));
|
||||
let flags = String::new();
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = &self.date;
|
||||
Row::new()
|
||||
.cell(Cell::new(hash).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw envelopees returned by the `notmuch` crate.
|
||||
pub type RawNotmuchEnvelopes = notmuch::Messages;
|
||||
|
||||
impl<'a> TryFrom<RawNotmuchEnvelopes> for NotmuchEnvelopes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result<Self, Self::Error> {
|
||||
let mut envelopes = vec![];
|
||||
for raw_envelope in raw_envelopes {
|
||||
let envelope: NotmuchEnvelope = raw_envelope
|
||||
.try_into()
|
||||
.context("cannot parse notmuch mail entry")?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
Ok(NotmuchEnvelopes { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw envelope returned by the `notmuch` crate.
|
||||
pub type RawNotmuchEnvelope = notmuch::Message;
|
||||
|
||||
impl<'a> TryFrom<RawNotmuchEnvelope> for NotmuchEnvelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result<Self, Self::Error> {
|
||||
info!("begin: try building envelope from notmuch parsed mail");
|
||||
|
||||
let id = raw_envelope.id().to_string();
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
let subject = raw_envelope
|
||||
.header("subject")
|
||||
.context("cannot get header \"Subject\" from notmuch message")?
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let sender = raw_envelope
|
||||
.header("from")
|
||||
.context("cannot get header \"From\" from notmuch message")?
|
||||
.ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", id))?
|
||||
.to_string();
|
||||
let sender = from_slice_to_addrs(sender)?
|
||||
.and_then(|senders| {
|
||||
if senders.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(senders)
|
||||
}
|
||||
})
|
||||
.map(|senders| match &senders[0] {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
|
||||
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
|
||||
}
|
||||
Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(),
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find sender"))?;
|
||||
let date = raw_envelope
|
||||
.header("date")
|
||||
.context("cannot get header \"Date\" from notmuch message")?
|
||||
.ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))?
|
||||
.to_string();
|
||||
let date =
|
||||
DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0)
|
||||
.context(format!(
|
||||
"cannot parse message date {:?} of notmuch message {:?}",
|
||||
date, id
|
||||
))?
|
||||
.naive_local()
|
||||
.to_string();
|
||||
|
||||
let envelope = Self {
|
||||
id,
|
||||
hash,
|
||||
flags: raw_envelope.tags().collect(),
|
||||
subject,
|
||||
sender,
|
||||
date,
|
||||
};
|
||||
trace!("envelope: {:?}", envelope);
|
||||
|
||||
info!("end: try building envelope from notmuch parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
//! Notmuch mailbox module.
|
||||
//!
|
||||
//! This module provides Notmuch types and conversion utilities
|
||||
//! related to the mailbox
|
||||
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mbox::Mboxes,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of Notmuch mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<NotmuchMbox>,
|
||||
}
|
||||
|
||||
impl Deref for NotmuchMboxes {
|
||||
type Target = Vec<NotmuchMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchMboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for NotmuchMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the notmuch virtual mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct NotmuchMbox {
|
||||
/// Represents the virtual mailbox name.
|
||||
pub name: String,
|
||||
|
||||
/// Represents the query associated to the virtual mailbox name.
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
impl NotmuchMbox {
|
||||
pub fn new(name: &str, query: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
query: query.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NotmuchMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for NotmuchMbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("QUERY").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).white())
|
||||
.cell(Cell::new(&self.query).green())
|
||||
}
|
||||
}
|
|
@ -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,9 +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 website].
|
||||
//!
|
||||
//! [clap's docs.rs website]: https://docs.rs/clap/2.33.3/clap/enum.Shell.html
|
||||
|
||||
pub mod compl_args;
|
||||
pub mod compl_handlers;
|
|
@ -1,109 +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,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::DeserializedAccountConfig,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
|
||||
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
|
||||
let mut accounts: Vec<_> = map
|
||||
.map(|(name, config)| match config {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => {
|
||||
Account::new(name, "imap", config.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => {
|
||||
Account::new(name, "maildir", config.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => {
|
||||
Account::new(name, "notmuch", config.default.unwrap_or_default())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
Self(accounts)
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
//! This module provides arguments related to the user account config.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table_arg;
|
||||
|
||||
type MaxTableWidth = Option<usize>;
|
||||
|
||||
/// Represents the account commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
/// Represents the list accounts command.
|
||||
List(MaxTableWidth),
|
||||
}
|
||||
|
||||
/// Represents the account command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
info!(">> account command matcher");
|
||||
|
||||
let cmd = if let Some(m) = m.subcommand_matches("accounts") {
|
||||
info!("accounts command matched");
|
||||
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
info!("<< account command matcher");
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the account subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("accounts")
|
||||
.aliases(&["account", "acc", "a"])
|
||||
.about("Lists accounts")
|
||||
.arg(table_arg::max_width())]
|
||||
}
|
||||
|
||||
/// Represents the user account name argument.
|
||||
/// This argument allows the user to select a different account than
|
||||
/// the default one.
|
||||
pub fn name_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("account")
|
||||
.long("account")
|
||||
.short("a")
|
||||
.help("Selects a specific account")
|
||||
.value_name("NAME")
|
||||
}
|
|
@ -1,437 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::MailAddr;
|
||||
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf};
|
||||
|
||||
use crate::{config::*, output::run_cmd};
|
||||
|
||||
/// Represents the user account.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct AccountConfig {
|
||||
/// Represents the name of the user account.
|
||||
pub name: String,
|
||||
/// Makes this account the default one.
|
||||
pub default: bool,
|
||||
/// Represents the display name of the user account.
|
||||
pub display_name: String,
|
||||
/// Represents the email address of the user account.
|
||||
pub email: String,
|
||||
/// Represents the downloads directory (mostly for attachments).
|
||||
pub downloads_dir: PathBuf,
|
||||
/// Represents the signature of the user.
|
||||
pub sig: Option<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: usize,
|
||||
/// Represents the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the default IMAP query "NEW" used to fetch new messages
|
||||
pub notify_query: String,
|
||||
/// Represents the watch commands.
|
||||
pub watch_cmds: Vec<String>,
|
||||
/// Represents the text/plain format as defined in the
|
||||
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
|
||||
pub format: Format,
|
||||
/// Overrides the default headers displayed at the top of
|
||||
/// the read message.
|
||||
pub read_headers: Vec<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// Represents hooks.
|
||||
pub hooks: Hooks,
|
||||
|
||||
/// Represents the SMTP host.
|
||||
pub smtp_host: String,
|
||||
/// Represents the SMTP port.
|
||||
pub smtp_port: u16,
|
||||
/// Enables StartTLS.
|
||||
pub smtp_starttls: bool,
|
||||
/// Trusts any certificate.
|
||||
pub smtp_insecure: bool,
|
||||
/// Represents the SMTP login.
|
||||
pub smtp_login: String,
|
||||
/// Represents the SMTP password command.
|
||||
pub smtp_passwd_cmd: String,
|
||||
|
||||
/// Represents the command used to encrypt a message.
|
||||
pub pgp_encrypt_cmd: Option<String>,
|
||||
/// Represents the command used to decrypt a message.
|
||||
pub pgp_decrypt_cmd: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> AccountConfig {
|
||||
/// tries to create an account from a config and an optional account name.
|
||||
pub fn from_config_and_opt_account_name(
|
||||
config: &'a DeserializedConfig,
|
||||
account_name: Option<&str>,
|
||||
) -> Result<(AccountConfig, BackendConfig)> {
|
||||
info!("begin: parsing account and backend configs from config and account name");
|
||||
|
||||
debug!("account name: {:?}", account_name.unwrap_or("default"));
|
||||
let (name, account) = match account_name.map(|name| name.trim()) {
|
||||
Some("default") | Some("") | None => config
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|(_, account)| match account {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(account) => account.default.unwrap_or_default(),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(account) => {
|
||||
account.default.unwrap_or_default()
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(account) => {
|
||||
account.default.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.map(|(name, account)| (name.to_owned(), account))
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => config
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account))
|
||||
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)),
|
||||
}?;
|
||||
|
||||
let base_account = account.to_base();
|
||||
let downloads_dir = base_account
|
||||
.downloads_dir
|
||||
.as_ref()
|
||||
.and_then(|dir| dir.to_str())
|
||||
.and_then(|dir| shellexpand::full(dir).ok())
|
||||
.map(|dir| PathBuf::from(dir.to_string()))
|
||||
.or_else(|| {
|
||||
config
|
||||
.downloads_dir
|
||||
.as_ref()
|
||||
.and_then(|dir| dir.to_str())
|
||||
.and_then(|dir| shellexpand::full(dir).ok())
|
||||
.map(|dir| PathBuf::from(dir.to_string()))
|
||||
})
|
||||
.unwrap_or_else(env::temp_dir);
|
||||
|
||||
let default_page_size = base_account
|
||||
.default_page_size
|
||||
.as_ref()
|
||||
.or_else(|| config.default_page_size.as_ref())
|
||||
.unwrap_or(&DEFAULT_PAGE_SIZE)
|
||||
.to_owned();
|
||||
|
||||
let default_sig_delim = DEFAULT_SIG_DELIM.to_string();
|
||||
let sig_delim = base_account
|
||||
.signature_delimiter
|
||||
.as_ref()
|
||||
.or_else(|| config.signature_delimiter.as_ref())
|
||||
.unwrap_or(&default_sig_delim);
|
||||
let sig = base_account
|
||||
.signature
|
||||
.as_ref()
|
||||
.or_else(|| config.signature.as_ref());
|
||||
let sig = sig
|
||||
.and_then(|sig| shellexpand::full(sig).ok())
|
||||
.map(String::from)
|
||||
.and_then(|sig| fs::read_to_string(sig).ok())
|
||||
.or_else(|| sig.map(|sig| sig.to_owned()))
|
||||
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
|
||||
|
||||
let account_config = AccountConfig {
|
||||
name,
|
||||
display_name: base_account
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(&config.name)
|
||||
.to_owned(),
|
||||
downloads_dir,
|
||||
sig,
|
||||
default_page_size,
|
||||
notify_cmd: base_account.notify_cmd.clone(),
|
||||
notify_query: base_account
|
||||
.notify_query
|
||||
.as_ref()
|
||||
.or_else(|| config.notify_query.as_ref())
|
||||
.unwrap_or(&String::from("NEW"))
|
||||
.to_owned(),
|
||||
watch_cmds: base_account
|
||||
.watch_cmds
|
||||
.as_ref()
|
||||
.or_else(|| config.watch_cmds.as_ref())
|
||||
.unwrap_or(&vec![])
|
||||
.to_owned(),
|
||||
format: base_account.format.unwrap_or_default(),
|
||||
read_headers: base_account.read_headers,
|
||||
mailboxes: base_account.mailboxes.clone(),
|
||||
hooks: base_account.hooks.unwrap_or_default(),
|
||||
default: base_account.default.unwrap_or_default(),
|
||||
email: base_account.email.to_owned(),
|
||||
|
||||
smtp_host: base_account.smtp_host.to_owned(),
|
||||
smtp_port: base_account.smtp_port,
|
||||
smtp_starttls: base_account.smtp_starttls.unwrap_or_default(),
|
||||
smtp_insecure: base_account.smtp_insecure.unwrap_or_default(),
|
||||
smtp_login: base_account.smtp_login.to_owned(),
|
||||
smtp_passwd_cmd: base_account.smtp_passwd_cmd.to_owned(),
|
||||
|
||||
pgp_encrypt_cmd: base_account.pgp_encrypt_cmd.to_owned(),
|
||||
pgp_decrypt_cmd: base_account.pgp_decrypt_cmd.to_owned(),
|
||||
};
|
||||
trace!("account config: {:?}", account_config);
|
||||
|
||||
let backend_config = match account {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => BackendConfig::Imap(ImapBackendConfig {
|
||||
imap_host: config.imap_host.clone(),
|
||||
imap_port: config.imap_port.clone(),
|
||||
imap_starttls: config.imap_starttls.unwrap_or_default(),
|
||||
imap_insecure: config.imap_insecure.unwrap_or_default(),
|
||||
imap_login: config.imap_login.clone(),
|
||||
imap_passwd_cmd: config.imap_passwd_cmd.clone(),
|
||||
}),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => {
|
||||
BackendConfig::Maildir(MaildirBackendConfig {
|
||||
maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(),
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => {
|
||||
BackendConfig::Notmuch(NotmuchBackendConfig {
|
||||
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)?
|
||||
.to_string()
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
};
|
||||
trace!("backend config: {:?}", backend_config);
|
||||
|
||||
info!("end: parsing account and backend configs from config and account name");
|
||||
Ok((account_config, backend_config))
|
||||
}
|
||||
|
||||
/// Builds the full RFC822 compliant address of the user account.
|
||||
pub fn address(&self) -> Result<MailAddr> {
|
||||
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
|
||||
let addr = if self.display_name.is_empty() {
|
||||
self.email.clone()
|
||||
} else if has_special_chars {
|
||||
// Wraps the name with double quotes if it contains any special character.
|
||||
format!("\"{}\" <{}>", self.display_name, self.email)
|
||||
} else {
|
||||
format!("{} <{}>", self.display_name, self.email)
|
||||
};
|
||||
|
||||
Ok(mailparse::addrparse(&addr)
|
||||
.context(format!(
|
||||
"cannot parse account address {:?}",
|
||||
self.display_name
|
||||
))?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
/// Builds the user account SMTP credentials.
|
||||
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
|
||||
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
|
||||
let passwd = passwd
|
||||
.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||
.to_owned();
|
||||
|
||||
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
|
||||
}
|
||||
|
||||
/// Encrypts a file.
|
||||
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
|
||||
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
|
||||
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
|
||||
run_cmd(&encrypt_file_cmd).map(Some).context(format!(
|
||||
"cannot run pgp encrypt command {:?}",
|
||||
encrypt_file_cmd
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypts a file.
|
||||
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
|
||||
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
|
||||
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
|
||||
run_cmd(&decrypt_file_cmd).map(Some).context(format!(
|
||||
"cannot run pgp decrypt command {:?}",
|
||||
decrypt_file_cmd
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the download path from a file name.
|
||||
pub fn get_download_file_path<S: AsRef<str>>(&self, file_name: S) -> Result<PathBuf> {
|
||||
let file_path = self.downloads_dir.join(file_name.as_ref());
|
||||
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
|
||||
.context(format!(
|
||||
"cannot get download file path of {:?}",
|
||||
file_name.as_ref()
|
||||
))
|
||||
}
|
||||
|
||||
/// Gets the unique download path from a file name by adding suffixes in case of name conflicts.
|
||||
pub fn get_unique_download_file_path(
|
||||
&self,
|
||||
original_file_path: &PathBuf,
|
||||
is_file: impl Fn(&PathBuf, u8) -> bool,
|
||||
) -> Result<PathBuf> {
|
||||
let mut count = 0;
|
||||
let file_ext = original_file_path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|fext| String::from(".") + fext)
|
||||
.unwrap_or_default();
|
||||
let mut file_path = original_file_path.clone();
|
||||
|
||||
while is_file(&file_path, count) {
|
||||
count += 1;
|
||||
file_path.set_file_name(OsStr::new(
|
||||
&original_file_path
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
|
||||
.ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
/// Runs the notify command.
|
||||
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
|
||||
let subject = subject.as_ref();
|
||||
let sender = sender.as_ref();
|
||||
|
||||
let default_cmd = format!(r#"notify-send "New message from {}" "{}""#, sender, subject);
|
||||
let cmd = self
|
||||
.notify_cmd
|
||||
.as_ref()
|
||||
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
|
||||
.unwrap_or(default_cmd);
|
||||
|
||||
debug!("run command: {}", cmd);
|
||||
run_cmd(&cmd).context("cannot run notify cmd")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the mailbox alias if exists, otherwise returns the
|
||||
/// mailbox. Also tries to expand shell variables.
|
||||
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String> {
|
||||
let mbox = self
|
||||
.mailboxes
|
||||
.get(&mbox.trim().to_lowercase())
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(mbox);
|
||||
shellexpand::full(mbox)
|
||||
.map(String::from)
|
||||
.with_context(|| format!("cannot expand mailbox path {:?}", mbox))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all existing kind of account (backend).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BackendConfig {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Imap(ImapBackendConfig),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Maildir(MaildirBackendConfig),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Notmuch(NotmuchBackendConfig),
|
||||
}
|
||||
|
||||
/// Represents the IMAP backend.
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ImapBackendConfig {
|
||||
/// Represents the IMAP host.
|
||||
pub imap_host: String,
|
||||
/// Represents the IMAP port.
|
||||
pub imap_port: u16,
|
||||
/// Enables StartTLS.
|
||||
pub imap_starttls: bool,
|
||||
/// Trusts any certificate.
|
||||
pub imap_insecure: bool,
|
||||
/// Represents the IMAP login.
|
||||
pub imap_login: String,
|
||||
/// Represents the IMAP password command.
|
||||
pub imap_passwd_cmd: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
impl ImapBackendConfig {
|
||||
/// Gets the IMAP password of the user account.
|
||||
pub fn imap_passwd(&self) -> Result<String> {
|
||||
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
|
||||
let passwd = passwd
|
||||
.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||
.to_owned();
|
||||
Ok(passwd)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the Maildir backend.
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MaildirBackendConfig {
|
||||
/// Represents the Maildir directory path.
|
||||
pub maildir_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Represents the Notmuch backend.
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NotmuchBackendConfig {
|
||||
/// Represents the Notmuch database path.
|
||||
pub notmuch_database_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_get_unique_download_file_path() {
|
||||
let account = AccountConfig::default();
|
||||
let path = PathBuf::from("downloads/file.ext");
|
||||
|
||||
// When file path is unique
|
||||
assert!(matches!(
|
||||
account.get_unique_download_file_path(&path, |_, _| false),
|
||||
Ok(path) if path == PathBuf::from("downloads/file.ext")
|
||||
));
|
||||
|
||||
// When 1 file path already exist
|
||||
assert!(matches!(
|
||||
account.get_unique_download_file_path(&path, |_, count| count < 1),
|
||||
Ok(path) if path == PathBuf::from("downloads/file_1.ext")
|
||||
));
|
||||
|
||||
// When 5 file paths already exist
|
||||
assert!(matches!(
|
||||
account.get_unique_download_file_path(&path, |_, count| count < 5),
|
||||
Ok(path) if path == PathBuf::from("downloads/file_5.ext")
|
||||
));
|
||||
|
||||
// When file path has no extension
|
||||
let path = PathBuf::from("downloads/file");
|
||||
assert!(matches!(
|
||||
account.get_unique_download_file_path(&path, |_, count| count < 5),
|
||||
Ok(path) if path == PathBuf::from("downloads/file_5")
|
||||
));
|
||||
|
||||
// When file path has 2 extensions
|
||||
let path = PathBuf::from("downloads/file.ext.ext2");
|
||||
assert!(matches!(
|
||||
account.get_unique_download_file_path(&path, |_, count| count < 5),
|
||||
Ok(path) if path == PathBuf::from("downloads/file.ext_5.ext2")
|
||||
));
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
//! Account handlers module.
|
||||
//!
|
||||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
config::{AccountConfig, Accounts, DeserializedConfig},
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
};
|
||||
|
||||
/// Lists all accounts.
|
||||
pub fn list<'a, P: PrinterService>(
|
||||
max_width: Option<usize>,
|
||||
config: &DeserializedConfig,
|
||||
account_config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!(">> account list handler");
|
||||
|
||||
let accounts: Accounts = config.accounts.iter().into();
|
||||
trace!("accounts: {:?}", accounts);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: &account_config.format,
|
||||
max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
info!("<< account list handler");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
config::{DeserializedAccountConfig, DeserializedImapAccountConfig},
|
||||
output::{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 PrinterService 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 config = DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account-1".into(),
|
||||
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
|
||||
default: Some(true),
|
||||
..DeserializedImapAccountConfig::default()
|
||||
}),
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
};
|
||||
|
||||
let account_config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
|
||||
assert!(list(None, &config, &account_config, &mut printer).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │BACKEND │DEFAULT \n",
|
||||
"account-1 │imap │yes \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
//! This module provides arguments related to the user config.
|
||||
|
||||
use clap::Arg;
|
||||
|
||||
/// Represents the config path argument.
|
||||
/// This argument allows the user to customize the config file path.
|
||||
pub fn path_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("config")
|
||||
.long("config")
|
||||
.short("c")
|
||||
.help("Forces a specific config path")
|
||||
.value_name("PATH")
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::{Format, Hooks};
|
||||
|
||||
pub trait ToDeserializedBaseAccountConfig {
|
||||
fn to_base(&self) -> DeserializedBaseAccountConfig;
|
||||
}
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DeserializedAccountConfig {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Imap(DeserializedImapAccountConfig),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Maildir(DeserializedMaildirAccountConfig),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Notmuch(DeserializedNotmuchAccountConfig),
|
||||
}
|
||||
|
||||
impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig {
|
||||
fn to_base(&self) -> DeserializedBaseAccountConfig {
|
||||
match self {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Self::Imap(config) => config.to_base(),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Self::Maildir(config) => config.to_base(),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Self::Notmuch(config) => config.to_base(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! make_account_config {
|
||||
($AccountConfig:ident, $($element: ident: $ty: ty),*) => {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct $AccountConfig {
|
||||
/// Overrides the display name of the user for this account.
|
||||
pub name: Option<String>,
|
||||
/// Overrides the downloads directory (mostly for attachments).
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
/// Overrides the signature for this account.
|
||||
pub signature: Option<String>,
|
||||
/// Overrides the signature delimiter for this account.
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Overrides the default page size for this account.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Overrides the notify command for this account.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the IMAP query used to fetch new messages for this account.
|
||||
pub notify_query: Option<String>,
|
||||
/// Overrides the watch commands for this account.
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
/// Represents the text/plain format as defined in the
|
||||
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
|
||||
pub format: Option<Format>,
|
||||
/// Represents the default headers displayed at the top of
|
||||
/// the read message.
|
||||
#[serde(default)]
|
||||
pub read_headers: Vec<String>,
|
||||
|
||||
/// Makes this account the default one.
|
||||
pub default: Option<bool>,
|
||||
/// Represents the account email address.
|
||||
pub email: String,
|
||||
|
||||
/// Represents the SMTP host.
|
||||
pub smtp_host: String,
|
||||
/// Represents the SMTP port.
|
||||
pub smtp_port: u16,
|
||||
/// Enables StartTLS.
|
||||
pub smtp_starttls: Option<bool>,
|
||||
/// Trusts any certificate.
|
||||
pub smtp_insecure: Option<bool>,
|
||||
/// Represents the SMTP login.
|
||||
pub smtp_login: String,
|
||||
/// Represents the SMTP password command.
|
||||
pub smtp_passwd_cmd: String,
|
||||
|
||||
/// Represents the command used to encrypt a message.
|
||||
pub pgp_encrypt_cmd: Option<String>,
|
||||
/// Represents the command used to decrypt a message.
|
||||
pub pgp_decrypt_cmd: Option<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
#[serde(default)]
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// Represents hooks.
|
||||
pub hooks: Option<Hooks>,
|
||||
|
||||
$(pub $element: $ty),*
|
||||
}
|
||||
|
||||
impl ToDeserializedBaseAccountConfig for $AccountConfig {
|
||||
fn to_base(&self) -> DeserializedBaseAccountConfig {
|
||||
DeserializedBaseAccountConfig {
|
||||
name: self.name.clone(),
|
||||
downloads_dir: self.downloads_dir.clone(),
|
||||
signature: self.signature.clone(),
|
||||
signature_delimiter: self.signature_delimiter.clone(),
|
||||
default_page_size: self.default_page_size.clone(),
|
||||
notify_cmd: self.notify_cmd.clone(),
|
||||
notify_query: self.notify_query.clone(),
|
||||
watch_cmds: self.watch_cmds.clone(),
|
||||
format: self.format.clone(),
|
||||
read_headers: self.read_headers.clone(),
|
||||
|
||||
default: self.default.clone(),
|
||||
email: self.email.clone(),
|
||||
|
||||
smtp_host: self.smtp_host.clone(),
|
||||
smtp_port: self.smtp_port.clone(),
|
||||
smtp_starttls: self.smtp_starttls.clone(),
|
||||
smtp_insecure: self.smtp_insecure.clone(),
|
||||
smtp_login: self.smtp_login.clone(),
|
||||
smtp_passwd_cmd: self.smtp_passwd_cmd.clone(),
|
||||
|
||||
pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(),
|
||||
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
|
||||
|
||||
mailboxes: self.mailboxes.clone(),
|
||||
hooks: self.hooks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
make_account_config!(DeserializedBaseAccountConfig,);
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
make_account_config!(
|
||||
DeserializedImapAccountConfig,
|
||||
imap_host: String,
|
||||
imap_port: u16,
|
||||
imap_starttls: Option<bool>,
|
||||
imap_insecure: Option<bool>,
|
||||
imap_login: String,
|
||||
imap_passwd_cmd: String
|
||||
);
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String);
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
make_account_config!(
|
||||
DeserializedNotmuchAccountConfig,
|
||||
notmuch_database_dir: String
|
||||
);
|
|
@ -1,97 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use toml;
|
||||
|
||||
use crate::config::DeserializedAccountConfig;
|
||||
|
||||
pub const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
|
||||
|
||||
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
|
||||
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
|
||||
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedConfig {
|
||||
/// Represents the display name of the user.
|
||||
pub name: String,
|
||||
/// Represents the downloads directory (mostly for attachments).
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
/// Represents the signature of the user.
|
||||
pub signature: Option<String>,
|
||||
/// Overrides the default signature delimiter "`-- \n`".
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Represents the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the default IMAP query "NEW" used to fetch new messages
|
||||
pub notify_query: Option<String>,
|
||||
/// Represents the watch commands.
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
|
||||
/// Represents all the user accounts.
|
||||
#[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> {
|
||||
info!("begin: try to 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 = toml::from_str(&content).context("cannot parse config file")?;
|
||||
info!("end: try to parse config from path");
|
||||
trace!("config: {:?}", config);
|
||||
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 find \"XDG_CONFIG_HOME\" env var")?;
|
||||
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 find {:?} 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 find {:?} 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())
|
||||
.context("cannot find config path")
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
/// Represents the text/plain format as defined in the [RFC2646]. The
|
||||
/// format is then used by the table system to adjust the way it is
|
||||
/// rendered.
|
||||
///
|
||||
/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(tag = "type", content = "width", rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
// Forces the content width with a fixed amount of pixels.
|
||||
Fixed(usize),
|
||||
// Makes the content fit the terminal.
|
||||
Auto,
|
||||
// Does not restrict the content.
|
||||
Flowed,
|
||||
}
|
||||
|
||||
impl Default for Format {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Hooks {
|
||||
pub pre_send: Option<String>,
|
||||
}
|
136
cli/src/lib.rs
136
cli/src/lib.rs
|
@ -1,136 +0,0 @@
|
|||
pub mod mbox {
|
||||
pub mod mbox;
|
||||
pub use mbox::*;
|
||||
|
||||
pub mod mbox_args;
|
||||
pub mod mbox_handlers;
|
||||
}
|
||||
|
||||
pub mod msg {
|
||||
pub mod envelope;
|
||||
pub use envelope::*;
|
||||
|
||||
pub mod msg_args;
|
||||
|
||||
pub mod msg_handlers;
|
||||
pub mod msg_utils;
|
||||
|
||||
pub mod flag_args;
|
||||
pub mod flag_handlers;
|
||||
|
||||
pub mod tpl_args;
|
||||
pub use tpl_args::TplOverride;
|
||||
|
||||
pub mod tpl_handlers;
|
||||
|
||||
pub mod msg_entity;
|
||||
pub use msg_entity::*;
|
||||
|
||||
pub mod parts_entity;
|
||||
pub use parts_entity::*;
|
||||
|
||||
pub mod addr_entity;
|
||||
pub use addr_entity::*;
|
||||
}
|
||||
|
||||
pub mod backends {
|
||||
pub mod backend;
|
||||
pub use backend::*;
|
||||
|
||||
pub mod id_mapper;
|
||||
pub use id_mapper::*;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub mod imap {
|
||||
pub mod imap_args;
|
||||
|
||||
pub mod imap_backend;
|
||||
pub use imap_backend::*;
|
||||
|
||||
pub mod imap_handlers;
|
||||
|
||||
pub mod imap_mbox;
|
||||
pub use imap_mbox::*;
|
||||
|
||||
pub mod imap_mbox_attr;
|
||||
pub use imap_mbox_attr::*;
|
||||
|
||||
pub mod imap_envelope;
|
||||
pub use imap_envelope::*;
|
||||
|
||||
pub mod imap_flag;
|
||||
pub use imap_flag::*;
|
||||
|
||||
pub mod msg_sort_criterion;
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub use self::imap::*;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub mod maildir {
|
||||
pub mod maildir_backend;
|
||||
pub use maildir_backend::*;
|
||||
|
||||
pub mod maildir_mbox;
|
||||
pub use maildir_mbox::*;
|
||||
|
||||
pub mod maildir_envelope;
|
||||
pub use maildir_envelope::*;
|
||||
|
||||
pub mod maildir_flag;
|
||||
pub use maildir_flag::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub use self::maildir::*;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub mod notmuch {
|
||||
pub mod notmuch_backend;
|
||||
pub use notmuch_backend::*;
|
||||
|
||||
pub mod notmuch_mbox;
|
||||
pub use notmuch_mbox::*;
|
||||
|
||||
pub mod notmuch_envelope;
|
||||
pub use notmuch_envelope::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub use self::notmuch::*;
|
||||
}
|
||||
|
||||
pub mod smtp {
|
||||
pub mod smtp_service;
|
||||
pub use smtp_service::*;
|
||||
}
|
||||
|
||||
pub mod config {
|
||||
pub mod deserialized_config;
|
||||
pub use deserialized_config::*;
|
||||
|
||||
pub mod deserialized_account_config;
|
||||
pub use deserialized_account_config::*;
|
||||
|
||||
pub mod config_args;
|
||||
|
||||
pub mod account_args;
|
||||
pub mod account_handlers;
|
||||
|
||||
pub mod account;
|
||||
pub use account::*;
|
||||
|
||||
pub mod account_config;
|
||||
pub use account_config::*;
|
||||
|
||||
pub mod format;
|
||||
pub use format::*;
|
||||
|
||||
pub mod hooks;
|
||||
pub use hooks::*;
|
||||
}
|
||||
|
||||
pub mod compl;
|
||||
pub mod output;
|
||||
pub mod ui;
|
347
cli/src/main.rs
347
cli/src/main.rs
|
@ -1,347 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use std::{convert::TryFrom, env};
|
||||
use url::Url;
|
||||
|
||||
use himalaya::{
|
||||
backends::Backend,
|
||||
compl::{compl_args, compl_handlers},
|
||||
config::{
|
||||
account_args, account_handlers, config_args, AccountConfig, BackendConfig,
|
||||
DeserializedConfig, DEFAULT_INBOX_FOLDER,
|
||||
},
|
||||
mbox::{mbox_args, mbox_handlers},
|
||||
msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers},
|
||||
output::{output_args, OutputFmt, StdoutPrinter},
|
||||
smtp::LettreService,
|
||||
};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya::backends::{imap_args, imap_handlers, ImapBackend};
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use himalaya::backends::MaildirBackend;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig};
|
||||
|
||||
fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||
let app = clap::App::new(env!("CARGO_PKG_NAME"))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
.global_setting(clap::AppSettings::GlobalVersion)
|
||||
.arg(&config_args::path_arg())
|
||||
.arg(&account_args::name_arg())
|
||||
.args(&output_args::args())
|
||||
.arg(mbox_args::source_arg())
|
||||
.subcommands(compl_args::subcmds())
|
||||
.subcommands(account_args::subcmds())
|
||||
.subcommands(mbox_args::subcmds())
|
||||
.subcommands(msg_args::subcmds());
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
let app = app.subcommands(imap_args::subcmds());
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
fn main() -> Result<()> {
|
||||
let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
|
||||
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
|
||||
|
||||
// Check mailto command BEFORE app initialization.
|
||||
let raw_args: Vec<String> = env::args().collect();
|
||||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
let config = DeserializedConfig::from_opt_path(None)?;
|
||||
let (account_config, backend_config) =
|
||||
AccountConfig::from_config_and_opt_account_name(&config, None)?;
|
||||
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
|
||||
let url = Url::parse(&raw_args[1])?;
|
||||
let mut smtp = LettreService::from(&account_config);
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
let mut imap;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
let mut maildir;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
let maildir_config: MaildirBackendConfig;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
let mut notmuch;
|
||||
|
||||
let backend: Box<&mut dyn Backend> = match backend_config {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
BackendConfig::Imap(ref imap_config) => {
|
||||
imap = ImapBackend::new(&account_config, imap_config);
|
||||
Box::new(&mut imap)
|
||||
}
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
BackendConfig::Maildir(ref maildir_config) => {
|
||||
maildir = MaildirBackend::new(&account_config, maildir_config);
|
||||
Box::new(&mut maildir)
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
BackendConfig::Notmuch(ref notmuch_config) => {
|
||||
maildir_config = MaildirBackendConfig {
|
||||
maildir_dir: notmuch_config.notmuch_database_dir.clone(),
|
||||
};
|
||||
maildir = MaildirBackend::new(&account_config, &maildir_config);
|
||||
notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?;
|
||||
Box::new(&mut notmuch)
|
||||
}
|
||||
};
|
||||
|
||||
return msg_handlers::mailto(&url, &account_config, &mut printer, backend, &mut smtp);
|
||||
}
|
||||
|
||||
let app = create_app();
|
||||
let m = app.get_matches();
|
||||
|
||||
// Check completion command BEFORE entities and services initialization.
|
||||
// Related issue: https://github.com/soywod/himalaya/issues/115.
|
||||
match compl_args::matches(&m)? {
|
||||
Some(compl_args::Command::Generate(shell)) => {
|
||||
return compl_handlers::generate(create_app(), shell);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Init entities and services.
|
||||
let config = DeserializedConfig::from_opt_path(m.value_of("config"))?;
|
||||
let (account_config, backend_config) =
|
||||
AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?;
|
||||
let mbox = m
|
||||
.value_of("mbox-source")
|
||||
.or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str()))
|
||||
.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
|
||||
#[cfg(feature = "imap-backend")]
|
||||
let mut imap;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
let mut maildir;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
let maildir_config: MaildirBackendConfig;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
let mut notmuch;
|
||||
|
||||
let backend: Box<&mut dyn Backend> = match backend_config {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
BackendConfig::Imap(ref imap_config) => {
|
||||
imap = ImapBackend::new(&account_config, imap_config);
|
||||
Box::new(&mut imap)
|
||||
}
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
BackendConfig::Maildir(ref maildir_config) => {
|
||||
maildir = MaildirBackend::new(&account_config, maildir_config);
|
||||
Box::new(&mut maildir)
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
BackendConfig::Notmuch(ref notmuch_config) => {
|
||||
maildir_config = MaildirBackendConfig {
|
||||
maildir_dir: notmuch_config.notmuch_database_dir.clone(),
|
||||
};
|
||||
maildir = MaildirBackend::new(&account_config, &maildir_config);
|
||||
notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?;
|
||||
Box::new(&mut notmuch)
|
||||
}
|
||||
};
|
||||
|
||||
let mut smtp = LettreService::from(&account_config);
|
||||
|
||||
// Check IMAP commands.
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
#[cfg(feature = "imap-backend")]
|
||||
if let BackendConfig::Imap(ref imap_config) = backend_config {
|
||||
let mut imap = ImapBackend::new(&account_config, imap_config);
|
||||
match imap_args::matches(&m)? {
|
||||
Some(imap_args::Command::Notify(keepalive)) => {
|
||||
return imap_handlers::notify(keepalive, mbox, &mut imap);
|
||||
}
|
||||
Some(imap_args::Command::Watch(keepalive)) => {
|
||||
return imap_handlers::watch(keepalive, mbox, &mut imap);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Check account commands.
|
||||
match account_args::matches(&m)? {
|
||||
Some(account_args::Cmd::List(max_width)) => {
|
||||
return account_handlers::list(max_width, &config, &account_config, &mut printer);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Check mailbox commands.
|
||||
match mbox_args::matches(&m)? {
|
||||
Some(mbox_args::Cmd::List(max_width)) => {
|
||||
return mbox_handlers::list(max_width, &account_config, &mut printer, backend);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Check message commands.
|
||||
match msg_args::matches(&m)? {
|
||||
Some(msg_args::Cmd::Attachments(seq)) => {
|
||||
return msg_handlers::attachments(seq, mbox, &account_config, &mut printer, backend);
|
||||
}
|
||||
Some(msg_args::Cmd::Copy(seq, mbox_dst)) => {
|
||||
return msg_handlers::copy(seq, mbox, mbox_dst, &mut printer, backend);
|
||||
}
|
||||
Some(msg_args::Cmd::Delete(seq)) => {
|
||||
return msg_handlers::delete(seq, mbox, &mut printer, backend);
|
||||
}
|
||||
Some(msg_args::Cmd::Forward(seq, attachment_paths, encrypt)) => {
|
||||
return msg_handlers::forward(
|
||||
seq,
|
||||
attachment_paths,
|
||||
encrypt,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
&mut smtp,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::List(max_width, page_size, page)) => {
|
||||
return msg_handlers::list(
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Move(seq, mbox_dst)) => {
|
||||
return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend);
|
||||
}
|
||||
Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => {
|
||||
return msg_handlers::read(
|
||||
seq,
|
||||
text_mime,
|
||||
raw,
|
||||
headers,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => {
|
||||
return msg_handlers::reply(
|
||||
seq,
|
||||
all,
|
||||
attachment_paths,
|
||||
encrypt,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
&mut smtp,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Save(raw_msg)) => {
|
||||
return msg_handlers::save(mbox, raw_msg, &mut printer, backend);
|
||||
}
|
||||
Some(msg_args::Cmd::Search(query, max_width, page_size, page)) => {
|
||||
return msg_handlers::search(
|
||||
query,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Sort(criteria, query, max_width, page_size, page)) => {
|
||||
return msg_handlers::sort(
|
||||
criteria,
|
||||
query,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Send(raw_msg)) => {
|
||||
return msg_handlers::send(raw_msg, &account_config, &mut printer, backend, &mut smtp);
|
||||
}
|
||||
Some(msg_args::Cmd::Write(atts, encrypt)) => {
|
||||
return msg_handlers::write(
|
||||
atts,
|
||||
encrypt,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
&mut smtp,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Flag(m)) => match m {
|
||||
Some(flag_args::Cmd::Set(seq_range, ref flags)) => {
|
||||
return flag_handlers::set(seq_range, flags, mbox, &mut printer, backend);
|
||||
}
|
||||
Some(flag_args::Cmd::Add(seq_range, ref flags)) => {
|
||||
return flag_handlers::add(seq_range, flags, mbox, &mut printer, backend);
|
||||
}
|
||||
Some(flag_args::Cmd::Remove(seq_range, ref flags)) => {
|
||||
return flag_handlers::remove(seq_range, flags, mbox, &mut printer, backend);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Some(msg_args::Cmd::Tpl(m)) => match m {
|
||||
Some(tpl_args::Cmd::New(tpl)) => {
|
||||
return tpl_handlers::new(tpl, &account_config, &mut printer);
|
||||
}
|
||||
Some(tpl_args::Cmd::Reply(seq, all, tpl)) => {
|
||||
return tpl_handlers::reply(
|
||||
seq,
|
||||
all,
|
||||
tpl,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(tpl_args::Cmd::Forward(seq, tpl)) => {
|
||||
return tpl_handlers::forward(
|
||||
seq,
|
||||
tpl,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(tpl_args::Cmd::Save(atts, tpl)) => {
|
||||
return tpl_handlers::save(mbox, &account_config, atts, tpl, &mut printer, backend);
|
||||
}
|
||||
Some(tpl_args::Cmd::Send(atts, tpl)) => {
|
||||
return tpl_handlers::send(
|
||||
mbox,
|
||||
&account_config,
|
||||
atts,
|
||||
tpl,
|
||||
&mut printer,
|
||||
backend,
|
||||
&mut smtp,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
backend.disconnect()
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
use std::fmt;
|
||||
|
||||
use crate::output::PrintTable;
|
||||
|
||||
pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable {
|
||||
//
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
//! Mailbox CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to the mailbox
|
||||
//! domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap;
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table_arg;
|
||||
|
||||
type MaxTableWidth = Option<usize>;
|
||||
|
||||
/// Represents the mailbox commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
/// Represents the list mailboxes command.
|
||||
List(MaxTableWidth),
|
||||
}
|
||||
|
||||
/// Defines the mailbox command matcher.
|
||||
pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
|
||||
info!("entering mailbox command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("mailboxes") {
|
||||
info!("mailboxes command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
return Ok(Some(Cmd::List(max_table_width)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Contains mailbox subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
vec![clap::SubCommand::with_name("mailboxes")
|
||||
.aliases(&["mailbox", "mboxes", "mbox", "mb", "m"])
|
||||
.about("Lists mailboxes")
|
||||
.arg(table_arg::max_width())]
|
||||
}
|
||||
|
||||
/// Defines the source mailbox argument.
|
||||
pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mbox-source")
|
||||
.short("m")
|
||||
.long("mailbox")
|
||||
.help("Specifies the source mailbox")
|
||||
.value_name("SOURCE")
|
||||
}
|
||||
|
||||
/// Defines the target mailbox argument.
|
||||
pub fn target_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mbox-target")
|
||||
.help("Specifies the targeted mailbox")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds() {
|
||||
let arg = clap::App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", "mailboxes"]);
|
||||
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
|
||||
|
||||
let arg = clap::App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", "mailboxes", "--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) => {
|
||||
clap::App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", $alias])
|
||||
.subcommand_name()
|
||||
};
|
||||
}
|
||||
|
||||
assert_eq!(Some("mailboxes"), get_matches_from!["mailboxes"]);
|
||||
assert_eq!(Some("mailboxes"), get_matches_from!["mboxes"]);
|
||||
assert_eq!(Some("mailboxes"), get_matches_from!["mbox"]);
|
||||
assert_eq!(Some("mailboxes"), get_matches_from!["mb"]);
|
||||
assert_eq!(Some("mailboxes"), get_matches_from!["m"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_source_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
clap::App::new("himalaya")
|
||||
.arg(source_arg())
|
||||
.get_matches_from(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(None, app.value_of("mbox-source"));
|
||||
|
||||
let app = get_matches_from!["-m", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
|
||||
|
||||
let app = get_matches_from!["--mailbox", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_target_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
clap::App::new("himalaya")
|
||||
.arg(target_arg())
|
||||
.get_matches_from_safe(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(
|
||||
clap::ErrorKind::MissingRequiredArgument,
|
||||
app.unwrap_err().kind
|
||||
);
|
||||
|
||||
let app = get_matches_from!["TARGET"];
|
||||
assert_eq!(Some("TARGET"), app.unwrap().value_of("mbox-target"));
|
||||
}
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
//! Mailbox handling module.
|
||||
//!
|
||||
//! This module gathers all mailbox actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::AccountConfig,
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
};
|
||||
|
||||
/// Lists all mailboxes.
|
||||
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
info!("entering list mailbox handler");
|
||||
let mboxes = backend.get_mboxes()?;
|
||||
trace!("mailboxes: {:?}", mboxes);
|
||||
printer.print_table(
|
||||
mboxes,
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
output::{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 PrinterService 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!()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestBackend;
|
||||
|
||||
impl<'a> Backend<'a> for TestBackend {
|
||||
fn add_mbox(&mut self, _: &str) -> Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
Ok(Box::new(ImapMboxes {
|
||||
mboxes: vec![
|
||||
ImapMbox {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||
},
|
||||
ImapMbox {
|
||||
delim: "/".into(),
|
||||
name: "Sent".into(),
|
||||
attrs: ImapMboxAttrs(vec![
|
||||
ImapMboxAttr::NoInferiors,
|
||||
ImapMboxAttr::Custom("HasNoChildren".into()),
|
||||
]),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
fn del_mbox(&mut self, _: &str) -> Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result<Box<dyn Envelopes>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result<Box<dyn ToString>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn get_msg(&mut self, _: &str, _: &str) -> Result<Msg> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn del_msg(&mut self, _: &str, _: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
let backend = Box::new(&mut backend);
|
||||
|
||||
assert!(list(None, &config, &mut printer, backend).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"DELIM │NAME │ATTRIBUTES \n",
|
||||
"/ │INBOX │NoSelect \n",
|
||||
"/ │Sent │NoInferiors, HasNoChildren \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
//! Module related to email addresses.
|
||||
//!
|
||||
//! This module regroups email address entities and converters.
|
||||
|
||||
use anyhow::Result;
|
||||
use mailparse;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Defines a single email address.
|
||||
pub type Addr = mailparse::MailAddr;
|
||||
|
||||
/// Defines a list of email addresses.
|
||||
pub type Addrs = mailparse::MailAddrList;
|
||||
|
||||
/// Converts a slice into an optional list of addresses.
|
||||
pub fn from_slice_to_addrs<S: AsRef<str> + Debug>(addrs: S) -> Result<Option<Addrs>> {
|
||||
let addrs = mailparse::addrparse(addrs.as_ref())?;
|
||||
Ok(if addrs.is_empty() { None } else { Some(addrs) })
|
||||
}
|
||||
|
||||
/// Converts a list of addresses into a list of [`lettre::message::Mailbox`].
|
||||
pub fn from_addrs_to_sendable_mbox(addrs: &Addrs) -> Result<Vec<lettre::message::Mailbox>> {
|
||||
let mut sendable_addrs: Vec<lettre::message::Mailbox> = vec![];
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => sendable_addrs.push(
|
||||
lettre::message::Mailbox::new(display_name.clone(), addr.parse()?),
|
||||
),
|
||||
Addr::Group(mailparse::GroupInfo { group_name, addrs }) => {
|
||||
for addr in addrs {
|
||||
sendable_addrs.push(lettre::message::Mailbox::new(
|
||||
addr.display_name.clone().or(Some(group_name.clone())),
|
||||
addr.to_string().parse()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(sendable_addrs)
|
||||
}
|
||||
|
||||
/// Converts a list of addresses into a list of [`lettre::Address`].
|
||||
pub fn from_addrs_to_sendable_addrs(addrs: &Addrs) -> Result<Vec<lettre::Address>> {
|
||||
let mut sendable_addrs = vec![];
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(mailparse::SingleInfo {
|
||||
display_name: _,
|
||||
addr,
|
||||
}) => {
|
||||
sendable_addrs.push(addr.parse()?);
|
||||
}
|
||||
mailparse::MailAddr::Group(mailparse::GroupInfo {
|
||||
group_name: _,
|
||||
addrs,
|
||||
}) => {
|
||||
for addr in addrs {
|
||||
sendable_addrs.push(addr.addr.parse()?);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(sendable_addrs)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
use std::{any, fmt};
|
||||
|
||||
use crate::output::PrintTable;
|
||||
|
||||
pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any {
|
||||
fn as_any(&self) -> &dyn any::Any;
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any> Envelopes for T {
|
||||
fn as_any(&self) -> &dyn any::Any {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
//! Message flag CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to the message flag
|
||||
//! domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::msg::msg_args;
|
||||
|
||||
type SeqRange<'a> = &'a str;
|
||||
type Flags = String;
|
||||
|
||||
/// Represents the flag commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
/// Represents the add flags command.
|
||||
Add(SeqRange<'a>, Flags),
|
||||
/// Represents the set flags command.
|
||||
Set(SeqRange<'a>, Flags),
|
||||
/// Represents the remove flags command.
|
||||
Remove(SeqRange<'a>, Flags),
|
||||
}
|
||||
|
||||
/// Defines the flag command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
info!("entering message flag command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("add") {
|
||||
info!("add subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: String = m
|
||||
.values_of("flags")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Cmd::Add(seq_range, flags)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("set") {
|
||||
info!("set subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: String = m
|
||||
.values_of("flags")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Cmd::Set(seq_range, flags)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("remove") {
|
||||
info!("remove subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: String = m
|
||||
.values_of("flags")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Cmd::Remove(seq_range, flags)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Defines the flags argument.
|
||||
fn flags_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("flags")
|
||||
.help("IMAP flags")
|
||||
.long_help("IMAP flags. Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
|
||||
.value_name("FLAGS…")
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Contains flag subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("flag")
|
||||
.aliases(&["flags", "flg"])
|
||||
.about("Handles flags")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("add")
|
||||
.aliases(&["a"])
|
||||
.about("Adds flags to a message")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("set")
|
||||
.aliases(&["s", "change", "c"])
|
||||
.about("Replaces all message flags")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("remove")
|
||||
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
|
||||
.about("Removes flags from a message")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
//! Message flag handling module.
|
||||
//!
|
||||
//! This module gathers all flag actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{backends::Backend, output::PrinterService};
|
||||
|
||||
/// 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: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.add_flags(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: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.del_flags(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: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.set_flags(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully set for message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
|
@ -1,472 +0,0 @@
|
|||
//! Module related to message CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to message.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
mbox::mbox_args,
|
||||
msg::{flag_args, msg_args, tpl_args},
|
||||
ui::table_arg,
|
||||
};
|
||||
|
||||
type Seq<'a> = &'a str;
|
||||
type PageSize = usize;
|
||||
type Page = usize;
|
||||
type Mbox<'a> = &'a str;
|
||||
type TextMime<'a> = &'a str;
|
||||
type Raw = bool;
|
||||
type All = bool;
|
||||
type RawMsg<'a> = &'a str;
|
||||
type Query = String;
|
||||
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||
type MaxTableWidth = Option<usize>;
|
||||
type Encrypt = bool;
|
||||
type Criteria = String;
|
||||
type Headers<'a> = Vec<&'a str>;
|
||||
|
||||
/// Message commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Attachments(Seq<'a>),
|
||||
Copy(Seq<'a>, Mbox<'a>),
|
||||
Delete(Seq<'a>),
|
||||
Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt),
|
||||
List(MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Seq<'a>, Mbox<'a>),
|
||||
Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>),
|
||||
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
|
||||
Save(RawMsg<'a>),
|
||||
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawMsg<'a>),
|
||||
Write(AttachmentPaths<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag_args::Cmd<'a>>),
|
||||
Tpl(Option<tpl_args::Cmd<'a>>),
|
||||
}
|
||||
|
||||
/// Message command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
info!("entering message command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("attachments") {
|
||||
info!("attachments command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
return Ok(Some(Cmd::Attachments(seq)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("copy") {
|
||||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
debug!(r#"target mailbox: "{:?}""#, mbox);
|
||||
return Ok(Some(Cmd::Copy(seq, mbox)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("delete") {
|
||||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
return Ok(Some(Cmd::Delete(seq)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("forward") {
|
||||
info!("forward command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
debug!("attachments paths: {:?}", paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Cmd::Forward(seq, paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("list") {
|
||||
info!("list command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap_or("1")
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
debug!("page: {}", page);
|
||||
return Ok(Some(Cmd::List(max_table_width, page_size, page)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("move") {
|
||||
info!("move command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
debug!("target mailbox: {:?}", mbox);
|
||||
return Ok(Some(Cmd::Move(seq, mbox)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("read") {
|
||||
info!("read command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mime = m.value_of("mime-type").unwrap();
|
||||
debug!("text mime: {}", mime);
|
||||
let raw = m.is_present("raw");
|
||||
debug!("raw: {}", raw);
|
||||
let headers: Vec<&str> = m.values_of("headers").unwrap_or_default().collect();
|
||||
debug!("headers: {:?}", headers);
|
||||
return Ok(Some(Cmd::Read(seq, mime, raw, headers)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("reply") {
|
||||
info!("reply command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
debug!("reply all: {}", all);
|
||||
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
debug!("attachments paths: {:?}", paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
|
||||
return Ok(Some(Cmd::Reply(seq, all, paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("save") {
|
||||
info!("save command matched");
|
||||
let msg = m.value_of("message").unwrap_or_default();
|
||||
trace!("message: {}", msg);
|
||||
return Ok(Some(Cmd::Save(msg)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("search") {
|
||||
info!("search command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
debug!("page: {}", page);
|
||||
let query = m
|
||||
.values_of("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(" ");
|
||||
debug!("query: {}", query);
|
||||
return Ok(Some(Cmd::Search(query, max_table_width, page_size, page)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("sort") {
|
||||
info!("sort command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
debug!("page: {:?}", page);
|
||||
let criteria = m
|
||||
.values_of("criterion")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("criteria: {:?}", criteria);
|
||||
let query = m
|
||||
.values_of("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(" ");
|
||||
debug!("query: {:?}", query);
|
||||
return Ok(Some(Cmd::Sort(
|
||||
criteria,
|
||||
query,
|
||||
max_table_width,
|
||||
page_size,
|
||||
page,
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("send") {
|
||||
info!("send command matched");
|
||||
let msg = m.value_of("message").unwrap_or_default();
|
||||
trace!("message: {}", msg);
|
||||
return Ok(Some(Cmd::Send(msg)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("write") {
|
||||
info!("write command matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
debug!("attachments paths: {:?}", attachment_paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Cmd::Write(attachment_paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("template") {
|
||||
return Ok(Some(Cmd::Tpl(tpl_args::matches(m)?)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("flag") {
|
||||
return Ok(Some(Cmd::Flag(flag_args::matches(m)?)));
|
||||
}
|
||||
|
||||
info!("default list command matched");
|
||||
Ok(Some(Cmd::List(None, None, 0)))
|
||||
}
|
||||
|
||||
/// Message sequence number argument.
|
||||
pub fn seq_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("seq")
|
||||
.help("Specifies the targetted message")
|
||||
.value_name("SEQ")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Message sequence range argument.
|
||||
pub fn seq_range_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("seq-range")
|
||||
.help("Specifies targetted message(s)")
|
||||
.long_help("Specifies a range of targetted messages. The range follows the [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501#section-9) format: `1:5` matches messages with sequence number between 1 and 5, `1,5` matches messages with sequence number 1 or 5, * matches all messages.")
|
||||
.value_name("SEQ")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Message reply all argument.
|
||||
pub fn reply_all_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("reply-all")
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
}
|
||||
|
||||
/// Message page size argument.
|
||||
fn page_size_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("page-size")
|
||||
.help("Page size")
|
||||
.short("s")
|
||||
.long("size")
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Message page argument.
|
||||
fn page_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("page")
|
||||
.help("Page number")
|
||||
.short("p")
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("0")
|
||||
}
|
||||
|
||||
/// Message attachment argument.
|
||||
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("attachments")
|
||||
.help("Adds attachment to the message")
|
||||
.short("a")
|
||||
.long("attachment")
|
||||
.value_name("PATH")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Represents the message headers argument.
|
||||
pub fn headers_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("headers")
|
||||
.help("Shows additional headers with the message")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("STR")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Message encrypt argument.
|
||||
pub fn encrypt_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("encrypt")
|
||||
.help("Encrypts the message")
|
||||
.short("e")
|
||||
.long("encrypt")
|
||||
}
|
||||
|
||||
/// Message subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![
|
||||
flag_args::subcmds(),
|
||||
tpl_args::subcmds(),
|
||||
vec![
|
||||
SubCommand::with_name("attachments")
|
||||
.aliases(&["attachment", "att", "a"])
|
||||
.about("Downloads all message attachments")
|
||||
.arg(msg_args::seq_arg()),
|
||||
SubCommand::with_name("list")
|
||||
.aliases(&["lst", "l"])
|
||||
.about("Lists all messages")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width()),
|
||||
SubCommand::with_name("search")
|
||||
.aliases(&["s", "query", "q"])
|
||||
.about("Lists messages matching the given IMAP query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width())
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.help("IMAP query")
|
||||
.long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.")
|
||||
.value_name("QUERY")
|
||||
.multiple(true)
|
||||
.required(true),
|
||||
),
|
||||
SubCommand::with_name("sort")
|
||||
.about("Sorts messages by the given criteria and matching the given IMAP query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width())
|
||||
.arg(
|
||||
Arg::with_name("criterion")
|
||||
.long("criterion")
|
||||
.short("c")
|
||||
.help("Defines the message 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",
|
||||
]),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.help("IMAP query")
|
||||
.long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.")
|
||||
.value_name("QUERY")
|
||||
.default_value("ALL")
|
||||
.raw(true),
|
||||
),
|
||||
SubCommand::with_name("write")
|
||||
.about("Writes a new message")
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("send")
|
||||
.about("Sends a raw message")
|
||||
.arg(Arg::with_name("message").raw(true)),
|
||||
SubCommand::with_name("save")
|
||||
.about("Saves a raw message")
|
||||
.arg(Arg::with_name("message").raw(true)),
|
||||
SubCommand::with_name("read")
|
||||
.about("Reads text bodies of a message")
|
||||
.arg(seq_arg())
|
||||
.arg(
|
||||
Arg::with_name("mime-type")
|
||||
.help("MIME type to use")
|
||||
.short("t")
|
||||
.long("mime-type")
|
||||
.value_name("MIME")
|
||||
.possible_values(&["plain", "html"])
|
||||
.default_value("plain"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("raw")
|
||||
.help("Reads raw message")
|
||||
.long("raw")
|
||||
.short("r"),
|
||||
)
|
||||
.arg(headers_arg()),
|
||||
SubCommand::with_name("reply")
|
||||
.aliases(&["rep", "r"])
|
||||
.about("Answers to a message")
|
||||
.arg(seq_arg())
|
||||
.arg(reply_all_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "f"])
|
||||
.about("Forwards a message")
|
||||
.arg(seq_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("copy")
|
||||
.aliases(&["cp", "c"])
|
||||
.about("Copies a message to the targetted mailbox")
|
||||
.arg(seq_arg())
|
||||
.arg(mbox_args::target_arg()),
|
||||
SubCommand::with_name("move")
|
||||
.aliases(&["mv"])
|
||||
.about("Moves a message to the targetted mailbox")
|
||||
.arg(seq_arg())
|
||||
.arg(mbox_args::target_arg()),
|
||||
SubCommand::with_name("delete")
|
||||
.aliases(&["del", "d", "remove", "rm"])
|
||||
.about("Deletes a message")
|
||||
.arg(seq_arg()),
|
||||
],
|
||||
]
|
||||
.concat()
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,379 +0,0 @@
|
|||
//! Module related to message handling.
|
||||
//!
|
||||
//! This module gathers all message commands.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use atty::Stream;
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::addrparse;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::{AccountConfig, DEFAULT_SENT_FOLDER},
|
||||
msg::{Msg, Part, Parts, TextPlainPart},
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
smtp::SmtpService,
|
||||
};
|
||||
|
||||
/// Downloads all message attachments to the user account downloads directory.
|
||||
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let attachments = backend.get_msg(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!(
|
||||
"Found {:?} attachment{} for message {:?}",
|
||||
attachments_len,
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
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(format!(
|
||||
"Attachment{} successfully downloaded to {:?}",
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
config.downloads_dir
|
||||
))
|
||||
}
|
||||
|
||||
/// Copy a message from a mailbox to another.
|
||||
pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
) -> Result<()> {
|
||||
backend.copy_msg(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully copied to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
}
|
||||
|
||||
/// Delete messages matching the given sequence range.
|
||||
pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.del_msg(mbox, seq)?;
|
||||
printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq))
|
||||
}
|
||||
|
||||
/// Forward the given message UID from the selected mailbox.
|
||||
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
seq: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List paginated messages from the selected mailbox.
|
||||
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
imap: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = imap.get_envelopes(mbox, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
PrintTableOpts {
|
||||
format: &config.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: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
url: &Url,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &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 = Msg {
|
||||
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(),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
msg.edit_with_editor(config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a message from a mailbox to another.
|
||||
pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.move_msg(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: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
text_mime: &str,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let msg = backend.get_msg(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: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?
|
||||
.add_flags(mbox, seq, "replied")
|
||||
}
|
||||
|
||||
/// Saves a raw message to the targetted mailbox.
|
||||
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
raw_msg: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
) -> Result<()> {
|
||||
info!("entering save message handler");
|
||||
|
||||
debug!("mailbox: {}", 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.add_msg(mbox, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paginate messages from the selected mailbox matching the specified query.
|
||||
pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria.
|
||||
pub fn sort<'a, P: PrinterService, 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: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a raw message.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
raw_msg: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
smtp: &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
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
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 = Msg::from_tpl(&raw_msg)?;
|
||||
smtp.send(&config, &msg)?;
|
||||
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose a new message.
|
||||
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
Msg::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::{debug, trace};
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
pub fn local_draft_path() -> PathBuf {
|
||||
let path = env::temp_dir().join("himalaya-draft.eml");
|
||||
trace!("local draft path: {:?}", path);
|
||||
path
|
||||
}
|
||||
|
||||
pub fn remove_local_draft() -> Result<()> {
|
||||
let path = local_draft_path();
|
||||
debug!("remove draft path at {:?}", path);
|
||||
fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path))
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use mailparse::MailHeaderMap;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
env, fs,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::AccountConfig;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextPlainPart {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextHtmlPart {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct BinaryPart {
|
||||
pub filename: String,
|
||||
pub mime: String,
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Part {
|
||||
TextPlain(TextPlainPart),
|
||||
TextHtml(TextHtmlPart),
|
||||
Binary(BinaryPart),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn new_text_plain(content: String) -> Self {
|
||||
Self::TextPlain(TextPlainPart { content })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Parts(pub Vec<Part>);
|
||||
|
||||
impl Parts {
|
||||
pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) {
|
||||
self.retain(|part| !matches!(part, Part::TextPlain(_)));
|
||||
self.push(Part::TextPlain(part));
|
||||
}
|
||||
|
||||
pub fn from_parsed_mail<'a>(
|
||||
account: &'a AccountConfig,
|
||||
part: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Result<Self> {
|
||||
let mut parts = vec![];
|
||||
build_parts_map_rec(account, part, &mut parts)?;
|
||||
Ok(Self(parts))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Parts {
|
||||
type Target = Vec<Part>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Parts {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn build_parts_map_rec(
|
||||
account: &AccountConfig,
|
||||
parsed_mail: &mailparse::ParsedMail,
|
||||
parts: &mut Vec<Part>,
|
||||
) -> Result<()> {
|
||||
if parsed_mail.subparts.is_empty() {
|
||||
let cdisp = parsed_mail.get_content_disposition();
|
||||
match cdisp.disposition {
|
||||
mailparse::DispositionType::Attachment => {
|
||||
let filename = cdisp
|
||||
.params
|
||||
.get("filename")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| String::from("noname"));
|
||||
let content = parsed_mail.get_body_raw().unwrap_or_default();
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
parts.push(Part::Binary(BinaryPart {
|
||||
filename,
|
||||
mime,
|
||||
content,
|
||||
}));
|
||||
}
|
||||
// TODO: manage other use cases
|
||||
_ => {
|
||||
if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") {
|
||||
let content = parsed_mail.get_body().unwrap_or_default();
|
||||
if ctype.starts_with("text/plain") {
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else if ctype.starts_with("text/html") {
|
||||
parts.push(Part::TextHtml(TextHtmlPart { content }))
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let ctype = parsed_mail
|
||||
.get_headers()
|
||||
.get_first_value("content-type")
|
||||
.ok_or_else(|| anyhow!("cannot get content type of multipart"))?;
|
||||
if ctype.starts_with("multipart/encrypted") {
|
||||
let decrypted_part = parsed_mail
|
||||
.subparts
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("cannot find encrypted part of multipart"))
|
||||
.and_then(|part| decrypt_part(account, part))
|
||||
.context("cannot decrypt part of multipart")?;
|
||||
let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes())
|
||||
.context("cannot parse decrypted part of multipart")?;
|
||||
build_parts_map_rec(account, &parsed_mail, parts)?;
|
||||
} else {
|
||||
for part in parsed_mail.subparts.iter() {
|
||||
build_parts_map_rec(account, part, parts)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result<String> {
|
||||
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
|
||||
let msg_body = msg
|
||||
.get_body()
|
||||
.context("cannot get body from encrypted part")?;
|
||||
fs::write(msg_path.clone(), &msg_body)
|
||||
.context(format!("cannot write encrypted part to temporary file"))?;
|
||||
account
|
||||
.pgp_decrypt_file(msg_path.clone())?
|
||||
.ok_or_else(|| anyhow!("cannot find pgp decrypt command in config"))
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
//! Module related to message template CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to message template.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::msg::msg_args;
|
||||
|
||||
type Seq<'a> = &'a str;
|
||||
type ReplyAll = bool;
|
||||
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||
type Tpl<'a> = &'a str;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct TplOverride<'a> {
|
||||
pub subject: Option<&'a str>,
|
||||
pub from: Option<Vec<&'a str>>,
|
||||
pub to: Option<Vec<&'a str>>,
|
||||
pub cc: Option<Vec<&'a str>>,
|
||||
pub bcc: Option<Vec<&'a str>>,
|
||||
pub headers: Option<Vec<&'a str>>,
|
||||
pub body: Option<&'a str>,
|
||||
pub sig: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
|
||||
fn from(matches: &'a ArgMatches<'a>) -> Self {
|
||||
Self {
|
||||
subject: matches.value_of("subject"),
|
||||
from: matches.values_of("from").map(|v| v.collect()),
|
||||
to: matches.values_of("to").map(|v| v.collect()),
|
||||
cc: matches.values_of("cc").map(|v| v.collect()),
|
||||
bcc: matches.values_of("bcc").map(|v| v.collect()),
|
||||
headers: matches.values_of("headers").map(|v| v.collect()),
|
||||
body: matches.value_of("body"),
|
||||
sig: matches.value_of("signature"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message template commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
New(TplOverride<'a>),
|
||||
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
|
||||
Forward(Seq<'a>, TplOverride<'a>),
|
||||
Save(AttachmentPaths<'a>, Tpl<'a>),
|
||||
Send(AttachmentPaths<'a>, Tpl<'a>),
|
||||
}
|
||||
|
||||
/// Message template command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
info!("entering message template command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("new") {
|
||||
info!("new subcommand matched");
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Cmd::New(tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("reply") {
|
||||
info!("reply subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
debug!("reply all: {}", all);
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Cmd::Reply(seq, all, tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("forward") {
|
||||
info!("forward subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template args: {:?}", tpl);
|
||||
return Ok(Some(Cmd::Forward(seq, tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("save") {
|
||||
info!("save subcommand matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
let tpl = m.value_of("template").unwrap_or_default();
|
||||
trace!("template: {}", tpl);
|
||||
return Ok(Some(Cmd::Save(attachment_paths, tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("send") {
|
||||
info!("send subcommand matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
let tpl = m.value_of("template").unwrap_or_default();
|
||||
trace!("template: {}", tpl);
|
||||
return Ok(Some(Cmd::Send(attachment_paths, tpl)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Message template args.
|
||||
pub fn tpl_args<'a>() -> Vec<Arg<'a, 'a>> {
|
||||
vec![
|
||||
Arg::with_name("subject")
|
||||
.help("Overrides the Subject header")
|
||||
.short("s")
|
||||
.long("subject")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name("from")
|
||||
.help("Overrides the From header")
|
||||
.short("f")
|
||||
.long("from")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("to")
|
||||
.help("Overrides the To header")
|
||||
.short("t")
|
||||
.long("to")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("cc")
|
||||
.help("Overrides the Cc header")
|
||||
.short("c")
|
||||
.long("cc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("bcc")
|
||||
.help("Overrides the Bcc header")
|
||||
.short("b")
|
||||
.long("bcc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("header")
|
||||
.help("Overrides a specific header")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("KEY: VAL")
|
||||
.multiple(true),
|
||||
Arg::with_name("body")
|
||||
.help("Overrides the body")
|
||||
.short("B")
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name("signature")
|
||||
.help("Overrides the signature")
|
||||
.short("S")
|
||||
.long("signature")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Message template subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("template")
|
||||
.aliases(&["tpl"])
|
||||
.about("Generates a message template")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("new")
|
||||
.aliases(&["n"])
|
||||
.about("Generates a new message template")
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("reply")
|
||||
.aliases(&["rep", "re", "r"])
|
||||
.about("Generates a reply message template")
|
||||
.arg(msg_args::seq_arg())
|
||||
.arg(msg_args::reply_all_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "fw", "f"])
|
||||
.about("Generates a forward message template")
|
||||
.arg(msg_args::seq_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("save")
|
||||
.about("Saves a message based on the given template")
|
||||
.arg(&msg_args::attachments_arg())
|
||||
.arg(Arg::with_name("template").raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("send")
|
||||
.about("Sends a message based on the given template")
|
||||
.arg(&msg_args::attachments_arg())
|
||||
.arg(Arg::with_name("template").raw(true)),
|
||||
)]
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
//! Module related to message template handling.
|
||||
//!
|
||||
//! This module gathers all message template commands.
|
||||
|
||||
use anyhow::Result;
|
||||
use atty::Stream;
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::AccountConfig,
|
||||
msg::{Msg, TplOverride},
|
||||
output::PrinterService,
|
||||
smtp::SmtpService,
|
||||
};
|
||||
|
||||
/// Generate a new message template.
|
||||
pub fn new<'a, P: PrinterService>(
|
||||
opts: TplOverride<'a>,
|
||||
account: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
) -> Result<()> {
|
||||
let tpl = Msg::default().to_tpl(opts, account)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a reply message template.
|
||||
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
opts: TplOverride<'a>,
|
||||
mbox: &str,
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a forward message template.
|
||||
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
opts: TplOverride<'a>,
|
||||
mbox: &str,
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.get_msg(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: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&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 = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let raw_msg = msg.into_sendable_msg(config)?.formatted();
|
||||
backend.add_msg(mbox, &raw_msg, "seen")?;
|
||||
printer.print_struct("Template successfully saved")
|
||||
}
|
||||
|
||||
/// Sends a message based on a template.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
mbox: &str,
|
||||
account: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
smtp: &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 = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let sent_msg = smtp.send(account, &msg)?;
|
||||
backend.add_msg(mbox, &sent_msg, "seen")?;
|
||||
printer.print_struct("Template successfully sent")
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
//! Module related to output formatting and printing.
|
||||
|
||||
pub mod output_args;
|
||||
|
||||
pub mod output_utils;
|
||||
pub use output_utils::*;
|
||||
|
||||
pub mod output_entity;
|
||||
pub use output_entity::*;
|
||||
|
||||
pub mod print;
|
||||
pub use print::*;
|
||||
|
||||
pub mod print_table;
|
||||
pub use print_table::*;
|
||||
|
||||
pub mod printer_service;
|
||||
pub use printer_service::*;
|
|
@ -1,26 +0,0 @@
|
|||
//! Module related to output CLI.
|
||||
//!
|
||||
//! This module provides arguments related to output.
|
||||
|
||||
use clap::Arg;
|
||||
|
||||
/// Output arguments.
|
||||
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
|
||||
vec![
|
||||
Arg::with_name("output")
|
||||
.long("output")
|
||||
.short("o")
|
||||
.help("Defines the output format")
|
||||
.value_name("FMT")
|
||||
.possible_values(&["plain", "json"])
|
||||
.default_value("plain"),
|
||||
Arg::with_name("log-level")
|
||||
.long("log-level")
|
||||
.alias("log")
|
||||
.short("l")
|
||||
.help("Defines the logs level")
|
||||
.value_name("LEVEL")
|
||||
.possible_values(&["error", "warn", "info", "debug", "trace"])
|
||||
.default_value("info"),
|
||||
]
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{convert::TryFrom, fmt};
|
||||
|
||||
/// Represents the available output formats.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum OutputFmt {
|
||||
Plain,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl From<&str> for OutputFmt {
|
||||
fn from(fmt: &str) -> Self {
|
||||
match fmt {
|
||||
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
|
||||
_ => Self::Plain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Option<&str>> for OutputFmt {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> {
|
||||
match fmt {
|
||||
Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
|
||||
Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
|
||||
None => Ok(Self::Plain),
|
||||
Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for OutputFmt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let fmt = match *self {
|
||||
OutputFmt::Json => "JSON",
|
||||
OutputFmt::Plain => "Plain",
|
||||
};
|
||||
write!(f, "{}", fmt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines a struct-wrapper to provide a JSON output.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct OutputJson<T: serde::Serialize> {
|
||||
response: T,
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize> OutputJson<T> {
|
||||
pub fn new(response: T) -> Self {
|
||||
Self { response }
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use log::debug;
|
||||
use std::{
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// TODO: move this in a more approriate place.
|
||||
pub fn run_cmd(cmd: &str) -> Result<String> {
|
||||
debug!("running command: {}", cmd);
|
||||
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd").args(&["/C", cmd]).output()
|
||||
} else {
|
||||
Command::new("sh").arg("-c").arg(cmd).output()
|
||||
}?;
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
}
|
||||
|
||||
pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("cannot spawn process from command {:?}", cmd))?;
|
||||
process
|
||||
.stdin
|
||||
.ok_or_else(|| anyhow!("cannot get stdin"))?
|
||||
.write_all(data)
|
||||
.with_context(|| "cannot write data to stdin")?;
|
||||
process
|
||||
.stdout
|
||||
.ok_or_else(|| anyhow!("cannot get stdout"))?
|
||||
.read_to_end(&mut res)
|
||||
.with_context(|| "cannot read data from stdout")?;
|
||||
|
||||
Ok(res)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::output::WriteColor;
|
||||
|
||||
pub trait Print {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()>;
|
||||
}
|
||||
|
||||
impl Print for &str {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
writeln!(writer, "{}", self).context("cannot write string to writer")
|
||||
}
|
||||
}
|
||||
|
||||
impl Print for String {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
self.as_str().print(writer)
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use std::io;
|
||||
use termcolor::{self, StandardStream};
|
||||
|
||||
use crate::config::Format;
|
||||
|
||||
pub trait WriteColor: io::Write + termcolor::WriteColor {}
|
||||
|
||||
impl WriteColor for StandardStream {}
|
||||
|
||||
pub trait PrintTable {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct PrintTableOpts<'a> {
|
||||
pub format: &'a Format,
|
||||
pub max_width: Option<usize>,
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
use anyhow::{Context, Error, Result};
|
||||
use atty::Stream;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Debug},
|
||||
};
|
||||
use termcolor::{ColorChoice, StandardStream};
|
||||
|
||||
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
|
||||
|
||||
pub trait PrinterService {
|
||||
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()>;
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
|
||||
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()>;
|
||||
fn is_json(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct StdoutPrinter {
|
||||
pub writer: Box<dyn WriteColor>,
|
||||
pub fmt: OutputFmt,
|
||||
}
|
||||
|
||||
impl PrinterService for StdoutPrinter {
|
||||
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print(self.writer.as_mut()),
|
||||
OutputFmt::Json => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print(self.writer.as_mut()),
|
||||
OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &OutputJson::new(data))
|
||||
.context("cannot write JSON to writer"),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print_table(self.writer.as_mut(), opts),
|
||||
OutputFmt::Json => {
|
||||
let json = &mut serde_json::Serializer::new(self.writer.as_mut());
|
||||
let ser = &mut <dyn erased_serde::Serializer>::erase(json);
|
||||
data.erased_serialize(ser).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_json(&self) -> bool {
|
||||
self.fmt == OutputFmt::Json
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutputFmt> for StdoutPrinter {
|
||||
fn from(fmt: OutputFmt) -> Self {
|
||||
let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) {
|
||||
// Colors should be deactivated if the terminal is not a tty.
|
||||
ColorChoice::Never
|
||||
} else {
|
||||
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
|
||||
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
|
||||
// - If `TERM` is set to dumb, then colors will be suppressed.
|
||||
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
|
||||
//
|
||||
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
|
||||
ColorChoice::Auto
|
||||
});
|
||||
let writer = Box::new(writer);
|
||||
Self { writer, fmt }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Option<&str>> for StdoutPrinter {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fmt: Option<&str>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
fmt: OutputFmt::try_from(fmt)?,
|
||||
..Self::from(OutputFmt::Plain)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
//! Module related to SMTP.
|
|
@ -1,85 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use lettre::{
|
||||
self,
|
||||
transport::smtp::{
|
||||
client::{Tls, TlsParameters},
|
||||
SmtpTransport,
|
||||
},
|
||||
Transport,
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd};
|
||||
|
||||
pub trait SmtpService {
|
||||
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub struct LettreService<'a> {
|
||||
account: &'a AccountConfig,
|
||||
transport: Option<SmtpTransport>,
|
||||
}
|
||||
|
||||
impl LettreService<'_> {
|
||||
fn transport(&mut self) -> Result<&SmtpTransport> {
|
||||
if let Some(ref transport) = self.transport {
|
||||
Ok(transport)
|
||||
} else {
|
||||
let builder = if self.account.smtp_starttls {
|
||||
SmtpTransport::starttls_relay(&self.account.smtp_host)
|
||||
} else {
|
||||
SmtpTransport::relay(&self.account.smtp_host)
|
||||
}?;
|
||||
|
||||
let tls = TlsParameters::builder(self.account.smtp_host.to_owned())
|
||||
.dangerous_accept_invalid_hostnames(self.account.smtp_insecure)
|
||||
.dangerous_accept_invalid_certs(self.account.smtp_insecure)
|
||||
.build()?;
|
||||
let tls = if self.account.smtp_starttls {
|
||||
Tls::Required(tls)
|
||||
} else {
|
||||
Tls::Wrapper(tls)
|
||||
};
|
||||
|
||||
self.transport = Some(
|
||||
builder
|
||||
.tls(tls)
|
||||
.port(self.account.smtp_port)
|
||||
.credentials(self.account.smtp_creds()?)
|
||||
.build(),
|
||||
);
|
||||
|
||||
Ok(self.transport.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpService for LettreService<'_> {
|
||||
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>> {
|
||||
let mut raw_msg = msg.into_sendable_msg(account)?.formatted();
|
||||
|
||||
let envelope: lettre::address::Envelope =
|
||||
if let Some(cmd) = account.hooks.pre_send.as_deref() {
|
||||
for cmd in cmd.split('|') {
|
||||
raw_msg = pipe_cmd(cmd.trim(), &raw_msg)
|
||||
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?
|
||||
}
|
||||
let parsed_mail = mailparse::parse_mail(&raw_msg)?;
|
||||
Msg::from_parsed_mail(parsed_mail, account)?.try_into()
|
||||
} else {
|
||||
msg.try_into()
|
||||
}?;
|
||||
|
||||
self.transport()?.send_raw(&envelope, &raw_msg)?;
|
||||
Ok(raw_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
|
||||
fn from(account: &'a AccountConfig) -> Self {
|
||||
Self {
|
||||
account,
|
||||
transport: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, error};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub enum PreEditChoice {
|
||||
Edit,
|
||||
Discard,
|
||||
Quit,
|
||||
}
|
||||
|
||||
pub fn pre_edit() -> Result<PreEditChoice> {
|
||||
println!("A draft was found:");
|
||||
print!("(e)dit, (d)iscard or (q)uit? ");
|
||||
io::stdout().flush().context("cannot flush stdout")?;
|
||||
|
||||
let mut buf = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut buf)
|
||||
.context("cannot read stdin")?;
|
||||
|
||||
match buf.bytes().next().map(|bytes| bytes as char) {
|
||||
Some('e') => {
|
||||
debug!("edit choice matched");
|
||||
Ok(PreEditChoice::Edit)
|
||||
}
|
||||
Some('d') => {
|
||||
debug!("discard choice matched");
|
||||
Ok(PreEditChoice::Discard)
|
||||
}
|
||||
Some('q') => {
|
||||
debug!("quit choice matched");
|
||||
Ok(PreEditChoice::Quit)
|
||||
}
|
||||
Some(choice) => {
|
||||
error!(r#"invalid choice "{}""#, choice);
|
||||
Err(anyhow!(r#"invalid choice "{}""#, choice))
|
||||
}
|
||||
None => {
|
||||
error!("empty choice");
|
||||
Err(anyhow!("empty choice"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PostEditChoice {
|
||||
Send,
|
||||
Edit,
|
||||
LocalDraft,
|
||||
RemoteDraft,
|
||||
Discard,
|
||||
}
|
||||
|
||||
pub fn post_edit() -> Result<PostEditChoice> {
|
||||
print!("(s)end, (e)dit, (l)ocal/(r)emote draft or (d)iscard? ");
|
||||
io::stdout().flush().context("cannot flush stdout")?;
|
||||
|
||||
let mut buf = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut buf)
|
||||
.context("cannot read stdin")?;
|
||||
|
||||
match buf.bytes().next().map(|bytes| bytes as char) {
|
||||
Some('s') => {
|
||||
debug!("send choice matched");
|
||||
Ok(PostEditChoice::Send)
|
||||
}
|
||||
Some('l') => {
|
||||
debug!("save local draft choice matched");
|
||||
Ok(PostEditChoice::LocalDraft)
|
||||
}
|
||||
Some('r') => {
|
||||
debug!("save remote draft matched");
|
||||
Ok(PostEditChoice::RemoteDraft)
|
||||
}
|
||||
Some('e') => {
|
||||
debug!("edit choice matched");
|
||||
Ok(PostEditChoice::Edit)
|
||||
}
|
||||
Some('d') => {
|
||||
debug!("discard choice matched");
|
||||
Ok(PostEditChoice::Discard)
|
||||
}
|
||||
Some(choice) => {
|
||||
error!(r#"invalid choice "{}""#, choice);
|
||||
Err(anyhow!(r#"invalid choice "{}""#, choice))
|
||||
}
|
||||
None => {
|
||||
error!("empty choice");
|
||||
Err(anyhow!("empty choice"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::debug;
|
||||
use std::{env, fs, process::Command};
|
||||
|
||||
use crate::msg::msg_utils;
|
||||
|
||||
pub fn open_with_tpl(tpl: String) -> Result<String> {
|
||||
let path = msg_utils::local_draft_path();
|
||||
|
||||
debug!("create draft");
|
||||
fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?;
|
||||
|
||||
debug!("open editor");
|
||||
Command::new(env::var("EDITOR").context(r#"cannot find "$EDITOR" env var"#)?)
|
||||
.arg(&path)
|
||||
.status()
|
||||
.context("cannot launch editor")?;
|
||||
|
||||
debug!("read draft");
|
||||
let content =
|
||||
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub fn open_with_draft() -> Result<String> {
|
||||
let path = msg_utils::local_draft_path();
|
||||
let tpl =
|
||||
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
|
||||
open_with_tpl(tpl)
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
//! Module related to User Interface.
|
||||
|
||||
pub mod table_arg;
|
||||
|
||||
pub mod table;
|
||||
pub use table::*;
|
||||
|
||||
pub mod choice;
|
||||
pub mod editor;
|
|
@ -1,445 +0,0 @@
|
|||
//! Toolbox for building responsive tables.
|
||||
//! A table is composed of rows, a row is composed of cells.
|
||||
//! The toolbox uses the [builder design pattern].
|
||||
//!
|
||||
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::trace;
|
||||
use termcolor::{Color, ColorSpec};
|
||||
use terminal_size;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
config::Format,
|
||||
output::{Print, PrintTableOpts, WriteColor},
|
||||
};
|
||||
|
||||
/// Defines the default terminal size.
|
||||
/// This is used when the size cannot be determined by the `terminal_size` crate.
|
||||
/// TODO: make this customizable.
|
||||
pub const DEFAULT_TERM_WIDTH: usize = 80;
|
||||
|
||||
/// Defines the minimum size of a shrinked cell.
|
||||
/// TODO: make this customizable.
|
||||
pub const MAX_SHRINK_WIDTH: usize = 5;
|
||||
|
||||
/// Represents a cell in a table.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Cell {
|
||||
/// Represents the style of the cell.
|
||||
style: ColorSpec,
|
||||
/// Represents the content of the cell.
|
||||
value: String,
|
||||
/// (Dis)allowes the cell to shrink when the table exceeds the container width.
|
||||
shrinkable: bool,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
pub fn new<T: AsRef<str>>(value: T) -> Self {
|
||||
Self {
|
||||
// Removes carriage returns, new line feeds, tabulations
|
||||
// and [variation selectors].
|
||||
//
|
||||
// [variation selectors]: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
|
||||
value: String::from(value.as_ref()).replace(
|
||||
|c| ['\r', '\n', '\t', '\u{fe0e}', '\u{fe0f}'].contains(&c),
|
||||
"",
|
||||
),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unicode width of the cell's value.
|
||||
pub fn unicode_width(&self) -> usize {
|
||||
UnicodeWidthStr::width(self.value.as_str())
|
||||
}
|
||||
|
||||
/// Makes the cell shrinkable. If the table exceeds the terminal width, this cell will be the
|
||||
/// one to shrink in order to prevent the table to overflow.
|
||||
pub fn shrinkable(mut self) -> Self {
|
||||
self.shrinkable = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the shrinkable state of a cell.
|
||||
pub fn is_shrinkable(&self) -> bool {
|
||||
self.shrinkable
|
||||
}
|
||||
|
||||
/// Applies the bold style to the cell.
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.style.set_bold(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the bold style to the cell conditionally.
|
||||
pub fn bold_if(self, predicate: bool) -> Self {
|
||||
if predicate {
|
||||
self.bold()
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the underline style to the cell.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.style.set_underline(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the red color to the cell.
|
||||
pub fn red(mut self) -> Self {
|
||||
self.style.set_fg(Some(Color::Red));
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the green color to the cell.
|
||||
pub fn green(mut self) -> Self {
|
||||
self.style.set_fg(Some(Color::Green));
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the yellow color to the cell.
|
||||
pub fn yellow(mut self) -> Self {
|
||||
self.style.set_fg(Some(Color::Yellow));
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the blue color to the cell.
|
||||
pub fn blue(mut self) -> Self {
|
||||
self.style.set_fg(Some(Color::Blue));
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the white color to the cell.
|
||||
pub fn white(mut self) -> Self {
|
||||
self.style.set_fg(Some(Color::White));
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the custom ansi color to the cell.
|
||||
pub fn ansi_256(mut self, code: u8) -> Self {
|
||||
self.style.set_fg(Some(Color::Ansi256(code)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the cell printable.
|
||||
impl Print for Cell {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
// Applies colors to the cell
|
||||
writer
|
||||
.set_color(&self.style)
|
||||
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
|
||||
|
||||
// Writes the colorized cell to stdout
|
||||
write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a row in a table.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Row(
|
||||
/// Represents a list of cells.
|
||||
pub Vec<Cell>,
|
||||
);
|
||||
|
||||
impl Row {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn cell(mut self, cell: Cell) -> Self {
|
||||
self.0.push(cell);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a table abstraction.
|
||||
pub trait Table
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
/// Defines the header row.
|
||||
fn head() -> Row;
|
||||
|
||||
/// Defines the row template.
|
||||
fn row(&self) -> Row;
|
||||
|
||||
/// Writes the table to the writer.
|
||||
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
|
||||
let is_format_flowed = matches!(opts.format, Format::Flowed);
|
||||
let max_width = match opts.format {
|
||||
Format::Fixed(width) => opts.max_width.unwrap_or(*width),
|
||||
Format::Flowed => 0,
|
||||
Format::Auto => opts
|
||||
.max_width
|
||||
.or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize))
|
||||
.unwrap_or(DEFAULT_TERM_WIDTH),
|
||||
};
|
||||
let mut table = vec![Self::head()];
|
||||
let mut cell_widths: Vec<usize> =
|
||||
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
|
||||
table.extend(
|
||||
items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let row = item.row();
|
||||
row.0.iter().enumerate().for_each(|(i, cell)| {
|
||||
cell_widths[i] = cell_widths[i].max(cell.unicode_width());
|
||||
});
|
||||
row
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
trace!("cell widths: {:?}", cell_widths);
|
||||
|
||||
let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
|
||||
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
|
||||
trace!("table width: {}", table_width);
|
||||
|
||||
for row in table.iter_mut() {
|
||||
let mut glue = Cell::default();
|
||||
for (i, cell) in row.0.iter_mut().enumerate() {
|
||||
glue.print(writer)?;
|
||||
|
||||
let table_is_overflowing = table_width > max_width;
|
||||
if table_is_overflowing && !is_format_flowed && cell.is_shrinkable() {
|
||||
trace!("table is overflowing and cell is shrinkable");
|
||||
|
||||
let shrink_width = table_width - max_width;
|
||||
trace!("shrink width: {}", shrink_width);
|
||||
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
|
||||
cell_widths[i] - shrink_width
|
||||
} else {
|
||||
MAX_SHRINK_WIDTH
|
||||
};
|
||||
trace!("cell width: {}", cell_width);
|
||||
trace!("cell unicode width: {}", cell.unicode_width());
|
||||
|
||||
let cell_is_overflowing = cell.unicode_width() > cell_width;
|
||||
if cell_is_overflowing {
|
||||
trace!("cell is overflowing");
|
||||
|
||||
let mut value = String::new();
|
||||
let mut chars_width = 0;
|
||||
|
||||
for c in cell.value.chars() {
|
||||
let char_width = UnicodeWidthStr::width(c.to_string().as_str());
|
||||
if chars_width + char_width >= cell_width {
|
||||
break;
|
||||
}
|
||||
|
||||
chars_width += char_width;
|
||||
value.push(c);
|
||||
}
|
||||
|
||||
value.push_str("… ");
|
||||
trace!("chars width: {}", chars_width);
|
||||
trace!("shrinked value: {}", value);
|
||||
let spaces_count = cell_width - chars_width - 1;
|
||||
trace!("number of spaces added to shrinked value: {}", spaces_count);
|
||||
value.push_str(&" ".repeat(spaces_count));
|
||||
cell.value = value;
|
||||
} else {
|
||||
trace!("cell is not overflowing");
|
||||
let spaces_count = cell_width - cell.unicode_width() + 1;
|
||||
trace!("number of spaces added to value: {}", spaces_count);
|
||||
cell.value.push_str(&" ".repeat(spaces_count));
|
||||
}
|
||||
} else {
|
||||
trace!("table is not overflowing or cell is not shrinkable");
|
||||
trace!("cell width: {}", cell_widths[i]);
|
||||
trace!("cell unicode width: {}", cell.unicode_width());
|
||||
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
|
||||
trace!("number of spaces added to value: {}", spaces_count);
|
||||
cell.value.push_str(&" ".repeat(spaces_count));
|
||||
}
|
||||
cell.print(writer)?;
|
||||
glue = Cell::new("│").ansi_256(8);
|
||||
}
|
||||
writeln!(writer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
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 {}
|
||||
|
||||
struct Item {
|
||||
id: u16,
|
||||
name: String,
|
||||
desc: String,
|
||||
}
|
||||
|
||||
impl<'a> Item {
|
||||
pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: String::from(name),
|
||||
desc: String::from(desc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Item {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID"))
|
||||
.cell(Cell::new("NAME").shrinkable())
|
||||
.cell(Cell::new("DESC"))
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(self.id.to_string()))
|
||||
.cell(Cell::new(self.name.as_str()).shrinkable())
|
||||
.cell(Cell::new(self.desc.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! write_items {
|
||||
($writer:expr, $($item:expr),*) => {
|
||||
Table::print($writer, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap();
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_smaller_than_head() {
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writer,
|
||||
Item::new(1, "a", "aa"),
|
||||
Item::new(2, "b", "bb"),
|
||||
Item::new(3, "c", "cc")
|
||||
];
|
||||
|
||||
let expected = concat![
|
||||
"ID │NAME │DESC \n",
|
||||
"1 │a │aa \n",
|
||||
"2 │b │bb \n",
|
||||
"3 │c │cc \n",
|
||||
];
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_bigger_than_head() {
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writer,
|
||||
Item::new(1, "a", "aa"),
|
||||
Item::new(2222, "bbbbb", "bbbbb"),
|
||||
Item::new(3, "c", "cc")
|
||||
];
|
||||
|
||||
let expected = concat![
|
||||
"ID │NAME │DESC \n",
|
||||
"1 │a │aa \n",
|
||||
"2222 │bbbbb │bbbbb \n",
|
||||
"3 │c │cc \n",
|
||||
];
|
||||
assert_eq!(expected, writer.content);
|
||||
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writer,
|
||||
Item::new(1, "a", "aa"),
|
||||
Item::new(2222, "bbbbb", "bbbbb"),
|
||||
Item::new(3, "cccccc", "cc")
|
||||
];
|
||||
|
||||
let expected = concat![
|
||||
"ID │NAME │DESC \n",
|
||||
"1 │a │aa \n",
|
||||
"2222 │bbbbb │bbbbb \n",
|
||||
"3 │cccccc │cc \n",
|
||||
];
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_shrink() {
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writer,
|
||||
Item::new(1, "", "desc"),
|
||||
Item::new(2, "short", "desc"),
|
||||
Item::new(3, "loooooong", "desc"),
|
||||
Item::new(4, "shriiiiink", "desc"),
|
||||
Item::new(5, "shriiiiiiiiiink", "desc"),
|
||||
Item::new(6, "😍😍😍😍", "desc"),
|
||||
Item::new(7, "😍😍😍😍😍", "desc"),
|
||||
Item::new(8, "!😍😍😍😍😍", "desc")
|
||||
];
|
||||
|
||||
let expected = concat![
|
||||
"ID │NAME │DESC \n",
|
||||
"1 │ │desc \n",
|
||||
"2 │short │desc \n",
|
||||
"3 │loooooong │desc \n",
|
||||
"4 │shriiiii… │desc \n",
|
||||
"5 │shriiiii… │desc \n",
|
||||
"6 │😍😍😍😍 │desc \n",
|
||||
"7 │😍😍😍😍… │desc \n",
|
||||
"8 │!😍😍😍… │desc \n",
|
||||
];
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_shrink_width() {
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writer,
|
||||
Item::new(1111, "shriiiiiiiink", "desc very looong"),
|
||||
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
|
||||
];
|
||||
|
||||
let expected = concat![
|
||||
"ID │NAME │DESC \n",
|
||||
"1111 │shri… │desc very looong \n",
|
||||
"2222 │shri… │desc very loooooooooong \n",
|
||||
];
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
use clap::Arg;
|
||||
|
||||
/// Defines the max table width argument.
|
||||
pub fn max_width<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("max-table-width")
|
||||
.help("Defines a maximum width for the table")
|
||||
.short("w")
|
||||
.long("max-width")
|
||||
.value_name("INT")
|
||||
}
|
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": 1627913399,
|
||||
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1637014545,
|
||||
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
|
||||
"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": 1639947939,
|
||||
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
|
||||
"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": 1640418986,
|
||||
"narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=",
|
||||
"owner": "NixOS",
|
||||
"lastModified": 1736437047,
|
||||
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2",
|
||||
"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": 1640418986,
|
||||
"narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2",
|
||||
"lastModified": 1737984647,
|
||||
"narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=",
|
||||
"owner": "pimalaya",
|
||||
"repo": "nix",
|
||||
"rev": "712a481632f4929d24a34cb5762e0ffdc901bd99",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1637453606,
|
||||
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
|
||||
"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": 1642838864,
|
||||
"narHash": "sha256-pHnhm3HWwtvtOK7NdNHwERih3PgNlacrfeDwachIG8E=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "9fb49daf1bbe1d91e6c837706c481f9ebb3d8097",
|
||||
"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": 1623875721,
|
||||
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
|
97
flake.nix
97
flake.nix
|
@ -1,87 +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";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
rust-overlay.overlay
|
||||
(self: super: {
|
||||
# Because rust-overlay bundles multiple rust packages
|
||||
# into one derivation, specify that mega-bundle here,
|
||||
# so that crate2nix will use them automatically.
|
||||
rustc = self.rust-bin.stable.latest.default;
|
||||
cargo = self.rust-bin.stable.latest.default;
|
||||
})
|
||||
];
|
||||
};
|
||||
in
|
||||
rec {
|
||||
# nix build
|
||||
defaultPackage = packages.${name};
|
||||
packages = {
|
||||
${name} = naersk.lib.${system}.buildPackage {
|
||||
pname = name;
|
||||
root = ./.;
|
||||
nativeBuildInputs = with pkgs; [ openssl.dev pkgconfig ];
|
||||
overrideMain = _: {
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/applications/
|
||||
cp assets/himalaya.desktop $out/share/applications/
|
||||
'';
|
||||
};
|
||||
};
|
||||
"${name}-vim" = pkgs.vimUtils.buildVimPluginFrom2Nix {
|
||||
inherit (packages.${name}) version;
|
||||
name = "${name}-vim";
|
||||
src = self;
|
||||
buildInputs = [ packages.${name} ];
|
||||
dontConfigure = false;
|
||||
configurePhase = "cd vim/";
|
||||
postInstall = ''
|
||||
mkdir -p $out/bin
|
||||
ln -s ${packages.${name}}/bin/himalaya $out/bin/himalaya
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# nix run
|
||||
defaultApp = apps.${name};
|
||||
apps.${name} = utils.lib.mkApp {
|
||||
inherit name;
|
||||
drv = packages.${name};
|
||||
};
|
||||
|
||||
# nix develop
|
||||
devShell = pkgs.mkShell {
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
inputsFrom = builtins.attrValues self.packages.${system};
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
cargo-watch
|
||||
trunk
|
||||
ripgrep
|
||||
rust-analyzer
|
||||
rustfmt
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
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"
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
[package]
|
||||
name = "himalaya-lib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
|
@ -1,8 +0,0 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = 2 + 2;
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
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
|
||||
];
|
||||
};
|
||||
}
|
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.82.0"
|
||||
profile = "default"
|
||||
components = ["rust-src", "rust-analyzer"]
|
74
rustfmt.toml
74
rustfmt.toml
|
@ -1,74 +0,0 @@
|
|||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
indent_style = "Block"
|
||||
use_small_heuristics = "Default"
|
||||
fn_call_width = 60
|
||||
attr_fn_like_width = 70
|
||||
struct_lit_width = 18
|
||||
struct_variant_width = 35
|
||||
array_width = 60
|
||||
chain_width = 60
|
||||
single_line_if_else_max_width = 50
|
||||
wrap_comments = false
|
||||
format_code_in_doc_comments = false
|
||||
comment_width = 80
|
||||
normalize_comments = false
|
||||
normalize_doc_attributes = false
|
||||
license_template_path = ""
|
||||
format_strings = false
|
||||
format_macro_matchers = false
|
||||
format_macro_bodies = true
|
||||
empty_item_single_line = true
|
||||
struct_lit_single_line = true
|
||||
fn_single_line = false
|
||||
where_single_line = false
|
||||
imports_indent = "Block"
|
||||
imports_layout = "Mixed"
|
||||
imports_granularity = "Preserve"
|
||||
group_imports = "Preserve"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
reorder_impl_items = false
|
||||
type_punctuation_density = "Wide"
|
||||
space_before_colon = false
|
||||
space_after_colon = true
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Front"
|
||||
remove_nested_parens = true
|
||||
combine_control_expr = true
|
||||
overflow_delimited_expr = false
|
||||
struct_field_align_threshold = 0
|
||||
enum_discrim_align_threshold = 0
|
||||
match_arm_blocks = true
|
||||
match_arm_leading_pipes = "Never"
|
||||
force_multiline_blocks = false
|
||||
fn_args_layout = "Tall"
|
||||
brace_style = "SameLineWhere"
|
||||
control_brace_style = "AlwaysSameLine"
|
||||
trailing_semicolon = true
|
||||
trailing_comma = "Vertical"
|
||||
match_block_trailing_comma = false
|
||||
blank_lines_upper_bound = 1
|
||||
blank_lines_lower_bound = 0
|
||||
edition = "2015"
|
||||
version = "One"
|
||||
inline_attribute_width = 0
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
condense_wildcard_suffixes = false
|
||||
color = "Auto"
|
||||
unstable_features = false
|
||||
disable_all_formatting = false
|
||||
skip_children = false
|
||||
hide_parse_errors = false
|
||||
error_on_line_overflow = false
|
||||
error_on_unformatted = false
|
||||
report_todo = "Never"
|
||||
report_fixme = "Never"
|
||||
ignore = []
|
||||
emit_mode = "Files"
|
||||
make_backup = false
|
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,
|
||||
}
|
||||
}
|
||||
}
|
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;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue