Compare commits
611 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd461fa820 | ||
![]() |
2870624d44 | ||
![]() |
30c599ab92 | ||
![]() |
c34465bab5 | ||
![]() |
1f6471556e | ||
![]() |
0cd0864fa1 | ||
![]() |
9f7dcfc21e | ||
![]() |
c8e055a718 | ||
![]() |
84564f44a3 | ||
![]() |
a1c9524f74 | ||
![]() |
2257b91b40 | ||
![]() |
ece6bfc2ce | ||
![]() |
d49344f9d8 | ||
![]() |
f944ebed81 | ||
![]() |
07812d2c85 | ||
![]() |
2e900be698 | ||
![]() |
91dc271d74 | ||
![]() |
fa896f6bb9 | ||
![]() |
b77a691b7d | ||
![]() |
91a17ece5c | ||
![]() |
e784e8d239 | ||
![]() |
80e5347178 | ||
![]() |
4a61a4b857 | ||
![]() |
12de82e7b4 | ||
![]() |
192ecea2a4 | ||
![]() |
ed85da51df | ||
![]() |
3433c5c3d5 | ||
![]() |
6b3636013c | ||
![]() |
60c90d7549 | ||
![]() |
5e77821f78 | ||
![]() |
fcab855fda | ||
![]() |
1f2fec198f | ||
![]() |
776298511b | ||
![]() |
de65eec3a9 | ||
![]() |
18e9d5c148 | ||
![]() |
fd243fa5ab | ||
![]() |
36a63e8878 | ||
![]() |
00ce9660ef | ||
![]() |
03df2ac128 | ||
![]() |
efab99fda2 | ||
![]() |
ce53925f36 | ||
![]() |
7dae430759 | ||
![]() |
a6df350843 | ||
![]() |
7f8f1cf65f | ||
![]() |
4be6936026 | ||
![]() |
9b7825bc59 | ||
![]() |
8c176d3840 | ||
![]() |
13e917d97b | ||
![]() |
49ecbb56f7 | ||
![]() |
cf16bf65f7 | ||
![]() |
18ae584836 | ||
![]() |
a389772d96 | ||
![]() |
0f09633899 | ||
![]() |
972424829c | ||
![]() |
25c32a6b95 | ||
![]() |
fb5a88c22c | ||
![]() |
567270e177 | ||
![]() |
5af6e059b7 | ||
![]() |
630df3083f | ||
![]() |
b7e215f9c2 | ||
![]() |
c375b48ebf | ||
![]() |
25f0a3f814 | ||
![]() |
2bd8d7ba01 | ||
![]() |
a27a28275b | ||
![]() |
68ec2c53a8 | ||
![]() |
f036f95eee | ||
![]() |
7ff1db143f | ||
![]() |
35fa8e94a6 | ||
![]() |
ecc9b4823e | ||
![]() |
707a129ea4 | ||
![]() |
e7a164de0c | ||
![]() |
3040521695 | ||
![]() |
e9ec6761f9 | ||
![]() |
7b1be139f2 | ||
![]() |
b912aabca2 | ||
![]() |
0e77bd5b4c | ||
![]() |
d5d3457914 | ||
![]() |
c1901c962d | ||
![]() |
d21c686da7 | ||
![]() |
bcbcb012ef | ||
![]() |
cb2dd5def5 | ||
![]() |
c6e9e4245d | ||
![]() |
320fddad41 | ||
![]() |
d2559e42e5 | ||
![]() |
2411481196 | ||
![]() |
789a88b28d | ||
![]() |
ad79bf84c2 | ||
![]() |
81ace71b35 | ||
![]() |
469168959f | ||
![]() |
50922d97b8 | ||
![]() |
0d088962d5 | ||
![]() |
ee89794248 | ||
![]() |
441fda568c | ||
![]() |
f656aff08e | ||
![]() |
3a93103520 | ||
![]() |
3558db514a | ||
![]() |
dba5b68be8 | ||
![]() |
6c315580b1 | ||
![]() |
1b708a9990 | ||
![]() |
05f404ba1f | ||
![]() |
ae29494575 | ||
![]() |
b2200ec3ab | ||
![]() |
c3cac77dee | ||
![]() |
27486f2908 | ||
![]() |
6cfe4da0c1 | ||
![]() |
77e7c3df60 | ||
![]() |
b930cb4940 | ||
![]() |
0c590bbc0c | ||
![]() |
b8e841bbcd | ||
![]() |
ca7eb79284 | ||
![]() |
39592ad02c | ||
![]() |
6d0d968040 | ||
![]() |
9865211076 | ||
![]() |
4f927bbe64 | ||
![]() |
5915f125c3 | ||
![]() |
cd2e4bf3a4 | ||
![]() |
a6c7621ce3 | ||
![]() |
7dee32ae88 | ||
![]() |
32f7e50fd4 | ||
![]() |
49dcbc5e58 | ||
![]() |
2069b4da09 | ||
![]() |
8af003abd0 | ||
![]() |
593ed22ba1 | ||
![]() |
e032acfab7 | ||
![]() |
e0cfe8e4d7 | ||
![]() |
2001b4dd06 | ||
![]() |
1b201bf611 | ||
![]() |
be3b3ef89b | ||
![]() |
7f0157a966 | ||
![]() |
7e800a8f3f | ||
![]() |
598a70f9df | ||
![]() |
1b3f2732b2 | ||
![]() |
e6fa7093bf | ||
![]() |
0b6988b7cf | ||
![]() |
23395491db | ||
![]() |
c82341f3af | ||
![]() |
b1f24cbe95 | ||
![]() |
a69122f8b0 | ||
![]() |
7cfcbb7ab1 | ||
![]() |
128b959f36 | ||
![]() |
b27bac7f85 | ||
![]() |
6eeb4571b7 | ||
![]() |
9a9cd03d9d | ||
![]() |
592ce15903 | ||
![]() |
fc3308e428 | ||
![]() |
b33433e457 | ||
![]() |
5f120309f9 | ||
![]() |
6bc0caf4e0 | ||
![]() |
d4636bcc70 | ||
![]() |
aaea3a5ab4 | ||
![]() |
b048c95a86 | ||
![]() |
f78884ce02 | ||
![]() |
571ae390b8 | ||
![]() |
2ddd28ee85 | ||
![]() |
7dbee81dad | ||
![]() |
46b2c3b1f7 | ||
![]() |
d0c81749ee | ||
![]() |
ba3ad8ed18 | ||
![]() |
719e2eb271 | ||
![]() |
b88dc4412b | ||
![]() |
e9a72072bf | ||
![]() |
c051190114 | ||
![]() |
d20a9d0afa | ||
![]() |
601e37117c | ||
![]() |
dc9e91df1f | ||
![]() |
6d520605ff | ||
![]() |
0c0f821000 | ||
![]() |
8f0e1d6640 | ||
![]() |
e9b87b2e40 | ||
![]() |
4c44c440f6 | ||
![]() |
5c4faea539 | ||
![]() |
2af5c8b6fd | ||
![]() |
00236b86f6 | ||
![]() |
90974e7c0d | ||
![]() |
4a26cfa106 | ||
![]() |
cbafdcf734 | ||
![]() |
7c056e4bdb | ||
![]() |
8205c7f51a | ||
![]() |
ac1349b850 | ||
![]() |
32acc3474f | ||
![]() |
b950fceab4 | ||
![]() |
65b32e7719 | ||
![]() |
b55edd4727 | ||
![]() |
a44486d904 | ||
![]() |
14d74f3689 | ||
![]() |
67b88d24fc | ||
![]() |
57b45a9c4a | ||
![]() |
1232e16ad9 | ||
![]() |
8091583221 | ||
![]() |
a55f65e131 | ||
![]() |
335cca88cb | ||
![]() |
4e967280e1 | ||
![]() |
2d320688ce | ||
![]() |
1779ad5d3a | ||
![]() |
1e11c29c88 | ||
![]() |
e48fcc3367 | ||
![]() |
151fcebe5b | ||
![]() |
4d4e189cb9 | ||
![]() |
b798ca4a95 | ||
![]() |
77da86eb0f | ||
![]() |
41e1fdd554 | ||
![]() |
86e25bc017 | ||
![]() |
604ae11128 | ||
![]() |
a7c73fc8cf | ||
![]() |
87d2cec9d9 | ||
![]() |
d6197e8b24 | ||
![]() |
dbbb1529e4 | ||
![]() |
4707ec9f2a | ||
![]() |
32e3be8b10 | ||
![]() |
26d33ce523 | ||
![]() |
394236ba8a | ||
![]() |
a4f344b396 | ||
![]() |
11798be804 | ||
![]() |
6fbf569fe0 | ||
![]() |
191725b5c2 | ||
![]() |
2bb9b20d95 | ||
![]() |
28f45805d7 | ||
![]() |
60833ee51d | ||
![]() |
53b0d035e4 | ||
![]() |
f06a9072d6 | ||
![]() |
9c1b442452 | ||
![]() |
4bbf446bc1 | ||
![]() |
1cfb0b1538 | ||
![]() |
72dea6f3b2 | ||
![]() |
f3ad824df9 | ||
![]() |
aed7a60fb9 | ||
![]() |
0ee7fc4d95 | ||
![]() |
6be5fd2610 | ||
![]() |
036586a2f7 | ||
![]() |
bf3a4c5dc1 | ||
![]() |
4b959f5c4f | ||
![]() |
978cefbb50 | ||
![]() |
a214a35c02 | ||
![]() |
8d45ecc15d | ||
![]() |
de72bc6ac7 | ||
![]() |
0bed37b5a7 | ||
![]() |
2084ce9375 | ||
![]() |
35f12b1551 | ||
![]() |
eda6620cb4 | ||
![]() |
7419b465ea | ||
![]() |
6ee148c041 | ||
![]() |
6b05279a09 | ||
![]() |
4684b6016b | ||
![]() |
94f345d731 | ||
![]() |
15d24ab0e3 | ||
![]() |
f7ec6d6bc5 | ||
![]() |
8481294147 | ||
![]() |
e6877e89c2 | ||
![]() |
2b3828d8d2 | ||
![]() |
7be8912c14 | ||
![]() |
9e9c04a3f6 | ||
![]() |
a8dad31776 | ||
![]() |
6513c18810 | ||
![]() |
84cfa358de | ||
![]() |
56b1bf28eb | ||
![]() |
14f2d91193 | ||
![]() |
6906585942 | ||
![]() |
3216324ccc | ||
![]() |
7020cd6698 | ||
![]() |
33836a3263 | ||
![]() |
374ea8bacb | ||
![]() |
201081b6d4 | ||
![]() |
20d7329263 | ||
![]() |
8e300c4661 | ||
![]() |
f3d59ebf64 | ||
![]() |
a83b4176b0 | ||
![]() |
6ae2cf0b6b | ||
![]() |
e329097d0b | ||
![]() |
f2e9cac38e | ||
![]() |
41d07fbcef | ||
![]() |
0b113cdbe1 | ||
![]() |
52c75e92b5 | ||
![]() |
8b568f6e3b | ||
![]() |
46df4b573b | ||
![]() |
85a55ed633 | ||
![]() |
824de287b4 | ||
![]() |
197132cca0 | ||
![]() |
8a74920dc7 | ||
![]() |
97af00cd83 | ||
![]() |
15eeac5191 | ||
![]() |
27ac3061e1 | ||
![]() |
afccebf331 | ||
![]() |
f5f1e06878 | ||
![]() |
06437e607c | ||
![]() |
a13bf13f24 | ||
![]() |
6379fbe8f4 | ||
![]() |
1e2e3da02f | ||
![]() |
8552e499e8 | ||
![]() |
96cc02a000 | ||
![]() |
91fdef9820 | ||
![]() |
b7da1d0f99 | ||
![]() |
9f783d9a07 | ||
![]() |
12695a00da | ||
![]() |
86f9b213bf | ||
![]() |
073aef8671 | ||
![]() |
475860c946 | ||
![]() |
814af0e94d | ||
![]() |
738f7c4695 | ||
![]() |
2dc1721a49 | ||
![]() |
1e50911c55 | ||
![]() |
0dc2462358 | ||
![]() |
a8e82a302b | ||
![]() |
2429f17b44 | ||
![]() |
3ebf5510ea | ||
![]() |
a9122c6e34 | ||
![]() |
a85b3a089f | ||
![]() |
a330ff96e9 | ||
![]() |
d1499242b2 | ||
![]() |
386208664b | ||
![]() |
100fa8b3d1 | ||
![]() |
608301dc3d | ||
![]() |
39e903b1d3 | ||
![]() |
9daf943758 | ||
![]() |
8dc4465c58 | ||
![]() |
5110813e87 | ||
![]() |
a82d1e1ebe | ||
![]() |
dfc2bb4311 | ||
![]() |
84d93d6555 | ||
![]() |
af6838c20c | ||
![]() |
d40ee6928f | ||
![]() |
7c47f70217 | ||
![]() |
ebc1fa3b8a | ||
![]() |
fd76df7889 | ||
![]() |
5b6c1aa88c | ||
![]() |
e96e9789db | ||
![]() |
e3c1656e05 | ||
![]() |
084a222a51 | ||
![]() |
122a2a4d76 | ||
![]() |
97aa6a8e6c | ||
![]() |
9fb5bc41b4 | ||
![]() |
1cce8c1162 | ||
![]() |
a9e9d952d5 | ||
![]() |
eb27773b47 | ||
![]() |
3251e7bd61 | ||
![]() |
6e1fea8059 | ||
![]() |
15f3a3fba6 | ||
![]() |
f9a3b33397 | ||
![]() |
dd525bd940 | ||
![]() |
8c880dc747 | ||
![]() |
7200589a9e | ||
![]() |
430cbdfd42 | ||
![]() |
fe604bf0ea | ||
![]() |
9fcb0a0451 | ||
![]() |
bbe2cffafe | ||
![]() |
6da4e2eca6 | ||
![]() |
e107d613a0 | ||
![]() |
c99633e141 | ||
![]() |
9ab404c57a | ||
![]() |
c4f7b77a39 | ||
![]() |
c04b593bdf | ||
![]() |
0ffe7fa5b3 | ||
![]() |
b4579075a8 | ||
![]() |
6f61176a99 | ||
![]() |
7bdc8f52b1 | ||
![]() |
7e8d19afc7 | ||
![]() |
d3a45b3442 | ||
![]() |
27c4876fca | ||
![]() |
ff3fe07758 | ||
![]() |
7856ea33f0 | ||
![]() |
a8956bafc8 | ||
![]() |
f1332e82b6 | ||
![]() |
819551876d | ||
![]() |
01bc62e0ad | ||
![]() |
8dd87c1ac5 | ||
![]() |
e9dd6becc3 | ||
![]() |
6be25ac3df | ||
![]() |
33999fc6ab | ||
![]() |
45bfcf8707 | ||
![]() |
3bab5324c4 | ||
![]() |
2bfe608678 | ||
![]() |
e8e7697001 | ||
![]() |
74f0d12afb | ||
![]() |
571bd98497 | ||
![]() |
e187bb3f0d | ||
![]() |
74a3539f88 | ||
![]() |
7eed944abc | ||
![]() |
3de4908d6b | ||
![]() |
f2b59a7633 | ||
![]() |
f7838b1ddf | ||
![]() |
a78f3f261d | ||
![]() |
50ecade74a | ||
![]() |
77867aeed4 | ||
![]() |
fc1122a2aa | ||
![]() |
7ba7dc70c5 | ||
![]() |
dce3852fe5 | ||
![]() |
4722d7ccb8 | ||
![]() |
f0d1b9cfd1 | ||
![]() |
ed5a6b04f4 | ||
![]() |
58d7327130 | ||
![]() |
0e1e5b9ea7 | ||
![]() |
06ec2790d0 | ||
![]() |
fe08d52a7f | ||
![]() |
39fbb164ed | ||
![]() |
c65635ef60 | ||
![]() |
4148aee59b | ||
![]() |
4bdfb3a31b | ||
![]() |
671d35e21e | ||
![]() |
a4ebe3b7d4 | ||
![]() |
57e3e643a1 | ||
![]() |
a8c7582fa3 | ||
![]() |
a9c3b151f1 | ||
![]() |
1abce964c7 | ||
![]() |
735b44f286 | ||
![]() |
50ff16c44f | ||
![]() |
9ca34a6864 | ||
![]() |
8fff740176 | ||
![]() |
8eaf03554f | ||
![]() |
8ec6f22090 | ||
![]() |
b5ddc397df | ||
![]() |
46e40856ba | ||
![]() |
35408b1689 | ||
![]() |
5d915baa81 | ||
![]() |
684fae3ed8 | ||
![]() |
ab04189887 | ||
![]() |
36b7c00b97 | ||
![]() |
3a5306e9dd | ||
![]() |
89c7972e12 | ||
![]() |
8f3dee9b22 | ||
![]() |
660022ce23 | ||
![]() |
29cc1bce5b | ||
![]() |
bc1b65316d | ||
![]() |
11a0586d56 | ||
![]() |
f70496f14c | ||
![]() |
8a16cf6db4 | ||
![]() |
11f3077b06 | ||
![]() |
dedee908d1 | ||
![]() |
255e93764a | ||
![]() |
c5e9e67604 | ||
![]() |
ae96038fbf | ||
![]() |
07072e2e3f | ||
![]() |
aa5737a004 | ||
![]() |
48cb9ee204 | ||
![]() |
c53a32de4c | ||
![]() |
a69c674c07 | ||
![]() |
6a66afe93e | ||
![]() |
974502c6ff | ||
![]() |
3e9144657b | ||
![]() |
35a9f33aab | ||
![]() |
475609fe92 | ||
![]() |
38bca8f8bc | ||
![]() |
ec01a4412a | ||
![]() |
4e941a9e8b | ||
![]() |
742f038f74 | ||
![]() |
484712b0c3 | ||
![]() |
264782d228 | ||
![]() |
41e965b8a3 | ||
![]() |
f31b5c4000 | ||
![]() |
8014af2563 | ||
![]() |
4ce616aeca | ||
![]() |
a3aaec382a | ||
![]() |
b8b24282a0 | ||
![]() |
e481880321 | ||
![]() |
a88b8c5ea0 | ||
![]() |
b820bd6d9c | ||
![]() |
3b93fa8e7c | ||
![]() |
634bd1917a | ||
![]() |
b5fd3f57a7 | ||
![]() |
1fcb1d59b8 | ||
![]() |
e2cdebe89c | ||
![]() |
3884c0da1f | ||
![]() |
26928e3ae9 | ||
![]() |
070930e671 | ||
![]() |
c7aee72525 | ||
![]() |
30a3205e4f | ||
![]() |
9af284b8db | ||
![]() |
62aee4644b | ||
![]() |
5af2e1ee66 | ||
![]() |
4e7b665672 | ||
![]() |
fd64fe0bf8 | ||
![]() |
51e3f163d4 | ||
![]() |
417b24cd84 | ||
![]() |
873a67d0fb | ||
![]() |
c332c2f5ff | ||
![]() |
1048ce6824 | ||
![]() |
70fc2b455c | ||
![]() |
8de8addd11 | ||
![]() |
1fe3619208 | ||
![]() |
0b468d88ad | ||
![]() |
1eca34b398 | ||
![]() |
5afc078587 | ||
![]() |
a37d5fc1d1 | ||
![]() |
60f26f9dae | ||
![]() |
e80ea9c9de | ||
![]() |
64e60cb0ee | ||
![]() |
81d1c0536b | ||
![]() |
cd448924ed | ||
![]() |
61a0c3c27f | ||
![]() |
7952006870 | ||
![]() |
ddab3179c2 | ||
![]() |
7861fb0402 | ||
![]() |
148f0433d9 | ||
![]() |
8185f2cf7d | ||
![]() |
0270db0123 | ||
![]() |
8ddd673dd8 | ||
![]() |
e3351d2755 | ||
![]() |
31401fa35c | ||
![]() |
33408146a1 | ||
![]() |
8a95febb78 | ||
![]() |
73d5b24e98 | ||
![]() |
0da97dd8c1 | ||
![]() |
933bf157ae | ||
![]() |
f685726eac | ||
![]() |
ab1b946fd9 | ||
![]() |
ce4ba06ce9 | ||
![]() |
bebb473d1b | ||
![]() |
f0866a3965 | ||
![]() |
f63774fa6d | ||
![]() |
808aa4942d | ||
![]() |
08518e1ca8 | ||
![]() |
34a2d52e7e | ||
![]() |
4026e25428 | ||
![]() |
ca7d7bb95d | ||
![]() |
ebe1b3da7e | ||
![]() |
506ae9f594 | ||
![]() |
b6f769b2f4 | ||
![]() |
3691cd2962 | ||
![]() |
97eb636375 | ||
![]() |
b3079715f6 | ||
![]() |
86bbf1ea57 | ||
![]() |
1b0bdd0a9a | ||
![]() |
7412c23870 | ||
![]() |
500fe7f7e4 | ||
![]() |
2419f4bd40 | ||
![]() |
59c99fdc79 | ||
![]() |
5f8d7c8039 | ||
![]() |
876616d45b | ||
![]() |
c41f35fdd5 | ||
![]() |
773254864b | ||
![]() |
e19f3e572c | ||
![]() |
1617212c5b | ||
![]() |
3ba1603af2 | ||
![]() |
0a617410ec | ||
![]() |
5ff4e8ae68 | ||
![]() |
c4344529e3 | ||
![]() |
f900dbea46 | ||
![]() |
f3e85738e7 | ||
![]() |
3a70979483 | ||
![]() |
24971d1960 | ||
![]() |
e37997d697 | ||
![]() |
3adba40e32 | ||
![]() |
da251455a0 | ||
![]() |
d16afc7d8d | ||
![]() |
7eedd86051 | ||
![]() |
c751b2e845 | ||
![]() |
031d0f7dc7 | ||
![]() |
2c6f180df9 | ||
![]() |
63a63253d7 | ||
![]() |
71f3ffe740 | ||
![]() |
10c3b0eabe | ||
![]() |
64898a0583 | ||
![]() |
77a8d9e2c2 | ||
![]() |
ed8a5de2cb | ||
![]() |
b5cc2a095f | ||
![]() |
5dd71ef1cd | ||
![]() |
28fa66cc2a | ||
![]() |
3b4acc15a5 | ||
![]() |
23c15261e7 | ||
![]() |
62b8465f2c | ||
![]() |
1c1be7d6c9 | ||
![]() |
ccf6f9a26e | ||
![]() |
3495ffd61b | ||
![]() |
458258e1aa | ||
![]() |
d018f07aa5 | ||
![]() |
0114e69542 | ||
![]() |
0a74c7d0e5 | ||
![]() |
54d21f25fd | ||
![]() |
ba7a97e90b | ||
![]() |
bcec745c24 | ||
![]() |
b61fc3ab64 | ||
![]() |
c2ae19d120 | ||
![]() |
84f3641ec1 | ||
![]() |
0e3a0c4b70 | ||
![]() |
7645ff1b87 | ||
![]() |
e0adcdfe15 | ||
![]() |
ab14f81900 | ||
![]() |
cd2ba80f8e | ||
![]() |
c1c41c9126 | ||
![]() |
a1cbb1988b | ||
![]() |
470cae6b88 | ||
![]() |
23507932f9 | ||
![]() |
0500e451da | ||
![]() |
6506fffb94 | ||
![]() |
f81a1e2338 | ||
![]() |
ef30228e08 | ||
![]() |
111a1160ad | ||
![]() |
bfc78a0803 | ||
![]() |
7387b67eee | ||
![]() |
af241d25cb | ||
![]() |
fa33a9468a | ||
![]() |
2db021fa0a | ||
![]() |
43bfd4131d | ||
![]() |
ac2a5dcdd1 | ||
![]() |
688e39a67e | ||
![]() |
0e60bdf26e | ||
![]() |
8a21be2177 | ||
![]() |
606f487fc5 | ||
![]() |
0f3b529459 | ||
![]() |
5a7919bb03 | ||
![]() |
f702dc220c | ||
![]() |
e95c275d68 | ||
![]() |
3105a0373b | ||
![]() |
7aec5b8e78 | ||
![]() |
e1b55340fa | ||
![]() |
d3cbf184e6 | ||
![]() |
e88957ae6e | ||
![]() |
3d85ca2edf | ||
![]() |
7888d8b2a5 | ||
![]() |
eb5d49c41a | ||
![]() |
714744366f | ||
![]() |
73b3ed559d | ||
![]() |
22525d40fb |
348 changed files with 68495 additions and 37213 deletions
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Use cargo-derivefmt to sort derives alphabetically
|
||||
f900dbea468e822c5a510a72ecc6367549443927
|
37
.gitea/Makefile.build
Normal file
37
.gitea/Makefile.build
Normal file
|
@ -0,0 +1,37 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
#
|
||||
# Makefile for: "Build and run Tests" workflow, in .gitea/workflows/build.yaml
|
||||
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_INCREMENTAL ?= 0
|
||||
CARGO_NET_RETRY ?= 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
|
||||
RUSTUP_MAX_RETRIES ?= 10
|
||||
RUST_BACKTRACE ?= short
|
||||
|
||||
.PHONY: all
|
||||
all: cargo-check cargo-test-compiles cargo-test rustdoc-build rustdoc-test
|
||||
@printf "All completed.\n"
|
||||
|
||||
.PHONY: cargo-check
|
||||
cargo-check:
|
||||
@printf "cargo-check\n"
|
||||
cargo check --all-features --all --tests --examples --benches --bins
|
||||
.PHONY: cargo-test-compiles
|
||||
cargo-test-compiles:
|
||||
@printf "cargo-test-compiles\n"
|
||||
cargo test --all --no-fail-fast --all-features --no-run --locked
|
||||
.PHONY: cargo-test
|
||||
cargo-test:
|
||||
@printf "cargo-test\n"
|
||||
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
|
||||
.PHONY: rustdoc-build
|
||||
rustdoc-build:
|
||||
@printf "rustdoc-build\n"
|
||||
env DISPLAY= WAYLAND_DISPLAY= make build-rustdoc
|
||||
.PHONY: rustdoc-test
|
||||
rustdoc-test:
|
||||
@printf "rustdoc-test\n"
|
||||
make test-docs
|
61
.gitea/Makefile.lint
Normal file
61
.gitea/Makefile.lint
Normal file
|
@ -0,0 +1,61 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
#
|
||||
# Makefile for: "Run cargo lints" workflow, in .gitea/workflows/lints.yaml
|
||||
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_INCREMENTAL ?= 0
|
||||
CARGO_NET_RETRY ?= 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
|
||||
RUSTUP_MAX_RETRIES ?= 10
|
||||
RUST_BACKTRACE ?= short
|
||||
NIGHTLY_EXISTS=`((cargo +nightly 2> /dev/null 1> /dev/null) && echo 0)|| echo 1)`
|
||||
GIT=env GIT_CONFIG_GLOBAL="" GIT_CONFIG_SYSTEM="" GIT_CONFIG_NOSYSTEM=1 git
|
||||
|
||||
.PHONY: all
|
||||
all: cargo-msrv rustfmt clippy cargo-derivefmt-melib cargo-derivefmt-meli cargo-derivefmt-tools
|
||||
@printf "All checks completed.\n"
|
||||
|
||||
# Check both melib and meli in the same Make target, because if melib does not
|
||||
# satisfy MSRV then meli won't either, since it depends on melib.
|
||||
.PHONY: cargo-msrv
|
||||
cargo-msrv:
|
||||
@printf "cargo-msrv\n"
|
||||
cargo msrv --output-format json --log-level trace --log-target stdout --path meli verify -- cargo check --all-targets
|
||||
cargo msrv --output-format json --log-level trace --log-target stdout --path melib verify -- cargo check --all-targets
|
||||
|
||||
.PHONY: rustfmt
|
||||
rustfmt:
|
||||
@printf "rustfmt\n"
|
||||
@((if [ "${NIGHTLY_EXISTS}" -eq 0 ]; then printf "running rustfmt with nightly toolchain\n"; else printf "running rustfmt with active toolchain\n"; fi))
|
||||
@((if [ "${NIGHTLY_EXISTS}" -eq 0 ]; then cargo +nightly fmt --check --all; else cargo fmt --check --all; fi))
|
||||
|
||||
.PHONY: clippy
|
||||
clippy:
|
||||
@printf "clippy\n"
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: cargo-derivefmt-melib
|
||||
cargo-derivefmt-melib:
|
||||
@printf "cargo-derivefmt-melib\n"
|
||||
@printf "Checking that derives are sorted alphabetically...\n"
|
||||
cargo derivefmt --manifest-path ./melib/Cargo.toml
|
||||
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
|
||||
@($(GIT) diff --quiet ./melib && $(GIT) diff --cached --quiet ./melib && printf "All ./melib derives are sorted alphabetically.\n") || (printf "Some derives in the ./melib crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
|
||||
|
||||
.PHONY: cargo-derivefmt-meli
|
||||
cargo-derivefmt-meli:
|
||||
@printf "cargo-derivefmt-meli\n"
|
||||
@printf "Checking that derives are sorted alphabetically...\n"
|
||||
cargo derivefmt --manifest-path ./meli/Cargo.toml
|
||||
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
|
||||
@($(GIT) diff --quiet ./meli && $(GIT) diff --cached --quiet ./meli && printf "All ./meli derives are sorted alphabetically.\n") || (printf "Some derives in the ./meli crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
|
||||
|
||||
.PHONY: cargo-derivefmt-tools
|
||||
cargo-derivefmt-tools:
|
||||
@printf "cargo-derivefmt-tools\n"
|
||||
@printf "Checking that derives are sorted alphabetically...\n"
|
||||
cargo derivefmt --manifest-path ./tools/Cargo.toml
|
||||
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
|
||||
@($(GIT) diff --quiet ./tools && $(GIT) diff --cached --quiet ./tools && printf "All ./tools derives are sorted alphabetically.\n") || (printf "Some derives in the ./tools crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
|
28
.gitea/Makefile.manifest-lint
Normal file
28
.gitea/Makefile.manifest-lint
Normal file
|
@ -0,0 +1,28 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
#
|
||||
# Makefile for: "Build and run Tests" workflow, in .gitea/workflows/build.yaml
|
||||
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_INCREMENTAL ?= 0
|
||||
CARGO_NET_RETRY ?= 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
|
||||
RUSTUP_MAX_RETRIES ?= 10
|
||||
RUST_BACKTRACE ?= short
|
||||
|
||||
.PHONY: all
|
||||
all: cargo-sort check-debian-changelog
|
||||
@printf "All checks completed.\n"
|
||||
|
||||
.PHONY: cargo-sort
|
||||
cargo-sort:
|
||||
@printf "cargo-sort\n"
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace fuzz
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace tools
|
||||
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace --workspace
|
||||
|
||||
.PHONY: check-debian-changelog
|
||||
check-debian-changelog:
|
||||
@printf "Check debian/changelog is up-to-date.\n"
|
||||
./scripts/check_debian_changelog.sh
|
|
@ -1,25 +1,41 @@
|
|||
---
|
||||
|
||||
name: "Pull Request"
|
||||
about: "Standard pull request template."
|
||||
title: "WIP: "
|
||||
about: "Basic pull request template"
|
||||
ref: "master"
|
||||
|
||||
---
|
||||
|
||||
<!-- If your PR is ready to merge/review, remove the `WIP: ` prefix from the title. -->
|
||||
<!-- If your PR is not ready to merge/review, you can add a `WIP: ` prefix to the title. -->
|
||||
<!--
|
||||
This template is just a suggestion, and is commented out using HTML comment syntax.
|
||||
It will not show up in your PR text unless you remove the comment markers.
|
||||
-->
|
||||
|
||||
### Summary of the PR
|
||||
<!--
|
||||
|
||||
<!-- Changes introduced in this PR. -->
|
||||
## Summary of the PR
|
||||
|
||||
### Requirements
|
||||
Changes introduced in this PR.
|
||||
|
||||
Before submitting your PR, please make sure you have addressed the following requirements:
|
||||
|
||||
* [ ] All commits in this PR are signed (with `git commit -s`), and the commit has a message describing the motivation behind the change, if appropriate.
|
||||
* [ ] All added/changed public-facing functionality, especially configuration options, are documented in the manual pages.
|
||||
## Requirements
|
||||
|
||||
Before submitting your PR, please make sure you have addressed the following
|
||||
requirements:
|
||||
|
||||
* [ ] All commits in this PR are signed (with `git commit -s`), and the commit
|
||||
has a message describing the motivation behind the change, if
|
||||
appropriate.
|
||||
* [ ] All added/changed public-facing functionality, especially configuration
|
||||
options, are documented in the manual pages.
|
||||
* [ ] Any newly added `unsafe` code is properly documented.
|
||||
* [ ] Each commit has been formatted with `rustfmt`. Run `make fmt` in the project root.
|
||||
* [ ] Each commit has been linted with `clippy`. Run `make lint` in the project root.
|
||||
* [ ] Each commit does not break any test. Run `make test` in the project root. If you have `cargo-nextest` installed, you can run `cargo nextest run --all --no-fail-fast --all-features --future-incompat-report` instead.
|
||||
* [ ] Each commit has been formatted with `rustfmt`. Run `make fmt` in the
|
||||
project root.
|
||||
* [ ] Each commit has been linted with `clippy`. Run `make lint` in the project
|
||||
root.
|
||||
* [ ] Each commit does not break any test. Run `make test` in the project root.
|
||||
If you have `cargo-nextest` installed, you can run `cargo nextest run
|
||||
--all --no-fail-fast --all-features --future-incompat-report` instead.
|
||||
|
||||
-->
|
||||
|
|
109
.gitea/check_dco.sh
Executable file
109
.gitea/check_dco.sh
Executable file
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env sh
|
||||
# SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
# Lint with shellcheck -s sh -S style check_dco.sh
|
||||
|
||||
# Notes:
|
||||
# ======
|
||||
#
|
||||
# - We need to make sure git commands do not read from any existing configs to
|
||||
# prevent surprises like default trailers being added.
|
||||
# - we need to pass `--always` to `git-format-patch` to check even empty
|
||||
# commits despite them not being something we would merge. This tripped me up
|
||||
# when debugging this workflow because I tested it with empty commits. My
|
||||
# fault.
|
||||
|
||||
export GIT_CONFIG_GLOBAL=""
|
||||
export GIT_CONFIG_SYSTEM=""
|
||||
export GIT_CONFIG_NOSYSTEM=1
|
||||
|
||||
ensure_env_var() {
|
||||
set | grep -q "^${1}=" || (printf "Environment variable %s missing from process environment, exiting.\n" "${1}"; exit "${2}")
|
||||
}
|
||||
|
||||
ensure_env_var "GITHUB_BASE_REF" 1 || exit $?
|
||||
ensure_env_var "GITHUB_HEAD_REF" 2 || exit $?
|
||||
|
||||
# contains_correct_signoff() {
|
||||
# author=$(git log --author="$1" --pretty="%an <%ae>" -1)
|
||||
# git format-patch --always --stdout "${1}^..${1}" | git interpret-trailers --parse | grep -q "^Signed-off-by: ${author}"
|
||||
# }
|
||||
contains_signoff() {
|
||||
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${1}" | git interpret-trailers --parse | grep -q "^Signed-off-by: "
|
||||
}
|
||||
|
||||
get_commit_sha() {
|
||||
if OUT=$(git rev-parse "${1}"); then
|
||||
printf "%s" "${OUT}"
|
||||
return
|
||||
fi
|
||||
printf "Could not git-rev-parse %s, falling back to HEAD...\n" "${1}" 1>&2
|
||||
git rev-parse HEAD
|
||||
}
|
||||
|
||||
echo "Debug workflow info:"
|
||||
echo "Base ref GITHUB_BASE_REF=${GITHUB_BASE_REF}"
|
||||
echo "Head ref GITHUB_HEAD_REF=${GITHUB_HEAD_REF}"
|
||||
BASE_REF=$(get_commit_sha "${GITHUB_BASE_REF}")
|
||||
HEAD_REF=$(get_commit_sha "${GITHUB_HEAD_REF}")
|
||||
echo "Processed base ref BASE_REF=${BASE_REF}"
|
||||
echo "Processed head ref HEAD_REF=${HEAD_REF}"
|
||||
|
||||
RANGE="${BASE_REF}..${HEAD_REF}"
|
||||
echo "Range to examine is RANGE=${RANGE}"
|
||||
|
||||
if ! SHA_LIST=$(git rev-list "${RANGE}"); then
|
||||
printf "Could not get commit range %s with git rev-list, bailing out...\n" "${RANGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "SHA list to examine is SHA_LIST="
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo "${SHA_LIST}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo ""
|
||||
echo "Starting checks..."
|
||||
|
||||
output=$(printf "%s" "${SHA_LIST}" | while read -r commit_sha; do
|
||||
contains_signoff_result=""
|
||||
|
||||
contains_signoff "${commit_sha}"; contains_signoff_result="$?"
|
||||
if [ "${contains_signoff_result}" -ne 0 ]; then
|
||||
printf "Commit does not contain Signed-off-by git trailer: %s\n\n" "${commit_sha}"
|
||||
echo "patch was:"
|
||||
echo "---------------------------------------------------------------------"
|
||||
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${commit_sha}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo "trailers were:"
|
||||
echo "---------------------------------------------------------------------"
|
||||
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${commit_sha}" | git interpret-trailers --parse
|
||||
echo "---------------------------------------------------------------------"
|
||||
echo "commit was:"
|
||||
echo "---------------------------------------------------------------------"
|
||||
git log --no-decorate --pretty=oneline --abbrev-commit -n 1 "${commit_sha}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
fi
|
||||
done)
|
||||
|
||||
if [ "${output}" = "" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "One or more of your commits in this Pull Request lack the Developer Certificate of Origin "
|
||||
echo "which is more commonly known as DCO or the \"Signed-off-by: \" trailer line in the "
|
||||
echo "git commit message."
|
||||
echo "For information, documentation, help, check: https://wiki.linuxfoundation.org/dco"
|
||||
|
||||
echo "The reported errors were:"
|
||||
printf "%s\n" "${output}" 1>&2
|
||||
|
||||
echo ""
|
||||
echo "Solution:"
|
||||
echo ""
|
||||
echo "- end all your commits with a 'Signed-off-by: User <user@localhost>' line, "
|
||||
echo " with your own display name and email address."
|
||||
echo "- Make sure the signoff is separated by the commit message body with an empty line."
|
||||
echo "- Make sure the signoff is the last line in your commit message."
|
||||
echo "- Lastly, make sure the signoff matches your git commit author name and email identity."
|
||||
|
||||
exit 1
|
123
.gitea/workflows/build.yaml
Normal file
123
.gitea/workflows/build.yaml
Normal file
|
@ -0,0 +1,123 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Build and run Tests
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/src/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/src/**'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, linux-arm64]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- build: linux-arm64
|
||||
arch: arm64
|
||||
os: ubuntu-latest-arm64
|
||||
rust: stable
|
||||
target: aarch64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev make
|
||||
- name: Cache rustup
|
||||
id: cache-rustup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/
|
||||
~/.cargo/env
|
||||
~/.cargo/config.toml
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: build-workflow-${{ matrix.build }}-rustup
|
||||
- id: rustup-setup
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
name: Install rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Source .cargo/env
|
||||
shell: bash
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
- name: Setup Rust target
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Add test dependencies
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
|
||||
- name: Restore build artifacts cache in target dir
|
||||
id: cache-deps
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: cargo-check
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build cargo-check
|
||||
- if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
name: Save build artifacts in target dir
|
||||
id: save-cache-deps
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: cargo-test-compiles
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build cargo-test-compiles
|
||||
- name: cargo-test
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build cargo-test
|
||||
- name: rustdoc build
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build rustdoc-build
|
||||
- name: rustdoc tests
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.build rustdoc-test
|
79
.gitea/workflows/build_artifacts.yaml
Normal file
79
.gitea/workflows/build_artifacts.yaml
Normal file
|
@ -0,0 +1,79 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Build release binaries
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build release binary
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, linux-arm64]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
artifact_name: 'meli-linux-amd64'
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- build: linux-arm64
|
||||
arch: arm64
|
||||
os: ubuntu-latest-arm64
|
||||
rust: stable
|
||||
artifact_name: 'meli-linux-arm64'
|
||||
target: aarch64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
|
||||
- id: rustup-setup
|
||||
name: Install rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Build binary
|
||||
run: |
|
||||
VERSION=$(grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
make
|
||||
mkdir artifacts
|
||||
mv target/*/release/* target/ || true
|
||||
mv target/release/* target/ || true
|
||||
mv target/meli artifacts/meli-${VERSION}-${{ matrix.target }}
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-${{ env.VERSION }}
|
||||
path: artifacts/meli-${{ env.VERSION }}-${{ matrix.target }}
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
71
.gitea/workflows/build_debian_package.yaml
Normal file
71
.gitea/workflows/build_debian_package.yaml
Normal file
|
@ -0,0 +1,71 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Build .deb package
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build-debian:
|
||||
name: Create debian package
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, linux-arm64]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
artifact_name: 'linux-amd64'
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- build: linux-arm64
|
||||
arch: arm64
|
||||
os: ubuntu-latest-arm64
|
||||
rust: stable
|
||||
artifact_name: 'linux-arm64'
|
||||
target: aarch64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y mandoc debhelper quilt build-essential
|
||||
- id: rustup-setup
|
||||
name: Install rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Build binary
|
||||
run: |
|
||||
VERSION=$(grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
make deb-dist
|
||||
mkdir artifacts
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
mv ../meli_*.deb artifacts/meli-${VERSION}-${{ matrix.artifact_name }}.deb
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: meli-${{env.VERSION}}-${{ matrix.artifact_name }}.deb
|
||||
path: artifacts/meli-${{env.VERSION}}-${{ matrix.artifact_name }}.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
|
@ -1,86 +0,0 @@
|
|||
name: Build release binary
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
artifact_name: 'meli-linux-amd64'
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v2
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
|
||||
#- id: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
uses: https://github.com/actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Build binary
|
||||
run: |
|
||||
make
|
||||
mkdir artifacts
|
||||
mv target/*/release/* target/ || true
|
||||
mv target/release/* target/ || true
|
||||
mv target/meli artifacts/
|
||||
- name: Upload Artifacts
|
||||
uses: https://github.com/actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: artifacts
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
26
.gitea/workflows/check_dco.yaml
Normal file
26
.gitea/workflows/check_dco.yaml
Normal file
|
@ -0,0 +1,26 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Verify DCO
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Verify DCO signoff on commit messages
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: check-dco
|
||||
shell: sh
|
||||
name: Check that commit messages end with a Signed-off-by git trailer
|
||||
run: |
|
||||
env GITHUB_BASE_REF="origin/${{env.GITHUB_BASE_REF}}" GITHUB_HEAD_REF="origin/${{env.GITHUB_HEAD_REF}}" sh ./.gitea/check_dco.sh
|
140
.gitea/workflows/lints.yaml
Normal file
140
.gitea/workflows/lints.yaml
Normal file
|
@ -0,0 +1,140 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Run cargo lints
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/src/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/src/**'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
lints:
|
||||
name: Run lints
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
|
||||
- name: Find meli MSRV from meli/Cargo.toml.
|
||||
run: |
|
||||
echo MELI_MSRV=$(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1) >> $GITHUB_ENV
|
||||
printf "Rust MSRV is %s\n" $(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
- name: Cache rustup
|
||||
id: cache-rustup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/
|
||||
~/.cargo/env
|
||||
~/.cargo/config.toml
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: lints-workflow-${{ matrix.build }}-rustup
|
||||
- id: rustup-setup
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
name: Install Rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ env.MELI_MSRV }}"
|
||||
rustup component add rustfmt --toolchain ${{ env.MELI_MSRV }}-${{ matrix.target }}
|
||||
rustup toolchain install --profile minimal --component clippy,rustfmt --target "${{ matrix.target }}" -- "${{ matrix.rust }}"
|
||||
rustup component add rustfmt --toolchain ${{ matrix.rust }}-${{ matrix.target }}
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Source .cargo/env
|
||||
shell: bash
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
- name: Setup Rust target
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Add lint dependencies
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install --version 0.15.1 --target "${{ matrix.target }}" cargo-msrv
|
||||
# "This package is currently implemented using rust-analyzer internals, so cannot be published on crates.io."
|
||||
RUSTFLAGS="" cargo install --locked --target "${{ matrix.target }}" --git https://github.com/dcchut/cargo-derivefmt --rev 95da8eee343de4adb25850893873b979258aed7f --bin cargo-derivefmt
|
||||
- name: Restore build artifacts cache in target dir
|
||||
id: cache-deps
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: clippy
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint clippy
|
||||
- if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
name: Save build artifacts in target dir
|
||||
id: save-cache-deps
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/
|
||||
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: cargo-msrv verify melib MSRV
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f ./.gitea/Makefile.lint cargo-msrv
|
||||
- name: rustfmt
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint rustfmt
|
||||
- name: cargo-derivefmt melib
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint cargo-derivefmt-melib
|
||||
- name: cargo-derivefmt meli
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint cargo-derivefmt-meli
|
||||
- name: cargo-derivefmt tools
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f .gitea/Makefile.lint cargo-derivefmt-tools
|
98
.gitea/workflows/manifest_lints.yaml
Normal file
98
.gitea/workflows/manifest_lints.yaml
Normal file
|
@ -0,0 +1,98 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Cargo manifest lints
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'fuzz/Cargo.toml'
|
||||
- 'tool/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.cargo/config.toml'
|
||||
|
||||
jobs:
|
||||
manifest_lint:
|
||||
name: Run Cargo manifest etc lints
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y mandoc
|
||||
- name: Cache rustup
|
||||
id: cache-rustup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/
|
||||
~/.cargo/env
|
||||
~/.cargo/config.toml
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: manifest_lints-workflow-${{ matrix.build }}-rustup
|
||||
- id: rustup-setup
|
||||
name: Install Rustup and toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ matrix.rust }}"
|
||||
rustup component add rustfmt --toolchain ${{ matrix.rust }}-${{ matrix.target }}
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Source .cargo/env
|
||||
shell: bash
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
|
||||
- name: Setup Rust target
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Add manifest lint dependencies
|
||||
if: steps.cache-rustup.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
|
||||
- name: cargo-sort
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
make -f ./.gitea/Makefile.manifest-lint cargo-sort
|
||||
- name: Check debian/changelog is up-to-date.
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make -f ./.gitea/Makefile.manifest-lint check-debian-changelog
|
|
@ -1,115 +0,0 @@
|
|||
name: Tests
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/src/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/src/**'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/**'
|
||||
- 'melib/src/**'
|
||||
- 'melib/Cargo.toml'
|
||||
- 'meli/src/**'
|
||||
- 'meli/Cargo.toml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v2
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
|
||||
#- id: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
uses: https://github.com/actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
components: clippy, rustfmt
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add lint dependencies
|
||||
run: |
|
||||
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
|
||||
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
|
||||
- name: cargo-check
|
||||
run: |
|
||||
cargo check --all-features --all --tests --examples --benches --bins
|
||||
- name: Compile
|
||||
if: success() || failure()
|
||||
run: cargo test --all --no-fail-fast --all-features --no-run --locked
|
||||
- name: cargo test
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
|
||||
#cargo test --all --no-fail-fast --all-features -- --nocapture --quiet
|
||||
- name: cargo-sort
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo sort --check
|
||||
- name: rustfmt
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo fmt --check --all
|
||||
- name: clippy
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
8
.mailmap
Normal file
8
.mailmap
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2024 Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
# Licensed under the EUPL-1.2-or-later.
|
||||
#
|
||||
# You may obtain a copy of the Licence at:
|
||||
# https://joinup.ec.europa.eu/software/page/eupl
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
Manos Pitsidianakis <manos@pitsidianak.is> <el13635@mail.ntua.gr>
|
91
BUILD.md
Normal file
91
BUILD.md
Normal file
|
@ -0,0 +1,91 @@
|
|||
# Build `meli`
|
||||
|
||||
For a quick start, build and install locally:
|
||||
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
Available subcommands for `make` are listed with `make help`.
|
||||
The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
`meli` requires rust version 1.70.0 or later and rust's package manager, Cargo.
|
||||
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||
|
||||
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
|
||||
Run `make install` to install the binary and man pages.
|
||||
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
|
||||
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
|
||||
## Build features
|
||||
|
||||
Some functionality is held behind "feature gates", or compile-time flags.
|
||||
|
||||
Cargo features for `meli` are documented in its [`README.md`](./meli/README.md) file.
|
||||
|
||||
Cargo features for `melib` are documented in its [`README.md`](./melib/README.md) file.
|
||||
|
||||
The following list explains each feature's purpose:
|
||||
|
||||
- `gpgme` enables GPG support via `libgpgme` (on by default)
|
||||
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
|
||||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
|
||||
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
|
||||
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
|
||||
|
||||
## Build Debian package (*deb*)
|
||||
|
||||
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
|
||||
|
||||
A `*.deb` package can be built with `make deb-dist`
|
||||
|
||||
## Using notmuch
|
||||
|
||||
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
|
||||
In Debian-like systems, install the `libnotmuch5` packages.
|
||||
`meli` detects the library's presence on runtime.
|
||||
If it is not detected, you can use the `library_file_path` setting on your notmuch account to specify the absolute path of the library.
|
||||
|
||||
## Using GPG
|
||||
|
||||
To use the optional gpg feature, you must have `libgpgme` installed in your system.
|
||||
In Debian-like systems, install the `libgpgme11` package.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
## Building and running on Android with `termux`
|
||||
|
||||
This is not a supported or stable setup so caveat emptor.
|
||||
|
||||
At the time of writing this, Android is not a stable Rust target.
|
||||
The packaged Rust from `termux` will be used.
|
||||
|
||||
The following steps should suffice to build and run `meli` on `termux`:
|
||||
|
||||
```console
|
||||
$ pkg install rust perl make m4 man
|
||||
$ cargo install meli # ensure .cargo/bin is in your PATH
|
||||
```
|
||||
|
||||
Exporting `EDITOR` and `PAGER` might be useful.
|
||||
|
||||
## Development
|
||||
|
||||
Development builds can be built and/or run with
|
||||
|
||||
```
|
||||
cargo build
|
||||
cargo run
|
||||
```
|
||||
|
||||
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
|
||||
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
|
||||
|
||||
To trace network and protocol communications you can enable the following features:
|
||||
|
||||
- `imap-trace`
|
||||
- `jmap-trace`
|
||||
- `nntp-trace`
|
||||
- `smtp-trace`
|
1325
CHANGELOG.md
1325
CHANGELOG.md
File diff suppressed because it is too large
Load diff
1931
Cargo.lock
generated
1931
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,50 @@
|
|||
# Development
|
||||
|
||||
Code style follows the `rustfmt.toml` file.
|
||||
|
||||
## Trace logs
|
||||
|
||||
Enable trace logs to `stderr` with:
|
||||
|
||||
```sh
|
||||
export MELI_DEBUG_STDERR=yes
|
||||
```
|
||||
|
||||
This means you will have to to redirect `stderr` to a file like `meli 2> trace.log`.
|
||||
|
||||
Tracing is opt-in by build features:
|
||||
|
||||
```sh
|
||||
cargo build --features=debug-tracing,imap-trace,smtp-trace
|
||||
```
|
||||
|
||||
## use `.git-blame-ignore-revs` file _optional_
|
||||
|
||||
Use this file to ignore formatting commits from `git-blame`.
|
||||
It needs to be set up per project because `git-blame` will fail if it's missing.
|
||||
|
||||
```sh
|
||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
```
|
||||
|
||||
## Formatting with `rustfmt`
|
||||
|
||||
```sh
|
||||
make fmt
|
||||
```
|
||||
|
||||
## Linting with `clippy`
|
||||
|
||||
```sh
|
||||
make lint
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
|
||||
How to run specific tests:
|
||||
|
||||
```sh
|
||||
|
@ -11,14 +54,14 @@ cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
|
|||
## Profiling
|
||||
|
||||
```sh
|
||||
perf record -g target/debug/bin
|
||||
perf record -g target/debug/meli
|
||||
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
|
||||
```
|
||||
|
||||
## Running fuzz targets
|
||||
|
||||
Note: `cargo-fuzz` requires the nightly toolchain.
|
||||
|
||||
```sh
|
||||
cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict
|
||||
```
|
||||
<!-- -->
|
||||
<!-- ## Running fuzz targets -->
|
||||
<!-- -->
|
||||
<!-- Note: `cargo-fuzz` requires the nightly toolchain. -->
|
||||
<!-- -->
|
||||
<!-- ```sh -->
|
||||
<!-- cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict -->
|
||||
<!-- ``` -->
|
||||
|
|
169
Makefile
169
Makefile
|
@ -19,13 +19,13 @@
|
|||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_TARGET_DIR ?= target
|
||||
MIN_RUSTC ?= 1.65.0
|
||||
CARGO_BIN ?= cargo
|
||||
TAGREF_BIN ?= tagref
|
||||
CARGO_ARGS ?=
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility
|
||||
CARGO_SORT_BIN = cargo-sort
|
||||
PRINTF = /usr/bin/printf
|
||||
CARGO_HACK_BIN = cargo-hack
|
||||
PRINTF := `command -v printf`
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -35,13 +35,14 @@ MANDIR ?= ${EXPANDED_PREFIX}/share/man
|
|||
|
||||
# Installation parameters
|
||||
DOCS_SUBDIR ?= meli/docs/
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
|
||||
FEATURES ?= --features "${MELI_FEATURES}"
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5 meli.7
|
||||
FEATURES != [ -z "$${MELI_FEATURES}" ] && ($(PRINTF) -- '--all-features') || ($(PRINTF) -- '--features %s' "$${MELI_FEATURES}")
|
||||
|
||||
MANPATHS != ACCUM="";for m in `manpath 2> /dev/null | tr ':' ' '`; do if [ -d "$${m}" ]; then REAL_PATH=`cd $${m} && pwd` ACCUM="$${ACCUM}:$${REAL_PATH}";fi;done;echo -n $${ACCUM} | sed 's/^://'
|
||||
VERSION != sed -n "s/^version\s*=\s*\"\(.*\)\"/\1/p" Cargo.toml
|
||||
GIT_COMMIT != git show-ref -s --abbrev HEAD
|
||||
DATE != date -I
|
||||
MANPATHS != ACCUM="";for m in `manpath 2> /dev/null | tr ':' ' '`; do if [ -d "$${m}" ]; then REAL_PATH=`cd $${m} && pwd` ACCUM="$${ACCUM}:$${REAL_PATH}";fi;done;echo $${ACCUM}'\c' | sed 's/^://'
|
||||
VERSION = `grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1`
|
||||
MIN_RUSTC = `grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1`
|
||||
GIT_COMMIT = `git show-ref -s --abbrev HEAD`
|
||||
DATE = `date -I`
|
||||
|
||||
# Output parameters
|
||||
BOLD ?= `[ -z $${TERM} ] && echo "" || tput bold`
|
||||
|
@ -50,19 +51,21 @@ ANSI_RESET ?= `[ -z $${TERM} ] && echo "" || tput sgr0`
|
|||
CARGO_COLOR ?= `[ -z $${NO_COLOR+x} ] && echo "" || echo "--color=never "`
|
||||
RED ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 1) || echo ""`
|
||||
GREEN ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 2) || echo ""`
|
||||
YELLOW ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 3) || echo ""`
|
||||
|
||||
.PHONY: meli
|
||||
meli: check-deps
|
||||
@echo ${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --release --bin meli
|
||||
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "For a quick start, build and install locally:\n ${BOLD}${GREEN}PREFIX=~/.local make install${ANSI_RESET}\n"
|
||||
@echo "For a quick start, build and install locally:\n\n${BOLD}${GREEN}make PREFIX=~/.local install${ANSI_RESET}\n"
|
||||
@echo "Available subcommands:"
|
||||
@echo " - ${BOLD}meli${ANSI_RESET} (builds meli with optimizations in \$$CARGO_TARGET_DIR)"
|
||||
@echo " - ${BOLD}install${ANSI_RESET} (installs binary in \$$BINDIR and documentation to \$$MANDIR)"
|
||||
@echo " - ${BOLD}uninstall${ANSI_RESET}"
|
||||
@echo "Secondary subcommands:"
|
||||
@echo "\nSecondary subcommands:"
|
||||
@echo " - ${BOLD}clean${ANSI_RESET} (cleans build artifacts)"
|
||||
@echo " - ${BOLD}check-deps${ANSI_RESET} (checks dependencies)"
|
||||
@echo " - ${BOLD}install-bin${ANSI_RESET} (installs binary to \$$BINDIR)"
|
||||
|
@ -73,44 +76,65 @@ help:
|
|||
@echo " - ${BOLD}deb-dist${ANSI_RESET} (builds debian package in the parent directory)"
|
||||
@echo " - ${BOLD}distclean${ANSI_RESET} (cleans distribution build artifacts)"
|
||||
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
|
||||
@echo "\nENVIRONMENT variables of interest:"
|
||||
@echo "* PREFIX = ${UNDERLINE}${EXPANDED_PREFIX}${ANSI_RESET}"
|
||||
@echo -n "* MELI_FEATURES = ${UNDERLINE}"
|
||||
@[ -z $${MELI_FEATURES+x} ] && echo -n "unset" || echo -n ${MELI_FEATURES}
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* BINDIR = ${UNDERLINE}${BINDIR}${ANSI_RESET}"
|
||||
@echo "* MANDIR = ${UNDERLINE}${MANDIR}${ANSI_RESET}"
|
||||
@echo -n "* MANPATH = ${UNDERLINE}"
|
||||
@[ $${MANPATH+x} ] && echo -n $${MANPATH} || echo -n "unset"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo ""
|
||||
@echo "ENVIRONMENT variables of interest:"
|
||||
@$(PRINTF) "* MELI_FEATURES "
|
||||
@[ -z $${MELI_FEATURES+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${MELI_FEATURES}${ANSI_RESET}
|
||||
@$(PRINTF) "* PREFIX "
|
||||
@[ -z ${EXPANDED_PREFIX} ] && echo "unset" || echo "= ${UNDERLINE}"${EXPANDED_PREFIX}${ANSI_RESET}
|
||||
@$(PRINTF) "* BINDIR = %s\n" "${UNDERLINE}${BINDIR}${ANSI_RESET}"
|
||||
@$(PRINTF) "* MANDIR "
|
||||
@[ -z ${MANDIR} ] && echo "unset" || echo "= ${UNDERLINE}"${MANDIR}${ANSI_RESET}
|
||||
@$(PRINTF) "* MANPATH = "
|
||||
@[ $${MANPATH+x} ] && echo ${UNDERLINE}$${MANPATH}${ANSI_RESET} || echo "unset"
|
||||
@echo "* (cleaned) output of manpath(1) = ${UNDERLINE}${MANPATHS}${ANSI_RESET}"
|
||||
@echo -n "* NO_MAN ${UNDERLINE}"
|
||||
@[ $${NO_MAN+x} ] && echo -n "set" || echo -n "unset"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo -n "* NO_COLOR ${UNDERLINE}"
|
||||
@[ $${NO_COLOR+x} ] && echo -n "set" || echo -n "unset"
|
||||
@echo ${ANSI_RESET}
|
||||
@$(PRINTF) "* NO_MAN "
|
||||
@[ $${NO_MAN+x} ] && echo "set" || echo "unset"
|
||||
@$(PRINTF) "* NO_COLOR "
|
||||
@([ $${NO_COLOR+x} ] && [ "$${NO_COLOR}" != "" ] && echo "set") || echo "unset"
|
||||
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
|
||||
@echo "* CARGO_ARGS = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}"
|
||||
@$(PRINTF) "* CARGO_ARGS "
|
||||
@([ -z "${CARGO_ARGS}" ] && echo "unset") || echo = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}
|
||||
@$(PRINTF) "* RUSTFLAGS = "
|
||||
@([ -z "${RUSTFLAGS}" ] && echo "unset") || echo = ${UNDERLINE}${RUSTFLAGS}${ANSI_RESET}
|
||||
@$(PRINTF) "* AUTHOR (for deb-dist) "
|
||||
@[ -z $${AUTHOR+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${AUTHOR}${ANSI_RESET}
|
||||
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
|
||||
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
|
||||
@echo "* VERSION = ${UNDERLINE}${VERSION}${ANSI_RESET}"
|
||||
@echo "* GIT_COMMIT = ${UNDERLINE}${GIT_COMMIT}${ANSI_RESET}"
|
||||
@echo "* CARGO_TARGET_DIR = ${CARGO_TARGET_DIR}"
|
||||
@echo ""
|
||||
@echo "Built-in/binary utilities"
|
||||
@echo "* PRINTF = ${UNDERLINE}${PRINTF}${ANSI_RESET}"
|
||||
|
||||
.PHONY: check
|
||||
check: check-tagrefs
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
|
||||
@OUT=$$($(CARGO_SORT_BIN) -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
|
||||
@OUT=$$($(CARGO_SORT_BIN) melib -w 2>&1 && $(CARGO_SORT_BIN) meli -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
|
||||
test: test-docs
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test-docs
|
||||
test-docs:
|
||||
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --doc
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
|
||||
|
||||
.PHONY: test-feature-permutations
|
||||
test-feature-permutations:
|
||||
$(CARGO_HACK_BIN) hack --feature-powerset
|
||||
|
||||
.PHONY: check-deps
|
||||
check-deps:
|
||||
|
@ -123,69 +147,75 @@ clean:
|
|||
-rm -rf ./${CARGO_TARGET_DIR}/
|
||||
|
||||
.PHONY: distclean
|
||||
distclean: clean
|
||||
@rm -f meli-${VERSION}.tar.gz
|
||||
distclean:
|
||||
rm -f meli-${VERSION}.tar.gz
|
||||
rm -rf .pc # rm debian stuff
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)${BINDIR}/meli
|
||||
-rm $(DESTDIR)${MANDIR}/man1/meli.1.gz
|
||||
-rm $(DESTDIR)${MANDIR}/man5/meli.conf.5.gz
|
||||
-rm $(DESTDIR)${MANDIR}/man5/meli-themes.5.gz
|
||||
for MANPAGE in ${MANPAGES}; do \
|
||||
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
|
||||
MANPAGEPATH="${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz"; \
|
||||
rm -f "$${MANAGEPATH}"
|
||||
; done
|
||||
|
||||
.PHONY: install-doc
|
||||
install-doc:
|
||||
@(if [ -z $${NO_MAN+x} ]; then \
|
||||
mkdir -p $(DESTDIR)${MANDIR}/man1 ; \
|
||||
mkdir -p $(DESTDIR)${MANDIR}/man5 ; \
|
||||
echo " - ${BOLD}Installing manpages to ${ANSI_RESET}${DESTDIR}${MANDIR}:" ; \
|
||||
for MANPAGE in ${MANPAGES}; do \
|
||||
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
|
||||
mkdir -p $(DESTDIR)${MANDIR}/man$${SECTION} ; \
|
||||
MANPAGEPATH=${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz; \
|
||||
echo " * installing $${MANPAGE} → ${GREEN}$${MANPAGEPATH}${ANSI_RESET}"; \
|
||||
gzip -n < ${DOCS_SUBDIR}$${MANPAGE} > $${MANPAGEPATH} \
|
||||
; done ; \
|
||||
(case ":${MANPATHS}:" in \
|
||||
*:${DESTDIR}${MANDIR}:*) echo -n "";; \
|
||||
*:${DESTDIR}${MANDIR}:*) echo "\c";; \
|
||||
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${MANDIR} is not contained in your MANPATH variable or the output of \`manpath\` command.${ANSI_RESET} \`man\` might fail finding the installed manpages. Consider adding it if necessary.\nMANPATH variable / output of \`manpath\`: ${MANPATHS}" ;; \
|
||||
esac) ; \
|
||||
else echo "NO_MAN is defined, so no documentation is going to be installed." ; fi)
|
||||
|
||||
.PHONY: install-bin
|
||||
install-bin: meli
|
||||
@mkdir -p $(DESTDIR)${BINDIR}
|
||||
mkdir -p $(DESTDIR)${BINDIR}
|
||||
@echo " - ${BOLD}Installing binary to ${ANSI_RESET}${GREEN}${DESTDIR}${BINDIR}/meli${ANSI_RESET}"
|
||||
@case ":${PATH}:" in \
|
||||
*:${DESTDIR}${BINDIR}:*) echo -n "";; \
|
||||
*:${DESTDIR}${BINDIR}:*) echo "\n";; \
|
||||
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${BINDIR} is not contained in your PATH variable.${ANSI_RESET} Consider adding it if necessary.\nPATH variable: ${PATH}";; \
|
||||
esac
|
||||
@mkdir -p $(DESTDIR)${BINDIR}
|
||||
@rm -f $(DESTDIR)${BINDIR}/meli
|
||||
@cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
|
||||
@chmod 755 $(DESTDIR)${BINDIR}/meli
|
||||
mkdir -p $(DESTDIR)${BINDIR}
|
||||
rm -f $(DESTDIR)${BINDIR}/meli
|
||||
cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
|
||||
chmod 755 $(DESTDIR)${BINDIR}/meli
|
||||
|
||||
|
||||
.PHONY: install
|
||||
.NOTPARALLEL: yes
|
||||
install: meli install-bin install-doc
|
||||
@(if [ -z $${NO_MAN+x} ]; then \
|
||||
echo "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
|
||||
$(PRINTF) "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
|
||||
$(PRINTF) "\n or the tutorial in meli(7) (\`man 7 meli\`).\n" ;\
|
||||
fi)
|
||||
@echo " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli.delivery${ANSI_RESET}"
|
||||
@echo " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker."
|
||||
@$(PRINTF) " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli-email.org${ANSI_RESET}\n"
|
||||
@$(PRINTF) " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker.\n"
|
||||
|
||||
.PHONY: dist
|
||||
dist:
|
||||
@git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
|
||||
git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
|
||||
@echo meli-${VERSION}.tar.gz
|
||||
|
||||
AUTHOR ?= grep -m1 authors meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1
|
||||
.PHONY: deb-dist
|
||||
deb-dist:
|
||||
@dpkg-buildpackage -b -rfakeroot -us -uc
|
||||
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_amd64.deb
|
||||
@$(PRINTF) "Override AUTHOR environment variable to set package metadata.\n"
|
||||
dpkg-buildpackage -b -rfakeroot -us -uc --build-by="${AUTHOR}" --release-by="${AUTHOR}"
|
||||
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_`dpkg --print-architecture`.deb
|
||||
|
||||
.PHONY: build-rustdoc
|
||||
build-rustdoc:
|
||||
@echo RUSTDOCFLAGS=\""--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}"\" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" --all-features --no-deps --workspace --document-private-items --open
|
||||
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
|
||||
|
||||
.PHONY: check-tagrefs
|
||||
|
@ -197,3 +227,36 @@ check-tagrefs:
|
|||
else \
|
||||
$(TAGREF_BIN);\
|
||||
fi)
|
||||
|
||||
.PHONY: test-makefile
|
||||
test-makefile:
|
||||
@$(PRINTF) "Checking that current version is detected. "
|
||||
@([ ! -z "${VERSION}" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${RED}ERROR${ANSI_RESET}\nVERSION env var is empty, check its definition.\n" 1>&2
|
||||
@$(PRINTF) "Checking that 'date -I' works on this platform. "
|
||||
@export DATEVAL=$$(printf "%s" ${DATE} | wc -c | tr -d "[:blank:]" 2>&1); ([ "$${DATEVAL}" = "10" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${RED}ERROR${ANSI_RESET}\n'date -I' does not produce a YYYY-MM-DD output on this platform.\n" 1>&2
|
||||
@$(PRINTF) "Checking that the git commit SHA can be detected. "
|
||||
@([ ! -z "$(GIT_COMMIT)" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${YELLOW}WARN${ANSI_RESET}\nGIT_COMMIT env var is empty.\n" 1>&2
|
||||
|
||||
# Checking if mdoc changes produce new lint warnings from mandoc(1) compared to HEAD version:
|
||||
#
|
||||
# example invocation: `mandoc_lint meli.1`
|
||||
#
|
||||
# with diff(1)
|
||||
# ============
|
||||
#function mandoc_lint () {
|
||||
#diff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
|
||||
#}
|
||||
#
|
||||
# with sdiff(1) (side by side)
|
||||
# ============================
|
||||
#
|
||||
#function mandoc_lint () {
|
||||
#sdiff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
|
||||
#}
|
||||
#
|
||||
# with delta(1)
|
||||
# =============
|
||||
#
|
||||
#function mandoc_lint () {
|
||||
#delta --side-by-side <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
|
||||
#}
|
||||
|
|
252
README.md
252
README.md
|
@ -1,53 +1,140 @@
|
|||
# meli [](https://github.com/meli/meli/blob/master/COPYING) [](https://crates.io/crates/meli)
|
||||
# meli   [](https://github.com/meli/meli/blob/master/COPYING) [](https://crates.io/crates/meli) [](ircs://irc.oftc.net:6697/%23meli)
|
||||
|
||||
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (usenet).**
|
||||
**BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
|
||||
|
||||
Community links:
|
||||
[mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
Try an [old, outdated but online and interactive web demo](https://meli-email.org/wasm2.html "online interactive web demo") powered by WebAssembly!
|
||||
|
||||
Main repository: <https://git.meli.delivery/meli/meli>
|
||||
* `#meli` on OFTC IRC
|
||||
* [Mailing lists](https://lists.meli-email.org/)
|
||||
* Main repository <https://git.meli-email.org/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli-email.org/meli/meli/issues "meli gitea issue tracker")<details><summary>Official git mirrors</summary>
|
||||
- <https://codeberg.org/meli/meli>
|
||||
- <https://github.com/meli/meli>
|
||||
- <https://ayllu-forge.org/meli/meli>
|
||||
- <https://gitlab.com/meli-project/meli>
|
||||
</details>
|
||||
|
||||
Official mirrors: <https://github.com/meli/meli>
|
||||
**Table of contents**:
|
||||
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
 |  | 
|
||||
Main view | Compact main view | Compose with embed terminal editor
|
||||
- [Install](#install)
|
||||
- [Build](#build)
|
||||
- [Quick start](#quick-start)
|
||||
- [Supported E-mail backends](#supported-e-mail-backends)
|
||||
- [E-mail submission backends](#e-mail-submission-backends)
|
||||
- [Non-exhaustive list of features](#non-exhaustive-list-of-features)
|
||||
- [HTML Rendering](#html-rendering)
|
||||
- [Documentation](#documentation)
|
||||
|
||||
## Description
|
||||
## Install
|
||||
|
||||
meli aims for configurability, extensibility with sane defaults, and modern
|
||||
practices. It is a mail client for both casual and power users of the terminal.
|
||||
<a href="https://repology.org/project/meli/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/meli.svg" alt="Packaging status table by repology.org" align="right">
|
||||
</a>
|
||||
|
||||
A variety of email workflows and software stacks should be usable with meli.
|
||||
Integrate e-mail storage, sync, tagging system, SMTP client, contact management
|
||||
and editor of your choice to replace the defaults.
|
||||
- `cargo install meli` or `cargo install --git https://git.meli-email.org/meli/meli.git meli` [crates.io link](https://crates.io/crates/meli)
|
||||
- Official Debian packages <https://packages.debian.org/trixie/meli>
|
||||
- AUR (archlinux) <https://aur.archlinux.org/packages/meli>
|
||||
- NetBSD with pkgsrc <https://pkgsrc.se/mail/meli>
|
||||
- OpenBSD ports <https://openports.pl/path/mail/meli>
|
||||
- macOS with MacPorts <https://ports.macports.org/port/meli/>
|
||||
- Nix with Nixpkgs <https://search.nixos.org/packages?query=meli>
|
||||
- [Pre-built debian package, static binaries](https://github.com/meli/meli/releases/ "github releases for meli") for <code>amd64</code>, <code>arm64</code> architectures
|
||||
|
||||
## Build
|
||||
|
||||
Run `make` or `cargo build --release --bin meli`.
|
||||
|
||||
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
|
||||
|
||||
### Cargo Compile-time Features
|
||||
|
||||
`meli` supports opting in and out of features at compile time with cargo features.
|
||||
|
||||
The contents of the `default` feature are:
|
||||
|
||||
```toml
|
||||
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
|
||||
```
|
||||
|
||||
A list of all the features and a description for each follows:
|
||||
|
||||
| Feature flag | Dependencies | Notes |
|
||||
|---------------------------------------------------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| <a name="notmuch-feature">`notmuch`</a> | `maildir` feature | Provides the *notmuch* backend |
|
||||
| <a name="jmap-feature">`jmap`</a> | `http` feature, `url` crate with `serde` feature | Provides the *JMAP* backend |
|
||||
| <a name="smtp-feature">`smtp`</a> | `tls` feature | Integrated async *SMTP* client |
|
||||
| <a name="sqlite3-feature">`sqlite3`</a> | `rusqlite` crate with `bundled-full` feature | Used in caches |
|
||||
| <a name="sqlite3-static-feature">`sqlite3-static`</a> | `rusqlite` crate with `bundled-full` feature | Same as `sqlite3` feature but provided for consistency and in case `sqlite3` feature stops bundling libsqlite3 statically in the future. |
|
||||
| <a name="smtp-trace-feature">`smtp-trace`</a> | `smtp` feature | Connection trace logs on the `trace` logging level |
|
||||
| <a name="gpgme-feature">`gpgme`</a> | | *GPG* use by dynamically loading `libgpgme.so` |
|
||||
| <a name="tls-static-feature">`tls-static`</a> | `native-tls` crate with `vendored` feature | Links with `OpenSSL` statically where it's used |
|
||||
| <a name="http-static-feature">`http-static`</a> | `isahc` crate with `static-curl` feature | Links with `curl` statically |
|
||||
| <a name="dbus-notifications-feature">`dbus-notifications`</a> | `notify-rust` dependency | Uses DBus notifications |
|
||||
| <a name="dbus-static-feature">`dbus-static`</a> | `notify-rust` dependency and enableds its `d_vendored` feature | Includes the dbus library statically. |
|
||||
| <a name="cli-docs-feature">`cli-docs`</a> | `flate2` dependency | Includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` |
|
||||
| <a name="libz-static-feature">`libz-static`</a> | `libz-sys` dependency and enables its `static` feature | Allows for the transitive dependency libz (from `curl`) to be linked statically. |
|
||||
| <a name="static-feature">`static`</a> | enables `tls-static`, `http-static`, `sqlite3-static`, `dbus-static`, `libz-static` features | |
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
# Create configuration file in ${XDG_CONFIG_HOME}/meli/config.toml:
|
||||
$ meli create-config
|
||||
# Edit configuration in ${EDITOR} or ${VISUAL}:
|
||||
$ meli edit-config
|
||||
# Optionally, install manual pages if installed via cargo:
|
||||
$ meli install-man
|
||||
# Ready to go.
|
||||
$ meli
|
||||
# You can read any manual page with the CLI subcommand `man`:
|
||||
$ meli man meli.7
|
||||
# See help output for all options and subcommands.
|
||||
$ meli --help
|
||||
```
|
||||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
|
||||
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
|
||||
Examples for configuration file settings can be found in `meli.conf.examples(5)`
|
||||
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
|
||||
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`.
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config cargo run
|
||||
```
|
||||
|
||||
See [`meli(7)`](./meli/docs/meli.7) for an extensive tutorial and [`meli.conf(5)`](./meli/docs/meli.conf.5) for all configuration values.
|
||||
|
||||
| Main view | Compact main view | Compose with embed terminal editor |
|
||||
|-----------|-------------------|------------------------------------|
|
||||
|  |  |  |
|
||||
|
||||
### Supported E-mail backends
|
||||
|
||||
| Protocol | Support |
|
||||
|:------------:|:----------------|
|
||||
| IMAP | full |
|
||||
| Maildir | full |
|
||||
| notmuch | full* |
|
||||
| mbox | read-only |
|
||||
| JMAP | functional |
|
||||
| NNTP / Usenet| functional |
|
||||
| Protocol | Support |
|
||||
|---------------|------------|
|
||||
| IMAP | full |
|
||||
| Maildir | full |
|
||||
| notmuch | full[^0] |
|
||||
| mbox | read-only |
|
||||
| JMAP | functional |
|
||||
| NNTP / Usenet | functional |
|
||||
|
||||
[^0]: there's no support for searching through all email directly, you'd have to
|
||||
create a mailbox with a notmuch query that returns everything and search
|
||||
inside that mailbox.
|
||||
|
||||
* there's no support for searching through all email directly, you'd have to
|
||||
create a mailbox with a notmuch query that returns everything and search
|
||||
inside that mailbox.
|
||||
|
||||
### E-mail Submission backends
|
||||
--------------------------
|
||||
### E-mail submission backends
|
||||
|
||||
- SMTP
|
||||
- Pipe to shell script
|
||||
- Server-side submission
|
||||
- Server-side submission when supported
|
||||
|
||||
### Non-exhaustive List of Features
|
||||
### Non-exhaustive list of features
|
||||
|
||||
- TLS
|
||||
- email threading support
|
||||
|
@ -70,93 +157,32 @@ and editor of your choice to replace the defaults.
|
|||
- GPG signing, encryption, signing + encryption
|
||||
- GPG signature verification
|
||||
|
||||
## Install
|
||||
- Try an [online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
|
||||
- Pre-built binaries for [pkgsrc](https://pkgsrc.se/mail/meli) and [openbsd ports](https://openports.pl/path/mail/meli).
|
||||
- `cargo install --git https://git.meli.delivery/meli/meli.git meli`
|
||||
- [Download and install pre-built debian package, static linux binary](https://github.com/meli/meli/releases/ "github releases for meli"), or
|
||||
- Install with [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'").
|
||||
## Documentation
|
||||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
|
||||
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
|
||||
Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
|
||||
|
||||
`meli` by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG` environment variable to their locations, i.e.:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config cargo run
|
||||
```
|
||||
|
||||
## Build
|
||||
For a quick start, build and install locally:
|
||||
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
Available subcommands for `make` are listed with `make help`.
|
||||
The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
`meli` requires rust 1.65 and rust's package manager, Cargo.
|
||||
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||
|
||||
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
|
||||
Run `make install` to install the binary and man pages.
|
||||
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=$HOME/.local install`.
|
||||
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
|
||||
### Build features
|
||||
|
||||
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
|
||||
|
||||
- `gpgme` enables GPG support via `libgpgme` (on by default)
|
||||
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
|
||||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
|
||||
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
|
||||
|
||||
### Build Debian package (*deb*)
|
||||
|
||||
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
|
||||
|
||||
A `*.deb` package can be built with `make deb-dist`
|
||||
|
||||
### Using notmuch
|
||||
|
||||
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
|
||||
In Debian-like systems, install the `libnotmuch5` packages.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
### Using GPG
|
||||
|
||||
To use the optional gpg feature, you must have `libgpgme` installed in your system.
|
||||
In Debian-like systems, install the `libgpgme11` package.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
### HTML Rendering
|
||||
## HTML Rendering
|
||||
|
||||
HTML rendering is achieved using [w3m](https://github.com/tats/w3m) by default.
|
||||
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./meli/docs/meli.conf.5)).
|
||||
|
||||
# Development
|
||||
|
||||
Development builds can be built and/or run with
|
||||
## Documentation
|
||||
|
||||
```
|
||||
cargo build
|
||||
cargo run
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
|
||||
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
|
||||
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
|
||||
|
||||
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, or use the `[-c, --config]` argument:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config meli
|
||||
```
|
||||
|
||||
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
|
||||
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
|
||||
or
|
||||
|
||||
Code style follows the `rustfmt.toml` file.
|
||||
```sh
|
||||
meli -c ./test_config
|
||||
```
|
||||
|
|
166
cliff.toml
Normal file
166
cliff.toml
Normal file
|
@ -0,0 +1,166 @@
|
|||
# configuration for https://github.com/orhun/git-cliff
|
||||
|
||||
[remote.gitea]
|
||||
owner = "meli"
|
||||
repo = "meli"
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
# note that the - before / after the % controls whether whitespace is rendered between each line.
|
||||
# Getting this right so that the markdown renders with the correct number of lines between headings
|
||||
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
|
||||
# is intentional as this escapes any backticks in the commit body.
|
||||
body = """
|
||||
|
||||
{% if not version %}
|
||||
## Unreleased
|
||||
{% else %}
|
||||
## [{{ version }}]({{ "https://git.meli-email.org/meli/meli/releases/tag/" ~ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif %}{% if get_env(name = "FRIENDS", default = "") != "" %}
|
||||
Contributors in alphabetical order:
|
||||
|
||||
{{ get_env(name = "FRIENDS") }}
|
||||
{%- endif -%}{% if gitea and gitea.contributors %}{% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* [@{{ contributor.username }}](https://git.meli-email.org/{{ contributor.username }}) made their first contribution in [#{{ contributor.pr_number }}]({{ "https://git.meli-email.org/meli/meli/pulls/" ~ contributor.pr_number }})
|
||||
{%- endfor -%}{%- endif -%}{% macro commit(commit) -%}
|
||||
- [**`{{ commit.id | truncate(length=8, end="") }}`**]({{ "https://git.meli-email.org/meli/meli/commit/" ~ commit.id }}) {% if commit.scope %}*({{commit.scope | lower }})* {% endif %}`{{ commit.message | split(pat="\n")| first }}`{% if commit.remote and commit.remote.pr_number and commit.remote.pr_title %} in PR [`#{{ commit.remote.pr_number }}` "{{ commit.remote.pr_title }}"]({{ "https://git.meli-email.org/meli/meli/pulls/" ~ commit.remote.pr_number }}){%- endif -%}{% endmacro -%}
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endfor -%}
|
||||
{% for commit in commits %}
|
||||
{%- if not commit.scope %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
"""
|
||||
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
|
||||
|
||||
<!-- generated by git-cliff <https://git-cliff.org> -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# don't parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = false
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = false
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meli-email.org/meli/meli/issues/${2}))" }, # Replace the issue number with the link
|
||||
{ pattern = " +", replace = " "}, # Replace multiple spaces with a single space
|
||||
{ pattern = "`[^`]+`", replace_command = "pandoc -f commonmark -t plain" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^CHANGELOG", skip = true },
|
||||
{ message = "(?i)^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
{ message = "^.github", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^.gitea", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "(?i)^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^scripts", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = '(?i)^chore', group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^debian", group = "<!-- 06 -->Packaging" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "(?i)readme", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "(?i)anpage", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "(?i)anual", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "meli.[17]", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "meli.conf.5", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "meli-themes.5", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "README.md", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "BUILD.md", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "DEVELOPMENT.md", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^[^.]*.rs:", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)^refactor\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)lints?\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)move\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)replace\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)remove\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)refactor\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)rename\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)formatting\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)cleanups?\b', group = "<!-- 02 -->Refactoring" },
|
||||
{ message = '(?i)fix\b', group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = '(?i)fixups?\b', group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "(?i)implement", group = "<!-- 00 -->Added" },
|
||||
{ message = '(?i)add\b', group = "<!-- 00 -->Added" },
|
||||
{ message = '(?i)^update\b', group = "<!-- 02 -->Changes" },
|
||||
{ message = '(?i)\bdependency\b', group = "<!-- 02 -->Changes" },
|
||||
{ message = "^feat", group = "<!-- 00 -->Added" },
|
||||
{ message = '(?i)retry\b', group = "<!-- 02 -->Changes" },
|
||||
{ message = "^conf", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^contacts?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^compos[ie]?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^types", group = "<!-- 02 -->Changes" },
|
||||
{ message = '(?i)^use', group = "<!-- 02 -->Changes" },
|
||||
{ message = "^terminal", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^listing", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mail", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^utilities", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mail/view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^backends?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^commands?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^actions?", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^log", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^pgp", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^gpgme", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^manage", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^smtp", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mbox", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^jmap", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^imap", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^nntp", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^notmuch", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^melib", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^meli", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^accounts", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^embedded", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^jobs", group = "<!-- 02 -->Changes" },
|
||||
{ message = ".*", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]+|alpha-[0-9]+"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = "v[^-]+-rc[.]?[0-9]+"
|
||||
# regex for skipping tags
|
||||
#skip_tags = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
69
codemeta.json
Normal file
69
codemeta.json
Normal file
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"@context": ["https://doi.org/10.5063/schema/codemeta-2.0", "http://schema.org/"],
|
||||
"@type": "SoftwareSourceCode",
|
||||
"applicationCategory": "E-mail client",
|
||||
"author": [
|
||||
{
|
||||
"@id": "https://pitsidianak.is/",
|
||||
"@type": "Person",
|
||||
"name": "epilys",
|
||||
"email": "manos@pitsidianak.is",
|
||||
"familyName": "Pitsidianakis",
|
||||
"givenName": "Manos",
|
||||
"url": "https://pitsidianak.is/"
|
||||
}
|
||||
],
|
||||
"codeRepository": "https://git.meli-email.org/meli/meli.git",
|
||||
"dateCreated": "2016-04-25",
|
||||
"dateModified": "2024-11-27",
|
||||
"datePublished": "2017-07-23",
|
||||
"description": "BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).",
|
||||
"downloadUrl": "https://git.meli-email.org/meli/meli/archive/v0.8.10.tar.gz",
|
||||
"identifier": "https://meli-email.org/",
|
||||
"isPartOf": "https://meli-email.org/",
|
||||
"keywords": [
|
||||
"e-mail",
|
||||
"email",
|
||||
"mail",
|
||||
"terminal user interface",
|
||||
"client",
|
||||
"mua",
|
||||
"mail user agent",
|
||||
"smtp",
|
||||
"imap",
|
||||
"jmap",
|
||||
"mbox",
|
||||
"maildir",
|
||||
"nntp"
|
||||
],
|
||||
"license": [
|
||||
"https://spdx.org/licenses/EUPL-1.2",
|
||||
"https://spdx.org/licenses/GPL-3.0-or-later"
|
||||
],
|
||||
"name": "meli",
|
||||
"operatingSystem": [
|
||||
"Linux",
|
||||
"macOS",
|
||||
"OpenBSD",
|
||||
"NetBSD"
|
||||
],
|
||||
"programmingLanguage": "Rust",
|
||||
"relatedLink": [
|
||||
"https://lists.meli-email.org/",
|
||||
"https://codeberg.org/meli/meli",
|
||||
"https://github.com/meli/meli",
|
||||
"https://gitlab.com/meli-project/meli",
|
||||
"https://crates.io/crates/meli",
|
||||
"https://packages.debian.org/trixie/meli",
|
||||
"https://pkgsrc.se/mail/meli",
|
||||
"https://openports.pl/path/mail/meli",
|
||||
"https://ports.macports.org/port/meli/",
|
||||
"https://search.nixos.org/packages?query=meli"
|
||||
],
|
||||
"version": "0.8.10",
|
||||
"contIntegration": "https://git.meli-email.org/meli/meli/actions",
|
||||
"developmentStatus": "active",
|
||||
"issueTracker": "https://git.meli-email.org/meli/meli/issues",
|
||||
"readme": "https://git.meli-email.org/meli/meli/raw/tag/v0.8.10/README.md",
|
||||
"buildInstructions": "https://git.meli-email.org/meli/meli/raw/tag/v0.8.10/BUILD.md"
|
||||
}
|
50
contrib/README.md
Normal file
50
contrib/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!-- SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later -->
|
||||
# Useful scripts and files for use with `meli`
|
||||
|
||||
This directory includes various useful scripts and files that are contributed
|
||||
by the community and not actively maintained or supported.
|
||||
|
||||
If you believe something in this directory needs updates to work with the
|
||||
current version of `meli` or there are bugs that need fixing, please file an
|
||||
issue on our issue tracker!
|
||||
|
||||
## Connecting to a Gmail account with OAUTH2
|
||||
|
||||
The script [`./oauth2.py`](./oauth2.py) is a helper script to authenticate to a Gmail account using IMAP OAUTH2 tokens.
|
||||
|
||||
See [`meli.conf(5)`](../meli/docs/meli.conf.5) for documentation.
|
||||
|
||||
If the script does not work and you're certain it's because it needs changes to
|
||||
work with Google's servers and not a user error on your part, please file a bug
|
||||
on our issue tracker!
|
||||
|
||||
## Using `meli` for `mailto:` links
|
||||
|
||||
To use `meli` to open `mailto:` links from your browser place the [`mailto-meli`](./mailto-meli) and [`mailto-meli-expect`](./mailto-meli-expect) scripts into `/usr/bin`
|
||||
(or `.local/bin`, and adjust the path in the script accordingly).
|
||||
|
||||
Ensure all scripts are executable by your user account, if not set the permissions accordingly:
|
||||
|
||||
```sh
|
||||
chmod u+x /path/to/mailto-meli
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```sh
|
||||
chmod u+x /path/to/mailto-meli-expect
|
||||
```
|
||||
|
||||
Then set `mailto-meli` as program to open `mailto` links
|
||||
in your browser.
|
||||
|
||||
E.g. in Firefox this can be done under "Settings" (`about:preferences`) which you can access from the menu button or `Edit -> Settings`.
|
||||
|
||||
```text
|
||||
General -> Applications -> Content-Type: mailto.
|
||||
```
|
||||
|
||||
You can test that it works by clicking the system menu entry `File -> Email link...`.
|
||||
|
||||
_NOTE_: that you need to have the [`expect`](https://en.wikipedia.org/wiki/Expect) binary installed for this to work.
|
||||
`expect` is a scripting language used for interactive with interactive terminal applications like `meli`.
|
21
contrib/mailto-meli
Executable file
21
contrib/mailto-meli
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env sh
|
||||
#
|
||||
# mailto-meli -- wrapper to use meli as mailto handler
|
||||
# To use meli as mailto: handler point your browser to use this as application for opening
|
||||
# mailto: links.
|
||||
# Note: This assumes that x-terminal-emulator supports the "-e" flag for passing along arguments.
|
||||
|
||||
# Copyright: 2024 Matthias Geiger <werdahias@debian.org>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Check if mailto-meli and expect are present
|
||||
if ! command -v mailto-meli > /dev/null 2>&1
|
||||
then echo "mailto-meli not found" && exit 1
|
||||
else
|
||||
if ! command -v expect > /dev/null 2>&1
|
||||
then echo "expect not found" && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
exec x-terminal-emulator -e mailto-meli-expect "$@"
|
||||
|
18
contrib/mailto-meli-expect
Executable file
18
contrib/mailto-meli-expect
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env -S expect -f
|
||||
# Copyright 2024 Manos Pitsidianakis
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
# Trap window resize signal
|
||||
trap {
|
||||
set rows [stty rows]
|
||||
set cols [stty columns]
|
||||
stty rows $rows columns $cols < $spawn_out(slave,name)
|
||||
} WINCH
|
||||
# send the input with human-like delay:
|
||||
set send_human {.001 .003 0.01 .005 .005}
|
||||
spawn meli
|
||||
send -h ":mailto "
|
||||
send -h [lindex $argv 0]
|
||||
send -h "\n"
|
||||
interact
|
875
debian/changelog
vendored
875
debian/changelog
vendored
|
@ -1,3 +1,876 @@
|
|||
meli (0.8.10-1) bookworm; urgency=low
|
||||
|
||||
Highlights:
|
||||
===========
|
||||
|
||||
- added pipe-attachment command
|
||||
- added sample scripts for using meli as a mailto scheme handler in
|
||||
contrib/
|
||||
- fixed GPG encryption with libgpgme
|
||||
|
||||
Contributors in alphabetical order:
|
||||
===================================
|
||||
|
||||
- Manos Pitsidianakis
|
||||
- Matthias Geiger
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 5e77821f mail/view: add pipe-attachment command in PR #540
|
||||
"mail/view: add pipe-attachment command"
|
||||
- fa896f6b contrib: add mailto: scheme handler scripts
|
||||
- 00ce9660
|
||||
melib/backends: add as_any/as_any_mut methods to BackendMailbox
|
||||
- fd243fa5 maildir: add mailbox creation tests
|
||||
- de65eec3 meli/accounts: add mailbox_by_path() tests in PR #535
|
||||
"Rework maildir mailbox path logic, add tests"
|
||||
- 6b363601 melib/gpgme: impl Display for gpgme::Key
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 60c90d75 melib/attachments: ensure MIME boundary prefixed with CRLF
|
||||
- 3433c5c3 compose/pgp: rewrite key selection logic in PR #541 "More
|
||||
gpgme/PGP fixes again"
|
||||
- 12de82e7 melib/conf: fix mutt_alias_file not being validated in PR
|
||||
#550 "Remove sealed_test dependency"
|
||||
- c8e055a7 Fix version migrations being triggered backwards in PR #557
|
||||
"Fix version migrations being triggered backwards"
|
||||
- efab99fd
|
||||
terminal: check for NO_COLOR env var without unicode validation
|
||||
- 36a63e88 melib/maildir: rewrite create_mailbox()
|
||||
- fcab855f view: ensure envelope headers are always populated in PR
|
||||
#538 "view: ensure envelope headers are always populated"
|
||||
- 84564f44 mailcap: don't drop File before opening it in PR #552
|
||||
"mailcap: don't drop File before opening it"
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- ed85da51 Remove sealed_test dependency
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 03df2ac1 meli/utilities: add print utilities for tests
|
||||
- 18e9d5c1 conf.rs: impl From<melib::AccountSettings> for AccountConf
|
||||
- 1f2fec19 Fix 1.83.0 lints in PR #536 "CI: Add action to check for
|
||||
DCO signoffs in PRs"
|
||||
- 192ecea2 compose/gpg.rs: Fix msrv regression
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- 4a61a4b8 melib: include README.md as preamble of crate rustdocs
|
||||
- 80e53471 BUILD.md: move melib specific stuff to melib/README.md
|
||||
- 91a17ece melib/README.md: mention sqlite3-static feature
|
||||
- b77a691b meli/README.md: Add cargo features section in PR #549
|
||||
"Document cargo features in READMEs"
|
||||
- 91dc271d contrib: add a README.md file
|
||||
- 2e900be6 contrib/README.md: add section about oauth2.py
|
||||
- 07812d2c contrib/README.md: elaborate a bit about mailto in PR #545
|
||||
"Add external mailto: handler support via scripts in contrib"
|
||||
- e784e8d2 scripts: add markdown_doc_lints.py
|
||||
|
||||
Continuous Integration
|
||||
======================
|
||||
|
||||
- 77629851 CI: Add action to check for DCO signoffs in PRs
|
||||
- f944ebed CI: Add error msg when cargo-derivefmt check fails
|
||||
- d49344f9 CI: Move MSRV checks from manifest to lints in PR #553
|
||||
"ci-workflow-fixes"
|
||||
- ece6bfc2 CI: non-zero exit if cargo-derivefmt-* targets fail
|
||||
- 2257b91b CI: add actions/cache steps in PR #554 "CI: add
|
||||
actions/cache steps"
|
||||
- a1c9524f CI: fix check_dco.sh not working with other repos in PR
|
||||
#555 "CI: fix check_dco.sh not working with other repos"
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Fri, 06 Dec 2024 07:03:58 +0200
|
||||
|
||||
meli (0.8.9-1) bookworm; urgency=low
|
||||
|
||||
This is mostly a fixups release.
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- cf16bf65 meli/sqlite3: add tests for reindexing
|
||||
- a389772d accounts: suggest tips on mailbox_by_path error
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 25f0a3f8 conf/terminal: fix serde of ProgressSpinnerSequence
|
||||
- c375b48e terminal: fix Synchronized Output response parsed as input
|
||||
in PR #523 "terminal: fix Synchronized Output response parsed as
|
||||
input"
|
||||
- b7e215f9
|
||||
melib/utils: fix test_fd_locks() on platforms without OFD support in
|
||||
PR #524 "melib/utils: fix test_fd_locks() on platforms without OFD
|
||||
support"
|
||||
- 25c32a6b meli/docs/meli.conf.examples.5: fix .Dt macro arguments
|
||||
- 18ae5848 meli: fix reindex of previously indexed account with sqlite3
|
||||
backend
|
||||
- 13e917d9 Fix some compilation errors with cfg feature attrs in PR #531
|
||||
"accounts: suggest tips on mailbox_by_path error"
|
||||
- 8c176d38 contacts/editor: fix crash on saving contact in PR #532
|
||||
"contacts/editor: fix crash on saving contact"
|
||||
- fb5a88c2
|
||||
melib/collection: ensure mailbox exists when inserting new envelopes
|
||||
in PR #529 "Small account stuff fixes"
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 7f8f1cf6 melib/gpgme bindings renewal in PR #533 "melib/gpgme
|
||||
bindings renewal"
|
||||
- 9b7825bc Update futures-util dep, remove stderrlog dep
|
||||
- 4be69360 Remove obsolete "encoding" dependency in PR #530
|
||||
"Remove/update obsolete dependencies"
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 5af6e059 meli/accounts: use Arc<str> for account name
|
||||
- 567270e1 melib: use Vec instead of SmallVec for search results
|
||||
- 2bd8d7ba
|
||||
conf/tests.rs: Rename test functions to follow path convention
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- 97242482 meli/docs: add meli.conf.examples to CLI and tests
|
||||
- 0f096338 README.md: Update ways to install, add gitlab mirror link
|
||||
in PR #528 "Integrate meli.conf.examples.5 into CLI and build, also
|
||||
update README with installation instructions"
|
||||
|
||||
Continuous Integration
|
||||
======================
|
||||
|
||||
- 630df308 CI: Add arm64 runners in job matrices in PR #527 "CI: Add
|
||||
arm64 runners in job matrices"
|
||||
- 49ecbb56 CI: .gitea/Makefile.lint: check if nightly exists
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Wed, 27 Nov 2024 16:16:06 +0200
|
||||
|
||||
meli (0.8.8-1) bookworm; urgency=low
|
||||
|
||||
WARNING: This release contains a breaking change in the configuration
|
||||
file: a global composing option is not required anymore. Now, composing
|
||||
options are per account.
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- f3d59ebf accounts: add force: bool arg to load()
|
||||
- 33836a32 melib/error: add WrapResultIntoError helper trait
|
||||
- 3216324c melib/mbox: impl FromStr for MboxFormat
|
||||
- 94f345d7 Implement mailbox renaming command
|
||||
- 8d45ecc1 melib/error: add related_path field
|
||||
- bf3a4c5d error: add ErrorChainDisplay struct for better output
|
||||
- 6be5fd26
|
||||
themes: add inheritance, and use themes when initializing grids
|
||||
- 0ee7fc4d Print clickable path links with subcommands
|
||||
- aed7a60f samples: add ibm-modern theme in PR #469 "conf-refactor"
|
||||
- 4bbf446b utils: add unix file locks module
|
||||
- 6fbf569f search: add Message-ID, and other header search support
|
||||
- 26d33ce5 address: add separator argument to display_slice()
|
||||
- 32e3be8b
|
||||
sqlite3: add optional directory field in DatabaseDescription
|
||||
- dbbb1529 Add missing ComponentUnrealize handlers
|
||||
- 87d2cec9 Add sealed_test dependency
|
||||
- 604ae111 Impl From<&[u8]> for u64-based hash newtypes
|
||||
- 8205c7f5 melib: add JsContact module in PR #479 "view-filters"
|
||||
- 2af5c8b6 terminal: add QuerySynchronizedOutputSupport WIP
|
||||
- 5c4faea5 Add transpose shortcut and tests for text field
|
||||
- e9b87b2e melib/maildr: add rename_regex config option
|
||||
- 8f0e1d66 Add human-readable identifiers in temp draft files
|
||||
- 601e3711 Add vCard exports
|
||||
- 719e2eb2 listing: add customizable view divider like sidebar's in PR
|
||||
#485 "listing: add customizable view divider like sidebar's"
|
||||
- ba3ad8ed listing: always show mail_view_divider in PR #486 "listing:
|
||||
always show mail_view_divider"
|
||||
- 46b2c3b1 Add listing.thread_layout config flag in PR #487 "Add
|
||||
listing.thread_layout config flag"
|
||||
- aaea3a5a nntp: add timeout conf flag
|
||||
- d4636bcc nntp: interpret IMPLEMENTATION cap as metadata
|
||||
- 5f120309 nntp: add select_group_by_name() method
|
||||
- 9a9cd03d nntp: add NntpType::article_message_id() method
|
||||
- 7cfcbb7a Add patch_retrieve module in PR #489 "Add patch_retrieve
|
||||
module"
|
||||
- c82341f3 File: try trimming filename if ENAMETOOLONG
|
||||
- 23395491 compose/pgp: add encrypt_for_self flag
|
||||
- 0b6988b7 gpgme: add always trust flag to encrypt op
|
||||
- be3b3ef8 melib/utils: add fnmatch(3) interface
|
||||
- 32f7e50f Add version migration support
|
||||
- a6c7621c jscontact: add {created,updated} fields
|
||||
- 39592ad0 jmap: implement changing mailbox subscription
|
||||
- ca7eb792 jmap: Implement deleting email
|
||||
- b8e841bb jmap: implement mailbox deletion
|
||||
- 77e7c3df Add support for signatures in PR #500 "Add support for
|
||||
signatures"
|
||||
- dba5b68b components: add prelude module
|
||||
- f656aff0 composer: add discard-draft command
|
||||
- 789a88b2 shortcuts: add select_motion equivalent to select_entry
|
||||
- cb2dd5de listing/threaded: impl missing select functionality in PR
|
||||
#514 "listing/threaded: impl missing filter functionality"
|
||||
- c1901c96
|
||||
melib/email/compose: add Content-Type header for utf8 text plain attachments
|
||||
- 0e77bd5b
|
||||
melib/email/compose/tests: add multipart mixed attachment test in PR
|
||||
#515 "Fix incorrect multipart/mixed rendering when sending text with
|
||||
attachments under certain circumstances"
|
||||
- 7b1be139 melib: make mbox backend build by default
|
||||
- 7ff1db14 manage-mailboxes: add delete option in PR #520
|
||||
"manage-mailboxes: add delete option"
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 6b05279a Update time dep to fix 1.80.0 breakage
|
||||
- 2084ce93 Fix invalid cfg feature combinations for macos in PR #471
|
||||
"Fix invalid cfg feature combinations for macos"
|
||||
- 4707ec9f text/line_break: fix ReflowState::{No,All} break
|
||||
- 86e25bc0 sqlite: fix database reset sequence
|
||||
- 4d4e189c imap: code style fixups
|
||||
- 335cca88 listing: fix highlight_self flag off by one error in PR
|
||||
#477 "listing: fix highlight_self flag off by one error"
|
||||
- 80915832 mailto: rewrite parsing in PR #480 "mailto-rewrite"
|
||||
- 65b32e77 subcommands: Fix wrong help info in imap-shell prompt
|
||||
- d0c81749 conf::data_types: minor style and error msg fixups
|
||||
- 7dbee81d view: fix nested filter jobs never being completed
|
||||
- f78884ce melib/nntp: fix an ancient FIXME
|
||||
- e0cfe8e4 Fix compilation for 32-bit architectures in PR #492 "Fix
|
||||
compilation for 32-bit architectures"
|
||||
- 1b708a99 melib: attempt FromSql from Blob for u64 hash in PR #506
|
||||
"melib: attempt FromSql from Blob for u64 hash"
|
||||
- 6c315580 compose: fix add-attachment-file-picker
|
||||
- c6e9e424 listing/threaded: impl missing filter functionality
|
||||
- e7a164de Configure some gpgme stuff under gpgme feature
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 8e300c46 melib/jmap: call req text(). asap
|
||||
- 374ea8ba accounts: extract tests to tests.rs file
|
||||
- 7020cd66 meli: derive PartialEq/Eq for some types
|
||||
- 69065859 accounts: split mailbox to enum out of JobRequest
|
||||
- 14f2d911 melib/backends: change RefreshEvent field decl order
|
||||
- 56b1bf28 meli/accounts: batch process refresh events
|
||||
- 6513c188 melib/imap: on sync only update exists/unseen if loaded
|
||||
- a8dad317 melib/imap: renamed cache module to sync
|
||||
- 9e9c04a3 Update indexmap dep to 2.3.0
|
||||
- 2b3828d8 Update futures dependency to 0.3.30
|
||||
- 84812941
|
||||
melib/jmap: do not serialize server-set fields in Set create
|
||||
- eda6620c jmap: detect supported Auth schemes on connect in PR #467
|
||||
"jmap: detect supported Auth schemes on connect"
|
||||
- 35f12b15 embedded: prevent double-close of pty fd in PR #468
|
||||
"embedded: prevent double-close of pty fd"
|
||||
- 0bed37b5 melib: use IndexMap in conf fields
|
||||
- f3ad824d meli: use itoa to format offset indices in listings
|
||||
- 1cfb0b15 Update nix dependency to 0.29.0
|
||||
- 9c1b4424 jobs: make cancel flag an AtomicBool
|
||||
- f06a9072 jmap: fetch mailbox with receivedAt descending sort
|
||||
- 53b0d035 accounts: cancel any previous mailbox fetches
|
||||
- 60833ee5 accounts: make mailbox available as soon as possible
|
||||
- 28f45805 mail/view: try cancel env fetch on Drop
|
||||
- 2bb9b20d mail/view: do not highlight reply subjects in thread
|
||||
- a4f344b3 Use create_new to avoid overwriting files
|
||||
- d6197e8b listing: clear count modifier on Home/End
|
||||
- b798ca4a imap: return cached response in {select,examine}_mailbox()
|
||||
- 151fcebe imap: use BTreeMap for message sequence number store
|
||||
- e48fcc33 imap/protocol_parser: also populate other_headers
|
||||
- 1e11c29c imap: resync cache first when fetching a mailbox
|
||||
- 1779ad5d imap: interpret empty server response as BYE
|
||||
- 2d320688 mail/listing: pre-lookup conf values
|
||||
- 4e967280 nntp: don't needlessly select group before ARTICLE in PR
|
||||
#473 "Various"
|
||||
- 67b88d24 Update polling dependency from "2.8" to "3"
|
||||
- 14d74f36 Update smol dependency from "1" to "2"
|
||||
- b950fcea melib: Use IndexMap in VCard
|
||||
- 32acc347 view: show signature verification properly
|
||||
- ac1349b8 command: alias pwd to cwd
|
||||
- 7c056e4b Retry loading mailbox on recoverable error in PR #481
|
||||
"Retry loading mailbox on recoverable error"
|
||||
- cbafdcf7 terminal: color report WIP
|
||||
- 4a26cfa1 logging: disable tracing from output
|
||||
- 90974e7c imap: cache miss if row env hash != row hash
|
||||
- 4c44c440 melib: #[ignore] shellexpand tests
|
||||
- dc9e91df contacts/editor: Use FormButtonAction in form
|
||||
- c0511901 Update debian/meli.{docs,examples} and Cargo exclude
|
||||
- 592ce159 mbox: use Uuid::nil() as default envelope from
|
||||
- 6eeb4571 nntp: make all fields public
|
||||
- b27bac7f nntp: use DEFLATE when available by default
|
||||
- 128b959f
|
||||
nntp: prepend Newsgroups header if missing on NntpType::submit()
|
||||
- a69122f8
|
||||
pgp: use default sign/encrypt keys when no keys are selected
|
||||
- e6fa7093 view/envelope: trim headers values to 3 lines maximum
|
||||
- 7f0157a9 compose: make dialogs bigger in height in PR #490 "pgp: use
|
||||
default sign/encrypt keys when no keys are selected"
|
||||
- e032acfa view: pass filtered body to Composer as reply text in PR
|
||||
#493 "view: pass filtered body to Composer as reply text"
|
||||
- 49dcbc5e terminal: Extend Ask default actions, prompts
|
||||
- cd2e4bf3 melib/utils: vendor urn crate
|
||||
- 5915f125 backends: use IsSubscribedFn in method signatures
|
||||
- 4f927bbe nntp: properly return all nntp mailboxes
|
||||
- b930cb49 maildir: do not use rename_regex when only updating flags
|
||||
- 27486f29 Accept newer versions of base64 dependency
|
||||
- c3cac77d Update imap-codec dependency to 2.0.0-alpha.4
|
||||
- 05f404ba jobs: do not use AtomicU64 in PR #505 "jobs: do not use
|
||||
AtomicU64"
|
||||
- 46916895 melib/gpgme: s/NULL/NUL when referring to NUL byte
|
||||
- 81ace71b terminal/embedded: lift error checking earlier
|
||||
- 24114811 manage: parse scroll_{left,right} actions
|
||||
- d2559e42 imap: return all mailboxes, not just subscribed ones in PR
|
||||
#509 "compose: fix add-attachment-file-picker"
|
||||
- 320fddad melib/gpgme: disable layout tests on non-x86_64 hosts in PR
|
||||
#511 "melib/gpgme: disable layout tests on non-x86_64 hosts"
|
||||
- bcbcb012
|
||||
melib/email/compose: ensure boundary always prefixed with CRLF
|
||||
- d21c686d melib/attachments: Make AttachmentBuilder::set_raw generic
|
||||
- d5d34579 melib/email/compose/tests: normalise test fn names
|
||||
- e9ec6761 melib: make base64 dep mandatory
|
||||
- 30405216 melib: make notmuch feature depend on maildir feature
|
||||
- 35fa8e94 melib/imap: gracefully retry without DEFLATE on BYE in PR
|
||||
#517 "Fix some unrelated bugs I found while debugging build failure
|
||||
on armhf"
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 20d73292 melib: replace async-stream dep with async-fn-stream
|
||||
- 201081b6 meli/command: move tests to tests.rs
|
||||
- 84cfa358 conf: remove need for global send_mail setting
|
||||
- 7be8912c Cargo.tomls: make formatting more consistent
|
||||
- e6877e89 melib/jmap: refactor some parser imports
|
||||
- f7ec6d6b melib/jmap: implement mailbox rename
|
||||
- 15d24ab0 meli/jobs: refactor spawn_{blocking,specialized} to spawn()
|
||||
- 6ee148c0 Fix 1.80.0 clippy lints
|
||||
- de72bc6a melib/error.rs: move network stuff to submodule
|
||||
- a214a35c conf: refactor into submodules
|
||||
- 978cefbb Replace Escape ascii char with hex literal
|
||||
- 4b959f5c Remove pcre feature/dependency
|
||||
- 036586a2 Update serde dependency to 1.0.205
|
||||
- 191725b5
|
||||
Fix some borrow checker error/warnings from upcoming 2024 edition
|
||||
- 11798be8 Replace Envelope::message_id_display() with Display impls
|
||||
- 394236ba email/address: Refactor References struct
|
||||
- a7c73fc8 gpgme: refactor Rust interface, add tests
|
||||
- 41e1fdd5 Fix cargo-derivefmt lints
|
||||
- a44486d9 imap: fix minor clippy lint
|
||||
- 0c0f8210 Add a "move to Trash" shortcut
|
||||
- d20a9d0a Fix new clippy lints
|
||||
- e9a72072 Remove unused/obsolete plugins code and mentions
|
||||
- 2ddd28ee main.rs: always send a JobFinished event to all components
|
||||
- 571ae390 pager.rs: don't set self dirty after filter selector in PR
|
||||
#488 "view: fix nested filter jobs never being completed"
|
||||
- 6bc0caf4 melib: remove redundant get_path_hash macro
|
||||
- fc3308e4 melib: Add Mail::as_mbox() method
|
||||
- b1f24cbe view/filters: forward events on child filters
|
||||
- 1b201bf6 Remove GlobMatch trait, replace usage with Fnmatch
|
||||
- 8af003ab Rename addressbook stuff to "contacts"
|
||||
- 2069b4da errors: impl From<xdg::BaseDirectoriesError>
|
||||
- 7dee32ae contacts: refactor Card to its own module
|
||||
- 6d0d9680 jmap: move EmailObject state to Store
|
||||
- 0c590bbc contact-editor: remove empty space in PR #495 "Add version
|
||||
migration support"
|
||||
- b2200ec3 Remove unused smtp tests in PR #501 "Apply patches from
|
||||
upstream debian package"
|
||||
- ae294945 remove unused module file
|
||||
- 3558db51 Move jobs and mailbox management Components together
|
||||
- 3a931035 command: move Composer actions under TabActions
|
||||
- 441fda56 terminal: move TextPresentation trait to melib
|
||||
- ee897942 lints: deny clippy::or_fun_call
|
||||
- 0d088962 lints: Address clippy::too_long_first_doc_paragraph
|
||||
- ecc9b482 Small repo cleanups
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- a83b4176 meli.1: small fixes
|
||||
- 72dea6f3 Manpage fixes
|
||||
- a55f65e1
|
||||
meli.conf.5: Fix wrong default value type in default_header_values
|
||||
- 57b45a9c docs/historical-manpages: add DEP5 copyright file
|
||||
- 00236b86 docs: add meli.conf.examples(5) WIP
|
||||
- b88dc441 Comment out svgfeature; no need to ship it in PR #482
|
||||
"milestone/0.8.8"
|
||||
- b048c95a BUILD.md: add instructions for Android build
|
||||
- 593ed22b pgp: perform gpgme's sign+encrypt manually in PR #494 "pgp:
|
||||
perform gpgme's sign+encrypt manually"
|
||||
- 50922d97 melib/README.md: update and fix feature table
|
||||
- b912aabc docs: add examples of file picker usage in PR #516 "docs:
|
||||
add examples of file picker usage"
|
||||
|
||||
Packaging
|
||||
=========
|
||||
|
||||
- b55edd47 debian: update meli.docs and add meli.manpages
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- 1232e16a scripts/make_html_manual_page.py: don't prettify
|
||||
- 6d520605 Vendor vobject crate
|
||||
- b33433e4
|
||||
Don't create backends as Box<dyn MailBackend>, but as Box<Self>
|
||||
- 2001b4dd Make subscribed_mailboxes conf val optional
|
||||
- 6cfe4da0 Enable rusqlite feature "modern_sqlite" always
|
||||
- 707a129e Coalesce repeating TUI notification messages
|
||||
- f036f95e scripts: add generate_release_changelog_entry.sh
|
||||
|
||||
Continuous Integration
|
||||
======================
|
||||
|
||||
- 4684b601 CI: remove env vars from action names in PR #458 "Minor QoL
|
||||
fixes"
|
||||
- 7419b465 CI: unpin rust version after updating time dependency in PR
|
||||
#460 "Update time dep to fix 1.80.0 breakage"
|
||||
- 77da86eb CI: Update cargo-derivefmt version
|
||||
- 1b3f2732 CI: Move build.yaml actions to Makefile.build
|
||||
- 598a70f9 CI: move lints.yaml actions to Makefile.lint
|
||||
- 7e800a8f
|
||||
CI: move manifest_lints.yaml actions to Makefile.manifest-lints
|
||||
- 98652110 CI: prepend printf commands with @
|
||||
- ad79bf84 .gitea/Makefile.lint: attempt cargo-fmt with +nightly
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Tue, 19 Nov 2024 14:09:13 +0200
|
||||
meli (0.8.7-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
||||
- Andrei Zisu
|
||||
- Damian Poddebniak
|
||||
- Herby Gillot
|
||||
- Manos Pitsidianakis
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 9fcb0a04 Add cargo-deny configuration file deny.toml
|
||||
- 7e8d19af Add Envelope::sender_any
|
||||
- 9ab404c5 Add pgp signed attachment support
|
||||
- b4579075 Allow XOAUTH2 string passed as string
|
||||
- 0ffe7fa5 Add text/plain or text/html arg for text decoding
|
||||
- e107d613 Add prelude module for import cleanup
|
||||
- 7200589a Add ErrorKind::NotFound
|
||||
- 8c880dc7 Add {Error,ErrorKind}::is_recoverable()
|
||||
- eb27773b Add pager.named_filters setting
|
||||
- 84d93d65 Add support for ID extension (opt-in)
|
||||
- af6838c2 Add metadata field to MailBackendCapabilities
|
||||
- d1499242 Add From<Infallible> impl
|
||||
- 814af0e9 Add --gzipped flag to man subcommand
|
||||
- 475860c9 Accept - for stdio in `{create,test}_config`
|
||||
- 86f9b213 Add timeout conf field in validate()
|
||||
- dd525bd9 Use Error::is_recoverable
|
||||
- 6e1fea80 Show suggestions on Unauthorized error
|
||||
- 38620866 Detect DNS lookup std::io::Error
|
||||
- a330ff96 Retry on DNS failure
|
||||
- 2429f17b On invalid conf value, print what value is expected
|
||||
- 6379fbe8 Add support for Undercurl attribute
|
||||
- a13bf13f Add stub Undercurl support
|
||||
- f5f1e068 Add UIDPLUS support
|
||||
- afccebf3 Add AUTH=PLAIN support
|
||||
- 9fb5bc41 Impl AUTH=ANONYMOUS (RFC4505)
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- ff3fe077 Fix new 1.79.0 clippy lints
|
||||
- 430cbdfd Fix python errors
|
||||
- e3c1656e Fix LOGINDISABLED support
|
||||
- a82d1e1e Fix RowsState::rename_env stale data
|
||||
- 8dc4465c Fix toml value ser after update of toml dependency
|
||||
- 39e903b1 Fix issues with ShellExpandTrait
|
||||
- 608301dc Expand save-to paths asap
|
||||
- 100fa8b3 Fix edge case in ShellExpandTrait
|
||||
- a85b3a08 Allow default_mailbox to be any mailbox
|
||||
- 0dc24623 Fix one by off error on menu unread count
|
||||
- 073aef86 Fix lints/errors when compiling specific feature combos
|
||||
- 12695a00 Fix MSRV breakage
|
||||
- 27ac3061 Fix tag support not being printed
|
||||
- 97af00cd Respect timeout value from user configuration
|
||||
- 824de287 Fix make_address! use
|
||||
- f2e9cac3 Use suggested minimum for maxObjectsInGet
|
||||
- 41d07fbc NewState in EmailImportResponse cannot be null
|
||||
- 197132cc Support fetching with BODY[] for buggy servers
|
||||
- 91fdef98 Return NotFound on cache miss
|
||||
- 96cc02a0 Do not use ErrorKind::Configuration
|
||||
- e96e9789 Don't discard pre-auth capabilities
|
||||
- 122a2a4d Drain event_queue when mailbox made available
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 27c4876f Prevent log flooding when drawing listing entries
|
||||
- 7bdc8f52 Highlight_self also when self is sender
|
||||
- c4f7b77a Rework attachment rendering logic with filters
|
||||
- 1cce8c11 Accept invalid "+" CRLF cont req
|
||||
- c04b593b Use BODY instead of RFC822
|
||||
- 084a222a Remove subscribed mailboxes list
|
||||
- 5b6c1aa8 Don't show all background jobs
|
||||
- f9a3b333 Return NotFound on empty FETCH
|
||||
- 15f3a3fb Retry fetch envelope only if err.is_recoverable()
|
||||
- 15eeac51 Enable dns_cache, tcp_keepalive & tcp_nodelay
|
||||
- 06437e60 Set not_yet_seen to 0 when inserting existing
|
||||
- 0b113cdb Use MELI_FEATURES in all cargo invocations
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 7856ea33 Transition more to imap-codec
|
||||
- 6f61176a Remove unecessary mut modifier
|
||||
- 3251e7bd Scrub skip_serializing_if from attributes
|
||||
- ebc1fa3b Move module to self dir
|
||||
- 5110813e Refactor MaildirOp and watch()
|
||||
- a9122c6e Draw with x range argument
|
||||
- 3ebf5510 Pass entire screen area when drawing overlay
|
||||
- 2dc1721a Move signal handling stuff to submodule
|
||||
- 738f7c46 Execute Opt subcommand in Opt::execute()
|
||||
- 46df4b57 Remove unused function stub
|
||||
- 52c75e92 Use HeaderName constants
|
||||
- 6da4e2ec Replace stringify! in Debug impls with type checked macro
|
||||
- 85a55ed6 Add some missing ErrorKinds to errors
|
||||
- 8b568f6e Add if_in_state argument in Set::new()
|
||||
- 1e2e3da0 Treat color input `; ;` as `; 0 ;`
|
||||
- 7c47f702 Extract test and parser modules to files
|
||||
- d40ee692 Extract tests mod from protocol_parser
|
||||
- 1e50911c Add utils module to protocol_parser
|
||||
- d3a45b34 Make default shared lib name a const
|
||||
- a9e9d952 Change termination_string arg to Option
|
||||
- fd76df78 Use MELI_CONFIG env var in mock tests
|
||||
- 8552e499 Replace std::mem::{replace,take}
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- dfc2bb43 Add link to MacPorts page for `meli`
|
||||
- 97aa6a8e Replace obsolete .Tn macro with .Em
|
||||
- a8e82a30 Add missing entries from JMAP
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- bbe2cffa Add rust-bindgen's friends.sh to scripts/
|
||||
- a8956baf Update to `imap-codec` v2.0.0-alpha.1
|
||||
- c99633e1 Update futures dependency 0.3.28 -> 0.3.30
|
||||
- fe604bf0 Update "openssl" dependency to 0.10.64
|
||||
- 9daf9437 Add test_cli_subcommands.rs
|
||||
- 9f783d9a Pin assert_cmd ver to 2.0.13
|
||||
- b7da1d0f Check all targets in cargo-msrv verify test
|
||||
- 8a74920d Pin rust version to 1.79.0
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Tue, 30 Jul 2024 14:21:31 +0300
|
||||
|
||||
meli (0.8.6-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
||||
- euxane
|
||||
- Manos Pitsidianakis
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 735b44f2 Add 'highlight_self' theme attribute
|
||||
- e187bb3f Add tools subcommand with smtp shell for debugging
|
||||
- 571bd984 Add proper imap-shell in tools subcommand for debugging
|
||||
- 0e1e5b9e Add support for Alternate Scroll Mode (xterm)
|
||||
- fe08d52a Add force_text_emoji_presentation option
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- 3de4908d man.7 Fix typo for toggle_expand_headers
|
||||
- a8c7582f Fix ENVELOPE parsing in untagged responses
|
||||
- c65635ef Fix compilation for macos
|
||||
- 06ec2790 Fix str slice index panic
|
||||
- f2b59a76 Add RequestUrlTemplate type
|
||||
- 7eed944a Fix screwed up rfc8620 module split
|
||||
- 74a3539f Fix degenerate OOB cell access
|
||||
- e8e76970 Fix edge case with strings/linebreaking
|
||||
- 81955187 Fix decryption error not shown
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- a9c3b151 Impl highlight_self in all index styles
|
||||
- 57e3e643 Remove excessive right padding in flags
|
||||
- a4ebe3b7 Add ErrorKind::Platform
|
||||
- 4bdfb3a3 Disable Nagle's algorithm by default
|
||||
- 4148aee5 Refactor smtp,draft errors and email tests
|
||||
- ed5a6b04 Add a symbols range to is_emoji check
|
||||
- fc1122a2 Rename to backend_mailbox.rs
|
||||
- 50ecade7 Merge rfc8620/tests.rs to tests.rs
|
||||
- a78f3f26 Move submodules to jmap/
|
||||
- f7838b1d Split to methods.rs and objects.rs
|
||||
- 74f0d12a Remove obsolete imapshell.rs and smtp_conn.rs
|
||||
- dce3852f Add capabilities module
|
||||
- 7ba7dc70 Imports cleanup in all modules
|
||||
- 45bfcf87 Minor refactors
|
||||
- 77867aee Unwrap object module
|
||||
- 33999fc6 Re-add Submission to USING
|
||||
- 6be25ac3 Don't use client field for get/posts
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- 4722d7cc Also mention server_password_command for jmap
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- 2bfe6086 Hide self from "add contacts" options
|
||||
- 9ca34a68 Update MSRV to 1.70.0
|
||||
- 50ff16c4 Add LIGHT, DARK constant theme keys
|
||||
- 1abce964 Add Envelope::recipient_any method
|
||||
- 671d35e2 Update mailin-embedded dependency to 0.8.2
|
||||
- 39fbb164 Change info_message_{next,prev} shortcuts to '<, >'
|
||||
- 58d73271 Change new mail text content
|
||||
- f0d1b9cf Add ayllu mirror link
|
||||
- 3bab5324 Improve Debug impl for ContentType etc
|
||||
- e9dd6bec Comment out content
|
||||
- 8dd87c1a Add ContentType::is_text_plain()
|
||||
- 01bc62e0 Add new_plaintext method
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sat, 08 Jun 2024 11:47:40 +0300
|
||||
|
||||
meli (0.8.5-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
||||
- Andrei Zisu
|
||||
- Ethra
|
||||
- Geert Stappers
|
||||
- Guillaume Ranquet
|
||||
- Manos Pitsidianakis
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 0e3a0c4b Add safe UI widget area drawing API
|
||||
- 0114e695 Add next_search_result and previous_search_result shortcuts
|
||||
- 0b468d88 Improve Error messages
|
||||
- 5af2e1ee Add subcommand to print config file location
|
||||
- 62aee464 Add subcommand to print log file location
|
||||
- e2cdebe8 Add option to highlight self in mailing list threads
|
||||
- cd448924 Add clear-selection command
|
||||
- 3a5306e9 View manpages in pager inside meli
|
||||
- a37d5fc1 Implement a key to command mapping
|
||||
- ce4ba06c Add a flag set/unset command
|
||||
- 148f0433 Implement flag set/unset action in UI
|
||||
- 417b24cd Print invalid command on error
|
||||
- 4e941a9e Add default_mailbox setting
|
||||
- 974502c6 Impl Hash for Card
|
||||
- ba7a97e9 Add x axis scroll support
|
||||
- ccf6f9a2 Remember previous set index_style preferences
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- bcec745c Fix command and status bar drawing
|
||||
- 62b8465f Fix ThreadView for new TUI API
|
||||
- 28fa66cc Fix ThreadedListing for new TUI API
|
||||
- 2c6f180d Fix macos compilation
|
||||
- 24971d19 Fix compilation with 1.70.0 cargo
|
||||
- 34a2d52e Fix rustdoc::redundant_explicit_links
|
||||
- f63774fa Fix new clippy lints (1.75)
|
||||
- 33408146 Fix feature permutation mis-compilations found with cargo-hack
|
||||
- e3351d27 Fix set unseen updating all mboxes
|
||||
- 8185f2cf Add deny clippy lints and fix them
|
||||
- 7861fb04 Fix typos found with typos tool
|
||||
- 64e60cb0 Fix select modifier regression
|
||||
- 60f26f9d Fix some old pre-intradoc rustdoc links
|
||||
- 1fe36192 Make conf validation recognize AccountSettings extra keys
|
||||
- c332c2f5 Fix new clippy lints (mostly clippy::blocks_in_conditions)
|
||||
- 070930e6 Fix auto index build when missing
|
||||
- 26928e3a Fix compilation for macos
|
||||
- 3884c0da Small typographic fixups
|
||||
- b820bd6d Remove unused imap_trace! and fix comp
|
||||
- a88b8c5e Debian/changelog warning fix
|
||||
- 4ce616ae Fix lints.yaml rustup install step
|
||||
- 264782d2 Various unimportant minor style/doc fixups
|
||||
- 475609fe Make {prev,next}_entry shortcut behavior consistent
|
||||
- a69c674c Fix new 1.77 clippy lints
|
||||
- 48cb9ee2 Fix compilation for macos
|
||||
- 8a16cf6d Fix wrong column index crash
|
||||
- bc1b6531 Fix constant redrawing
|
||||
- 29cc1bce Remove obsolete file melib/src/text/tables.rs.gz
|
||||
- ab041898 Fix new warnings for 1.78.0
|
||||
- 46e40856 Fix UIConfirmationDialog highlight printing
|
||||
- 3b93fa8e Don't draw messages above embedded terminal
|
||||
- 684fae3e Copy old content to new buf when resizing
|
||||
- 5d915baa Use Screen::resize instead of CellBuffer::resize
|
||||
- 6a66afe9 Make add contact dialog scrollable on overflow
|
||||
- aa5737a0 Prevent drawing pager on embedded mode
|
||||
- 07072e2e Prevent panic if envelope is deleted
|
||||
- 8ddd673d Update all mailboxes
|
||||
- 3691cd29 Send EnvelopeUpdate event after self.collection.update_flags()
|
||||
- 1fcb1d59 Remove rerun when build.rs changes
|
||||
- 933bf157 Ack \ as an atom
|
||||
- a1cbb198 Return Results instead of panicking
|
||||
- b5ddc397 Remove unwrap() from get_events() loop
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 61a0c3c2 Do not clear selection after action
|
||||
- 9af284b8 Don't hide unread count for mailboxes that are partly truncated
|
||||
- 35408b16 Run pager filter asynchronously
|
||||
- e80ea9c9 Changed default manpage install path
|
||||
- 742f038f Move sent_mailbox to settings
|
||||
- 86bbf1ea Refresh NotmuchMailbox counts when setting flags
|
||||
- f0866a39 Make config error more user-friendly
|
||||
- 11f3077b Add more possible values for manpage names
|
||||
- 1eca34b3 Set lowest priority to shortcut command UIEvents
|
||||
- 484712b0 Check for unrecoverable errors in is_online
|
||||
- 8ec6f220 Use ShellExpandTrait::expand in more user-provided paths
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 0500e451 Add missing EnvelopeRemove event handler
|
||||
- ab14f819 Make write_string_to_grid a CellBuffer method
|
||||
- e0adcdfe Move rest of methods under CellBuffer
|
||||
- 0a74c7d0 Overhaul refactor
|
||||
- 3b4acc15 Add tests
|
||||
- 7eedd860 Remove address_list! macro
|
||||
- f3e85738 Move build.rs scripts to build directory
|
||||
- 77325486 Remove on-push hooks for actions w/ run on-pr
|
||||
- 08518e1c Remove obsolete position.rs module
|
||||
- ddab3179 Move tests to tests module
|
||||
- 79520068 Remove doctests, add tests module
|
||||
- 4e7b6656 Sqlite caching refactor
|
||||
- b5fd3f57 Make self.view an Option
|
||||
- a3aaec38 Remove unused imports
|
||||
- 11a0586d Remove num_cpus dependency
|
||||
- 8f3dee9b Extract mod manpages to standalone file
|
||||
- 89c7972e Add suggestions to BadValue variant
|
||||
- 35a9f33a Extract common FlagString logic
|
||||
- 1b0bdd0a Split queries and mailbox into submodules
|
||||
- 506ae9f5 Add ErrorKind::LinkedLibrary variant
|
||||
- ebe1b3da Wrap *mut struct fields in NonNull<_>
|
||||
- ca7d7bb9 Use message freeze/thaw for flag changes
|
||||
- 4026e254 Add some doc comments
|
||||
- 808aa494 Rename text_processing to text for the whole brevity thing
|
||||
- bebb473d Derive extra traits for enums
|
||||
- ab1b946f Don't print details if it's an empty string.
|
||||
- f685726e Add backtrace field to ParsingError
|
||||
- 73d5b24e Merge integration tests in one crate
|
||||
- 31401fa3 Add LazyCountSet::contains method
|
||||
- 0270db01 From<&[u8]> -> From<B: AsRef<[u9]>>
|
||||
- 873a67d0 Replace erroneous use of set_err_kind with set_kind
|
||||
- 51e3f163 Use Url instead of String in deserializing
|
||||
- 8014af25 Reduce debug prints
|
||||
- f31b5c40 Don't print raw bytes as escaped unicode
|
||||
- 41e965b8 Split mbox/job stuff in submodules
|
||||
- ec01a441 Turn some sync connections to unsync
|
||||
- 3e914465 Store children process metadata
|
||||
- c53a32de Re-enables horizontal thread view
|
||||
- 36b7c00b Put doc text type names and co. in backtics
|
||||
- 634bd191 Convert log prints to traces
|
||||
- 1048ce68 Add hostname() utility function
|
||||
- 7645ff1b Rename write_string{to_grid,}
|
||||
- c2ae19d1 Return Option from current_pos
|
||||
- b61fc3ab Add HelpView struct for shortcuts widget
|
||||
- 3495ffd6 Change UIEvent::Notification structure
|
||||
- 23c15261 Abstract envelope view filters away
|
||||
- 031d0f7d Add area.is_empty() checks in cell iterators
|
||||
- e37997d6 Store Link URL value in Link type
|
||||
- b6f769b2 Add field names to row_attr! bool values
|
||||
- 0da97dd8 Check row_updates in is_dirty()
|
||||
- 6506fffb Rewrite email flag modifications
|
||||
- 23507932 Update cache on set_flags
|
||||
- 470cae6b Update thread cache on email flag modifications
|
||||
- 84f3641e Re-add on-screen message display
|
||||
- 54d21f25 Re-add contact list and editor support
|
||||
- 458258e1 Re-enable compact listing style
|
||||
- 1c1be7d6 Add display_name(), display_slice(), display_name_slice() methods
|
||||
- 5dd71ef1 Upgrade JobsView component to new TUI API
|
||||
- b5cc2a09 Upgrade MailboxManager component to new TUI API
|
||||
- ed8a5de2 Re-enable EditAttachments component
|
||||
- 77a8d9e2 Make ModSequence publicly accessible
|
||||
- 64898a05 Make UIDStore constructor pub
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- e4818803 Various manpage touchups and URL updates
|
||||
- 38bca8f8 Mention use_oauth2=true for gmail oauth2
|
||||
- 660022ce Add mailaddr.7 manpage
|
||||
- c5e9e676 Add historical-manpages dir
|
||||
- 5afc0785 Update README.md, DEVELOPMENT.md and create BUILD.md
|
||||
- d018f07a Retouch manual pages
|
||||
- 3adba40e Add macos manpage mirror url
|
||||
|
||||
Packaging
|
||||
=========
|
||||
|
||||
- cd2ba80f Update metadata
|
||||
- 5f8d7c80 Update deb-dist target command with author metadata
|
||||
- 59c99fdc Update debian package metadata
|
||||
- 97eb6363 Add dpkg --print-architecture to deb filename
|
||||
- 7412c238 Bump meli version to 0.8.5-rc.3
|
||||
- 500fe7f7 Update CHANGELOG.md
|
||||
- 5ff4e8ae Run builds.yaml when any manifest file changes
|
||||
- 0a617410 Split test.yaml to test.yaml and lints.yaml
|
||||
- 3ba1603a Add manifest file only lints workflow
|
||||
- 1617212c Add scripts/check_debian_changelog.sh lint
|
||||
- c41f35fd Use actions/checkout@v3
|
||||
- 876616d4 Use actions/upload-artifact@v3
|
||||
- 2419f4bd Add debian package build workflow
|
||||
- 10c3b0ea Bump version to 0.8.5-rc.1
|
||||
- d16afc7d Bump version to 0.8.5-rc.2
|
||||
- da251455 Bump meli version to 0.8.5-rc.2
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- c4344529 Add .git-blame-ignore-revs file
|
||||
- f70496f1 Add codemeta.json
|
||||
- b3079715 Disable flakey test_smtp()
|
||||
- 8a95febb Set debuginfo=0 in test/lint builds
|
||||
- 81d1c053 Add mandoc_lint.sh
|
||||
- 8de8addd Add cfg for musl builds
|
||||
- 70fc2b45 Update nix dependency to 0.27
|
||||
- fd64fe0b Update codeberg.org URL
|
||||
- 30a3205e Add clippy::doc_markdown
|
||||
- c7aee725 Add clippy::doc_markdown
|
||||
- b8b24282 Update all instances of old domains with meli-email.org
|
||||
- ae96038f Make unicode-segmentation a hard dependency
|
||||
- 255e9376 Update linkify dep from 0.8.1 to 0.10.0
|
||||
- dedee908 Update notify dep from 4.0.17 to 6.1.1
|
||||
- c1c41c91 Update README.md and add Codeberg mirror
|
||||
- 71f3ffe7 Update Makefile
|
||||
- 63a63253 Use type alias for c_char
|
||||
- c751b2e8 Re-enable conversations listing style
|
||||
- 3a709794 Update minimum rust version from 1.65.0 to 1.68.2
|
||||
- f900dbea Use cargo-derivefmt to sort derives alphabetically
|
||||
- e19f3e57 Cargo-sort all Cargo.toml files
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 05 May 2024 18:46:42 +0300
|
||||
|
||||
meli (0.8.5-rc.3-1) bookworm; urgency=low
|
||||
|
||||
* Update to 0.8.5-rc.3
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 10 Dec 2023 15:22:18 +0000
|
||||
|
||||
meli (0.8.5-rc.2-1) bookworm; urgency=low
|
||||
|
||||
* Update to 0.8.5-rc.2
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 4 Dec 2023 19:34:00 +0200
|
||||
|
||||
meli (0.8.4-1) bookworm; urgency=low
|
||||
|
||||
* Update to 0.8.4
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 27 Nov 2023 19:34:00 +0200
|
||||
|
||||
meli (0.7.2-1) bullseye; urgency=low
|
||||
Added
|
||||
|
||||
|
@ -25,7 +898,7 @@ meli (0.7.1-1) bullseye; urgency=low
|
|||
- melib/nntp: implement refresh
|
||||
- melib/nntp: update total/new counters on new articles
|
||||
- melib/nntp: implement NNTP posting
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on extra unused conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
Fixed
|
||||
|
|
1
debian/compat
vendored
1
debian/compat
vendored
|
@ -1 +0,0 @@
|
|||
11
|
18
debian/control
vendored
18
debian/control
vendored
|
@ -1,14 +1,20 @@
|
|||
Source: meli
|
||||
Section: mail
|
||||
Priority: optional
|
||||
Maintainer: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
|
||||
Maintainer: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Build-Depends: debhelper-compat (=13), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
|
||||
Standards-Version: 4.1.4
|
||||
Homepage: https://meli.delivery
|
||||
Rules-Requires-Root: no
|
||||
Vcs-Git: https://git.meli-email.org/meli/meli.git
|
||||
Vcs-Browser: https://git.meli-email.org/meli/meli
|
||||
Homepage: https://meli-email.org
|
||||
|
||||
Package: meli
|
||||
Architecture: any
|
||||
Multi-Arch: foreign
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}
|
||||
Recommends: libnotmuch, xdg-utils (>=1.1.3-1)
|
||||
Description: terminal mail client
|
||||
Recommends: xdg-utils (>=1.1.3-1), w3m, mailcap
|
||||
Suggests: libnotmuch5, notmuch, rss2email, xterm, neovim, msmtp
|
||||
Provides: mail-reader, imap-client
|
||||
Description: terminal mail client.
|
||||
meli supports mbox, maildir, IMAP, JMAP, notmuch and NNTP (Usernet) with
|
||||
TLS/SSL, SASL, GPG features.
|
||||
|
|
4
debian/copyright
vendored
4
debian/copyright
vendored
|
@ -1,11 +1,11 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: meli
|
||||
Source: <https://git.meli.delivery/meli/meli>
|
||||
Source: https://git.meli-email.org/meli/meli
|
||||
#
|
||||
# Please double check copyright with the licensecheck(1) command.
|
||||
|
||||
Files: *
|
||||
Copyright: 2017-2020 Manos Pitsidianakis
|
||||
Copyright: 2017-2023 Manos Pitsidianakis
|
||||
License: GPL-3.0+
|
||||
#----------------------------------------------------------------------------
|
||||
# License file: COPYING
|
||||
|
|
8
debian/extra/meli.desktop
vendored
Normal file
8
debian/extra/meli.desktop
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=meli
|
||||
Exec=meli
|
||||
Categories=Office;Network;Email;
|
||||
Comment=Terminal mail client
|
||||
NoDisplay=false
|
||||
Terminal=true
|
||||
Type=Application
|
7
debian/meli.bug-presubj
vendored
Normal file
7
debian/meli.bug-presubj
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
WARNING: This package is not distributed by debian, it was generated from the source repository of meli.
|
||||
|
||||
Please do not report bugs to debian, but to the appropriate issue tracker for meli:
|
||||
|
||||
- https://git.meli-email.org/meli/meli/issues
|
||||
- Send e-mail to the mailing list, "meli general" <meli-general@meli-email.org>
|
||||
https://lists.meli-email.org/list/meli-general/
|
9
debian/meli.bug-script
vendored
Executable file
9
debian/meli.bug-script
vendored
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Including output of \`meli -v\` and \`meli compiled-with\`..."
|
||||
|
||||
LC_ALL=C meli -v >&3
|
||||
|
||||
echo "\nEnabled compile-time features"
|
||||
echo "-----------------------------"
|
||||
LC_ALL=C meli compiled-with >&3 || true
|
5
debian/meli.doc-base
vendored
Normal file
5
debian/meli.doc-base
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
Document: meli
|
||||
Title: meli E-mail Client Manual
|
||||
Author: Various
|
||||
Abstract: Manual for meli the terminal e-mail client.
|
||||
Section: Network/Communication
|
7
debian/meli.docs
vendored
7
debian/meli.docs
vendored
|
@ -1,3 +1,4 @@
|
|||
docs/meli.1
|
||||
docs/meli.conf.5
|
||||
docs/meli-themes.5
|
||||
meli/docs/*.1
|
||||
meli/docs/*.5
|
||||
meli/docs/*.7
|
||||
meli/docs/external-tools.md
|
||||
|
|
3
debian/meli.examples
vendored
Normal file
3
debian/meli.examples
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
contrib/*
|
||||
meli/docs/mail.vim
|
||||
meli/docs/samples/*
|
3
debian/meli.manpages
vendored
Normal file
3
debian/meli.manpages
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
meli/docs/*.1
|
||||
meli/docs/*.5
|
||||
meli/docs/*.7
|
4
debian/patches/fix-prefix-for-debian.patch
vendored
4
debian/patches/fix-prefix-for-debian.patch
vendored
|
@ -1,6 +1,6 @@
|
|||
Description: Fix PREFIX env var in Makefile for use in Debian
|
||||
Author: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Last-Update: 2023-03-06
|
||||
Author: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Last-Update: 2024-11-19
|
||||
Index: meli/Makefile
|
||||
===================================================================
|
||||
--- meli.orig/Makefile
|
||||
|
|
1
debian/patches/series
vendored
1
debian/patches/series
vendored
|
@ -1 +1,2 @@
|
|||
fix-prefix-for-debian.patch
|
||||
usr_bin_editor.patch
|
||||
|
|
18
debian/patches/usr_bin_editor.patch
vendored
Normal file
18
debian/patches/usr_bin_editor.patch
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
Description: If EDITOR or VISUAL is not set, fall back to /usr/bin/editor, which is set by update-alternatives.
|
||||
Author: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Last-Update: 2024-11-19
|
||||
Index: meli/meli/src/subcommands.rs
|
||||
===================================================================
|
||||
--- meli.orig/meli/src/subcommands.rs
|
||||
+++ meli/meli/src/subcommands.rs
|
||||
@@ -56,9 +56,7 @@
|
||||
pub fn edit_config() -> Result<()> {
|
||||
let editor = std::env::var("EDITOR")
|
||||
.or_else(|_| std::env::var("VISUAL"))
|
||||
- .map_err(|err| {
|
||||
- format!("Could not find any value in environment variables EDITOR and VISUAL. {err}")
|
||||
- })?;
|
||||
+ .unwrap_or_else(|_| "/usr/bin/editor".into());
|
||||
let config_path = conf::get_config_file()?;
|
||||
|
||||
let mut cmd = Command::new(editor);
|
12
debian/rules
vendored
12
debian/rules
vendored
|
@ -1,14 +1,22 @@
|
|||
#!/usr/bin/make -f
|
||||
# You must remove unused comment lines for the released package.
|
||||
#export DH_VERBOSE = 1
|
||||
export RUSTUP_HOME=${HOME}/.rustup
|
||||
export DH_VERBOSE = 1
|
||||
export NO_MAN
|
||||
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all
|
||||
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
|
||||
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
|
||||
export MELI_FEATURES = cli-docs sqlite3
|
||||
#export MELI_FEATURES = cli-docs sqlite3
|
||||
|
||||
%:
|
||||
dh $@ --with quilt
|
||||
|
||||
override_dh_auto_configure:
|
||||
true
|
||||
|
||||
override_dh_auto_test:
|
||||
true
|
||||
|
||||
#override_dh_auto_install:
|
||||
# dh_auto_install -- prefix=/usr
|
||||
|
||||
|
|
191
deny.toml
Normal file
191
deny.toml
Normal file
|
@ -0,0 +1,191 @@
|
|||
# cargo-deny configuration
|
||||
|
||||
[graph]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
[output]
|
||||
feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2021-0145", reason = "Affects Windows, which we do not support officially." },
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
#"MIT",
|
||||
#"Apache-2.0",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = [""]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
bitbucket = [""]
|
|
@ -8,17 +8,14 @@ edition = "2018"
|
|||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[[bin]]
|
||||
name = "envelope_parse"
|
||||
path = "fuzz_targets/envelope_parse.rs"
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.3"
|
||||
|
||||
[dependencies.melib]
|
||||
path = "../melib"
|
||||
features = ["unicode-algorithms"]
|
||||
melib = { path = "../melib" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "envelope_parse"
|
||||
path = "fuzz_targets/envelope_parse.rs"
|
||||
|
|
102
meli/Cargo.toml
102
meli/Cargo.toml
|
@ -1,18 +1,18 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.8.1"
|
||||
version = "0.8.10"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
license = "GPL-3.0-or-later"
|
||||
rust-version = "1.70.0"
|
||||
license = "EUPL-1.2 OR GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
description = "terminal mail client"
|
||||
homepage = "https://meli.delivery"
|
||||
repository = "https://git.meli.delivery/meli/meli.git"
|
||||
description = "terminal e-mail client"
|
||||
homepage = "https://meli-email.org"
|
||||
repository = "https://git.meli-email.org/meli/meli.git"
|
||||
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
|
||||
categories = ["command-line-utilities", "email"]
|
||||
default-run = "meli"
|
||||
exclude = ["/docs/historical-manpages"]
|
||||
|
||||
[[bin]]
|
||||
name = "meli"
|
||||
|
@ -22,72 +22,70 @@ path = "src/main.rs"
|
|||
name = "meli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "managesieve-client"
|
||||
path = "src/managesieve.rs"
|
||||
required-features = ["melib/imap"]
|
||||
|
||||
[dependencies]
|
||||
async-task = "^4.2.0"
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
async-task = { version = "^4.2.0" }
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1", optional = true }
|
||||
futures = "0.3.5"
|
||||
indexmap = { version = "^1.6", features = ["serde-1"] }
|
||||
futures = { version = "0.3.30", default-features = false, features = ["async-await", "executor", "std"] }
|
||||
indexmap = { version = "^2.3", default-features = false, features = ["serde", "std"] }
|
||||
itoa = { version = "1.0.11", default-features = false }
|
||||
libc = { version = "0.2.125", default-features = false, features = ["extra_traits"] }
|
||||
linkify = { version = "^0.8", default-features = false }
|
||||
melib = { path = "../melib", version = "0.8.1" }
|
||||
nix = { version = "^0.24", default-features = false }
|
||||
notify = { version = "4.0.1", default-features = false } # >:c
|
||||
num_cpus = "1.12.0"
|
||||
pcre2 = { version = "0.2.3", optional = true }
|
||||
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = "1.0"
|
||||
signal-hook = { version = "^0.3", default-features = false }
|
||||
libz-sys = { version = "1.1", features = ["static"], optional = true }
|
||||
linkify = { version = "^0.10", default-features = false }
|
||||
melib = { path = "../melib", version = "0.8.10", features = [] }
|
||||
nix = { version = "0.29", default-features = false, features = ["signal", "poll", "term", "ioctl", "process"] }
|
||||
regex = { version = "1" }
|
||||
serde = { version = "1.0.71" }
|
||||
serde_derive = { version = "1.0.71" }
|
||||
serde_json = { version = "1.0" }
|
||||
signal-hook = { version = "^0.3", default-features = false, features = ["iterator"] }
|
||||
signal-hook-registry = { version = "1.2.0", default-features = false }
|
||||
smallvec = { version = "^1.5.0", features = ["serde"] }
|
||||
structopt = { version = "0.3.14", default-features = false }
|
||||
svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
structopt = { version = "0.3.26", default-features = false }
|
||||
# svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
toml = { version = "0.5.6", default-features = false, features = ["preserve_order"] }
|
||||
xdg = "2.1.0"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
notify-rust = { version = "^4", default-features = false, features = ["dbus"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1", optional = true }
|
||||
proc-macro2 = "1.0.37"
|
||||
quote = "^1.0"
|
||||
regex = "1"
|
||||
syn = { version = "1", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
flate2 = { version = "1" }
|
||||
regex = "1"
|
||||
tempfile = "3.3"
|
||||
toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] }
|
||||
xdg = { version = "2.1.0" }
|
||||
|
||||
[features]
|
||||
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "text-processing"]
|
||||
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
|
||||
notmuch = ["melib/notmuch"]
|
||||
jmap = ["melib/jmap"]
|
||||
sqlite3 = ["melib/sqlite3"]
|
||||
smtp = ["melib/smtp"]
|
||||
smtp-trace = ["smtp", "melib/smtp-trace"]
|
||||
regexp = ["pcre2"]
|
||||
dbus-notifications = ["notify-rust"]
|
||||
cli-docs = ["flate2"]
|
||||
svgscreenshot = ["svg_crate"]
|
||||
text-processing = ["melib/unicode-algorithms"]
|
||||
dbus-notifications = ["dep:notify-rust"]
|
||||
cli-docs = ["dep:flate2"]
|
||||
# svgscreenshot = ["dep:svg_crate"]
|
||||
gpgme = ["melib/gpgme"]
|
||||
# Static / vendoring features.
|
||||
tls-static = ["melib/tls-static"]
|
||||
http-static = ["melib/http-static"]
|
||||
sqlite3-static = ["melib/sqlite3-static"]
|
||||
static = ["tls-static", "http-static", "sqlite3-static"]
|
||||
dbus-static = ["dep:notify-rust", "notify-rust?/d_vendored"]
|
||||
libz-static = ["dep:libz-sys", "libz-sys?/static"]
|
||||
static = ["tls-static", "http-static", "sqlite3-static", "dbus-static", "libz-static"]
|
||||
|
||||
# Print tracing logs as meli runs in stderr
|
||||
# enable for debug tracing logs: build with --features=debug-tracing and export MELI_DEBUG_STDERR
|
||||
debug-tracing = ["melib/debug-tracing"]
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1", optional = true }
|
||||
proc-macro2 = { version = "1.0.37" }
|
||||
quote = { version = "^1.0" }
|
||||
regex = { version = "1" }
|
||||
syn = { version = "1", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = { version = "=2.0.13" }
|
||||
flate2 = { version = "1" }
|
||||
predicates = { version = "3" }
|
||||
regex = { version = "1" }
|
||||
rusty-fork = { version = "0.3.0" }
|
||||
tempfile = { version = "3.3" }
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
notify-rust = { version = "^4", default-features = false, features = ["dbus"], optional = true }
|
||||
|
|
|
@ -22,12 +22,11 @@
|
|||
extern crate proc_macro;
|
||||
extern crate quote;
|
||||
extern crate syn;
|
||||
mod config_macros;
|
||||
include!("config_macros.rs");
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
|
||||
config_macros::override_derive(&[
|
||||
override_derive(&[
|
||||
("src/conf/pager.rs", "PagerSettings"),
|
||||
("src/conf/listing.rs", "ListingSettings"),
|
||||
("src/conf/notifications.rs", "NotificationsSettings"),
|
||||
|
@ -40,7 +39,7 @@ fn main() {
|
|||
{
|
||||
use flate2::{Compression, GzBuilder};
|
||||
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
|
||||
use std::{env, fs::File, io::prelude::*, path::Path, process::Command};
|
||||
use std::{env, io::prelude::*, path::Path};
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
|
||||
|
@ -76,10 +75,20 @@ fn main() {
|
|||
|
||||
cl("docs/meli.1", "meli.txt.gz", false);
|
||||
cl("docs/meli.conf.5", "meli.conf.txt.gz", false);
|
||||
cl(
|
||||
"docs/meli.conf.examples.5",
|
||||
"meli.conf.examples.txt.gz",
|
||||
false,
|
||||
);
|
||||
cl("docs/meli-themes.5", "meli-themes.txt.gz", false);
|
||||
cl("docs/meli.7", "meli.7.txt.gz", false);
|
||||
cl("docs/meli.1", "meli.mdoc.gz", true);
|
||||
cl("docs/meli.conf.5", "meli.conf.mdoc.gz", true);
|
||||
cl(
|
||||
"docs/meli.conf.examples.5",
|
||||
"meli.conf.examples.mdoc.gz",
|
||||
true,
|
||||
);
|
||||
cl("docs/meli-themes.5", "meli-themes.mdoc.gz", true);
|
||||
cl("docs/meli.7", "meli.7.mdoc.gz", true);
|
||||
}
|
||||
|
|
|
@ -56,16 +56,21 @@ pub(crate) fn override_derive(filenames: &[(&str, &str)]) {
|
|||
|
||||
#![allow(clippy::derivable_impls)]
|
||||
|
||||
//! This module is automatically generated by config_macros.rs.
|
||||
//! This module is automatically generated by `config_macros.rs`.
|
||||
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
|
||||
use crate::conf::{*, data_types::*};
|
||||
|
||||
"##
|
||||
.to_string();
|
||||
|
||||
let cfg_attr_default_attr_regex = Regex::new(r"\s*default\s*[,]").unwrap();
|
||||
let cfg_attr_default_val_attr_regex = Regex::new(r#"\s*default\s*=\s*"[^"]*"\s*,\s*"#).unwrap();
|
||||
let cfg_attr_skip_ser_attr_regex =
|
||||
Regex::new(r#"\s*,?\s*skip_serializing_if\s*=\s*"[^"]*"\s*,?\s*"#).unwrap();
|
||||
let cfg_attr_feature_regex = Regex::new(r"[(](?:not[(]\s*)?feature").unwrap();
|
||||
|
||||
'file_loop: for (filename, ident) in filenames {
|
||||
|
@ -120,6 +125,14 @@ use melib::HeaderName;
|
|||
f.tokens.clone().into_iter().next().unwrap()
|
||||
{
|
||||
let mut attr_inner_value = f.tokens.to_string();
|
||||
if attr_inner_value.contains("skip_serializing_if") {
|
||||
attr_inner_value = cfg_attr_skip_ser_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
.to_string();
|
||||
let new_toks: proc_macro2::TokenStream =
|
||||
attr_inner_value.parse().unwrap();
|
||||
new_attr.tokens = quote! { #new_toks };
|
||||
}
|
||||
if cfg_attr_feature_regex.is_match(&attr_inner_value) {
|
||||
attr_inner_value = cfg_attr_default_val_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
|
@ -154,6 +167,11 @@ use melib::HeaderName;
|
|||
} else if attr_inner_value.starts_with("( default")
|
||||
|| attr_inner_value.starts_with("(default")
|
||||
{
|
||||
if attr_inner_value.ends_with("default)")
|
||||
|| attr_inner_value.ends_with("default )")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let rest = g.stream().into_iter().skip(2);
|
||||
new_attr.tokens = quote! { ( #(#rest)*) };
|
||||
match new_attr.tokens.to_string().as_str() {
|
||||
|
@ -192,7 +210,7 @@ use melib::HeaderName;
|
|||
#(#attrs_tokens)*
|
||||
impl Default for #override_ident {
|
||||
fn default() -> Self {
|
||||
#override_ident {
|
||||
Self {
|
||||
#(#field_idents: None),*
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Sending mail with a command line tool
|
||||
|
||||
`composing.send_mail` can use either settings for an SMTP server or a shell
|
||||
`send_mail` can use either settings for an SMTP server or a shell
|
||||
command to which it pipes new mail to.
|
||||
|
||||
### `msmtp` and `send_mail`
|
||||
|
@ -12,7 +12,6 @@ with many SMTP servers. It supports queuing and other small useful features.
|
|||
See [the documentation](https://marlam.de/msmtp/msmtp.html).
|
||||
|
||||
```toml
|
||||
[composing]
|
||||
send_mail = 'msmtp --logfile=/home/user/.mail/msmtp.log --read-recipients
|
||||
--read-envelope-from'
|
||||
```
|
||||
|
@ -120,3 +119,32 @@ The HTML of the e-mail is piped into `html_filter`'s standard input.
|
|||
If your account's syncing is handled by an external tool, you can use the
|
||||
refresh shortcuts within `meli` to call this tool with
|
||||
`accounts.refresh_command`.
|
||||
|
||||
## Viewing binary attachments such as images inside your terminal
|
||||
|
||||
If you have a specific terminal tool that lets you pipe binary data to it and
|
||||
it outputs command suitable for the terminal, you can use the `pipe-attachment`
|
||||
command to view/preview attachments without leaving `meli` or opening a GUI app.
|
||||
|
||||
This requires the output to be interactive otherwise `meli` will run the tool
|
||||
and immediately return, probably too quickly for you to notice the output in
|
||||
your terminal. A general solution is to pipe the output to an interactive pager
|
||||
like `less` which requires the user to exit it interactively.
|
||||
|
||||
The [`chafa`] tool can be used for images in this example:
|
||||
|
||||
Write a wrapper script that outputs the tool's output into a pager, for example
|
||||
`less`. If the output contains ANSI escape codes (i.e. colors, or bold/italic
|
||||
text) make sure to use `less -r` to preserve those codes.
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
/bin/chafa "$@" | less -r
|
||||
```
|
||||
|
||||
Save it somewhere as a file with executable permissions and you can use
|
||||
`pipe-attachment 1 /path/to/your/chafa/wrapper` to view the first attachment as
|
||||
an image with [`chafa`].
|
||||
|
||||
[`chafa`]: https://hpjansson.org/chafa/
|
||||
|
|
42
meli/docs/historical-manpages/copyright
Normal file
42
meli/docs/historical-manpages/copyright
Normal file
|
@ -0,0 +1,42 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Comment: This directory contains various manual pages that may be of use to meli users.
|
||||
|
||||
Files: mailaddr.7.gz
|
||||
Comment: Sourced from debian manpages package.
|
||||
Copyright: Copyright (c) 1983, 1987 The Regents of the University of California.
|
||||
License: 6.5 (Berkeley) 2/14/89
|
||||
Redistribution and use in source and binary forms are permitted
|
||||
provided that the above copyright notice and this paragraph are
|
||||
duplicated in all such forms and that any documentation,
|
||||
advertising materials, and other materials related to such
|
||||
distribution and use acknowledge that the software was developed
|
||||
by the University of California, Berkeley. The name of the
|
||||
University may not be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
Files: maildir.5.en.gz
|
||||
Comment: Sourced from debian maildrop package.
|
||||
Copyright: Copyright 1998 - 2007 Double Precision, Inc.
|
||||
License: GPLv3 with OpenSSL linking extension
|
||||
This software is released under the GPL, version 3 (see COPYING.GPL).
|
||||
Additionally, compiling, linking, and/or using the OpenSSL toolkit in
|
||||
conjunction with this software is allowed.
|
||||
|
||||
Files: mbox.5.en.gz
|
||||
Comment: Sourced from debian mutt package.
|
||||
Copyright: Copyright (C) 2000 Thomas Roessler <roessler@does-not-exist.org>
|
||||
License: public-domain
|
||||
This document is in the public domain and may be distributed and
|
||||
changed arbitrarily.
|
||||
|
||||
Files: mbox.5qmail.en.gz
|
||||
Comment: Sourced from (now obsolete) debian qmail package.
|
||||
Copyright: D. J. Bernstein
|
||||
License: From http://cr.yp.to/qmail/dist.html
|
||||
I hereby place the qmail package (in particular, qmail-1.03.tar.gz,
|
||||
with MD5 checksum 622f65f982e380dbe86e6574f3abcb7c) into the public
|
||||
domain. You are free to modify the package, distribute modified
|
||||
versions, etc.
|
BIN
meli/docs/historical-manpages/mailaddr.7.gz
Normal file
BIN
meli/docs/historical-manpages/mailaddr.7.gz
Normal file
Binary file not shown.
BIN
meli/docs/historical-manpages/maildir.5.en.gz
Normal file
BIN
meli/docs/historical-manpages/maildir.5.en.gz
Normal file
Binary file not shown.
BIN
meli/docs/historical-manpages/mbox.5.en.gz
Normal file
BIN
meli/docs/historical-manpages/mbox.5.en.gz
Normal file
Binary file not shown.
BIN
meli/docs/historical-manpages/mbox.5qmail.en.gz
Normal file
BIN
meli/docs/historical-manpages/mbox.5qmail.en.gz
Normal file
Binary file not shown.
239
meli/docs/historical-manpages/qmail-maildir.5.gz
Normal file
239
meli/docs/historical-manpages/qmail-maildir.5.gz
Normal file
|
@ -0,0 +1,239 @@
|
|||
.TH maildir 5
|
||||
.SH "NAME"
|
||||
maildir \- directory for incoming mail messages
|
||||
.SH "INTRODUCTION"
|
||||
.I maildir
|
||||
is a structure for
|
||||
directories of incoming mail messages.
|
||||
It solves the reliability problems that plague
|
||||
.I mbox
|
||||
files and
|
||||
.I mh
|
||||
folders.
|
||||
.SH "RELIABILITY ISSUES"
|
||||
A machine may crash while it is delivering a message.
|
||||
For both
|
||||
.I mbox
|
||||
files and
|
||||
.I mh
|
||||
folders this means that the message will be silently truncated.
|
||||
Even worse: for
|
||||
.I mbox
|
||||
format, if the message is truncated in the middle of a line,
|
||||
it will be silently joined to the next message.
|
||||
The mail transport agent will try again later to deliver the message,
|
||||
but it is unacceptable that a corrupted message should show up at all.
|
||||
In
|
||||
.IR maildir ,
|
||||
every message is guaranteed complete upon delivery.
|
||||
|
||||
A machine may have two programs simultaneously delivering mail
|
||||
to the same user.
|
||||
The
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
formats require the programs to update a single central file.
|
||||
If the programs do not use some locking mechanism,
|
||||
the central file will be corrupted.
|
||||
There are several
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
locking mechanisms,
|
||||
none of which work portably and reliably.
|
||||
In contrast, in
|
||||
.IR maildir ,
|
||||
no locks are ever necessary.
|
||||
Different delivery processes never touch the same file.
|
||||
|
||||
A user may try to delete messages from his mailbox at the same
|
||||
moment that the machine delivers a new message.
|
||||
For
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
formats, the user's mail-reading program must know
|
||||
what locking mechanism the mail-delivery programs use.
|
||||
In contrast, in
|
||||
.IR maildir ,
|
||||
any delivered message
|
||||
can be safely updated or deleted by a mail-reading program.
|
||||
|
||||
Many sites use Sun's
|
||||
.B Network F\fPa\fBil\fPur\fBe System
|
||||
(NFS),
|
||||
presumably because the operating system vendor does not offer
|
||||
anything else.
|
||||
NFS exacerbates all of the above problems.
|
||||
Some NFS implementations don't provide
|
||||
.B any
|
||||
reliable locking mechanism.
|
||||
With
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
formats,
|
||||
if two machines deliver mail to the same user,
|
||||
or if a user reads mail anywhere except the delivery machine,
|
||||
the user's mail is at risk.
|
||||
.I maildir
|
||||
works without trouble over NFS.
|
||||
.SH "THE MAILDIR STRUCTURE"
|
||||
A directory in
|
||||
.I maildir
|
||||
format has three subdirectories,
|
||||
all on the same filesystem:
|
||||
.BR tmp ,
|
||||
.BR new ,
|
||||
and
|
||||
.BR cur .
|
||||
|
||||
Each file in
|
||||
.B new
|
||||
is a newly delivered mail message.
|
||||
The modification time of the file is the delivery date of the message.
|
||||
The message is delivered
|
||||
.I without
|
||||
an extra UUCP-style
|
||||
.B From_
|
||||
line,
|
||||
.I without
|
||||
any
|
||||
.B >From
|
||||
quoting,
|
||||
and
|
||||
.I without
|
||||
an extra blank line at the end.
|
||||
The message is normally in RFC 822 format,
|
||||
starting with a
|
||||
.B Return-Path
|
||||
line and a
|
||||
.B Delivered-To
|
||||
line,
|
||||
but it could contain arbitrary binary data.
|
||||
It might not even end with a newline.
|
||||
|
||||
Files in
|
||||
.B cur
|
||||
are just like files in
|
||||
.BR new .
|
||||
The big difference is that files in
|
||||
.B cur
|
||||
are no longer new mail:
|
||||
they have been seen by the user's mail-reading program.
|
||||
.SH "HOW A MESSAGE IS DELIVERED"
|
||||
The
|
||||
.B tmp
|
||||
directory is used to ensure reliable delivery,
|
||||
as discussed here.
|
||||
|
||||
A program delivers a mail message in six steps.
|
||||
First, it
|
||||
.B chdir()\fPs
|
||||
to the
|
||||
.I maildir
|
||||
directory.
|
||||
Second, it
|
||||
.B stat()s
|
||||
the name
|
||||
.BR tmp/\fItime.pid.host ,
|
||||
where
|
||||
.I time
|
||||
is the number of seconds since the beginning of 1970 GMT,
|
||||
.I pid
|
||||
is the program's process ID,
|
||||
and
|
||||
.I host
|
||||
is the host name.
|
||||
Third, if
|
||||
.B stat()
|
||||
returned anything other than ENOENT,
|
||||
the program sleeps for two seconds, updates
|
||||
.IR time ,
|
||||
and tries the
|
||||
.B stat()
|
||||
again, a limited number of times.
|
||||
Fourth, the program
|
||||
creates
|
||||
.BR tmp/\fItime.pid.host .
|
||||
Fifth, the program
|
||||
.I NFS-writes
|
||||
the message to the file.
|
||||
Sixth, the program
|
||||
.BR link() s
|
||||
the file to
|
||||
.BR new/\fItime.pid.host .
|
||||
At that instant the message has been successfully delivered.
|
||||
|
||||
The delivery program is required to start a 24-hour timer before
|
||||
creating
|
||||
.BR tmp/\fItime.pid.host ,
|
||||
and to abort the delivery
|
||||
if the timer expires.
|
||||
Upon error, timeout, or normal completion,
|
||||
the delivery program may attempt to
|
||||
.B unlink()
|
||||
.BR tmp/\fItime.pid.host .
|
||||
|
||||
.I NFS-writing
|
||||
means
|
||||
(1) as usual, checking the number of bytes returned from each
|
||||
.B write()
|
||||
call;
|
||||
(2) calling
|
||||
.B fsync()
|
||||
and checking its return value;
|
||||
(3) calling
|
||||
.B close()
|
||||
and checking its return value.
|
||||
(Standard NFS implementations handle
|
||||
.B fsync()
|
||||
incorrectly
|
||||
but make up for it by abusing
|
||||
.BR close() .)
|
||||
.SH "HOW A MESSAGE IS READ"
|
||||
A mail reader operates as follows.
|
||||
|
||||
It looks through the
|
||||
.B new
|
||||
directory for new messages.
|
||||
Say there is a new message,
|
||||
.BR new/\fIunique .
|
||||
The reader may freely display the contents of
|
||||
.BR new/\fIunique ,
|
||||
delete
|
||||
.BR new/\fIunique ,
|
||||
or rename
|
||||
.B new/\fIunique
|
||||
as
|
||||
.BR cur/\fIunique:info .
|
||||
See
|
||||
.B http://pobox.com/~djb/proto/maildir.html
|
||||
for the meaning of
|
||||
.IR info .
|
||||
|
||||
The reader is also expected to look through the
|
||||
.B tmp
|
||||
directory and to clean up any old files found there.
|
||||
A file in
|
||||
.B tmp
|
||||
may be safely removed if it
|
||||
has not been accessed in 36 hours.
|
||||
|
||||
It is a good idea for readers to skip all filenames in
|
||||
.B new
|
||||
and
|
||||
.B cur
|
||||
starting with a dot.
|
||||
Other than this, readers should not attempt to parse filenames.
|
||||
.SH "ENVIRONMENT VARIABLES"
|
||||
Mail readers supporting
|
||||
.I maildir
|
||||
use the
|
||||
.B MAILDIR
|
||||
environment variable
|
||||
as the name of the user's primary mail directory.
|
||||
.SH "SEE ALSO"
|
||||
mbox(5),
|
||||
qmail-local(8)
|
|
@ -17,7 +17,8 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.Dd November 11, 2022
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI-THEMES 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -31,15 +32,15 @@ comes with two themes,
|
|||
.Ic dark
|
||||
(default) and
|
||||
.Ic light .
|
||||
.sp
|
||||
.Pp
|
||||
Custom themes are defined as lists of key-values in the configuration files:
|
||||
.Bl -bullet -compact
|
||||
.Bl -item -compact -offset 2
|
||||
.It
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It
|
||||
.Pa $XDG_CONFIG_HOME/meli/themes/*.toml
|
||||
.El
|
||||
.sp
|
||||
.Pp
|
||||
The application theme is defined in the configuration as follows:
|
||||
.Bd -literal
|
||||
[terminal]
|
||||
|
@ -56,9 +57,9 @@ keys are settings for the
|
|||
.Ic compact
|
||||
mail listing style.
|
||||
A setting contains three fields: fg for foreground color, bg for background color, and attrs for text attribute.
|
||||
.sp
|
||||
.Pp
|
||||
.Dl \&"widget.key.label\&" = { fg = \&"Default\&", bg = \&"Default\&", attrs = \&"Default\&" }
|
||||
.sp
|
||||
.Pp
|
||||
Each field contains a value, which may be either a color/attribute, a link (key name) or a valid alias.
|
||||
An alias is a string starting with the \&"\&$\&" character and must be declared in advance in the
|
||||
.Ic color_aliases
|
||||
|
@ -69,10 +70,14 @@ An alias' value can be any valid value, including links and other aliases, as lo
|
|||
In the case of a link the setting's real value depends on the value of the referred key.
|
||||
This allows for defaults within a group of associated values.
|
||||
Cyclic references in a theme results in an error:
|
||||
.sp
|
||||
.Pp
|
||||
.Dl spooky theme contains a cycle: fg: mail.listing.compact.even -> mail.listing.compact.highlighted -> mail.listing.compact.odd -> mail.listing.compact.even
|
||||
.Pp
|
||||
Two themes are included by default, `light` and `dark`.
|
||||
Two themes are included by default,
|
||||
.Ql light
|
||||
and
|
||||
.Ql dark Ns
|
||||
\&.
|
||||
.Sh EXAMPLES
|
||||
Specific settings from already defined themes can be overwritten:
|
||||
.Bd -literal
|
||||
|
@ -100,18 +105,18 @@ Custom themes can be included in your configuration files or be saved independen
|
|||
.Pa $XDG_CONFIG_HOME/meli/themes/
|
||||
directory as TOML files.
|
||||
To start creating a theme right away, you can begin by editing the default theme keys and values:
|
||||
.sp
|
||||
.Pp
|
||||
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
|
||||
.sp
|
||||
.Pp
|
||||
.Pa new_theme.toml
|
||||
will now include all keys and values of the "dark" theme.
|
||||
.sp
|
||||
.Pp
|
||||
.Dl meli print-loaded-themes
|
||||
.sp
|
||||
.Pp
|
||||
will print all loaded themes with the links resolved.
|
||||
.Sh VALID ATTRIBUTE VALUES
|
||||
Case-sensitive.
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
"Default"
|
||||
.It
|
||||
|
@ -123,6 +128,8 @@ Case-sensitive.
|
|||
.It
|
||||
"Underline"
|
||||
.It
|
||||
"Undercurl"
|
||||
.It
|
||||
"Blink"
|
||||
.It
|
||||
"Reverse"
|
||||
|
@ -133,7 +140,7 @@ Any combo of the above separated by a bitwise XOR "\&|" eg "Dim | Italics"
|
|||
.El
|
||||
.Sh VALID COLOR VALUES
|
||||
Color values are of type String with the following valid contents:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
"Default" is the terminal default. (Case-sensitive)
|
||||
.It
|
||||
|
@ -146,8 +153,10 @@ Three character shorthand is also valid, e.g. #09c → #0099cc (Case-insensitive
|
|||
name but with some modifications (for a full table see COLOR NAMES addendum) (Case-sensitive)
|
||||
.El
|
||||
.Sh NO COLOR
|
||||
To completely disable ANSI colors, there are two options:
|
||||
.Bl -bullet -compact
|
||||
To completely disable
|
||||
.Em ANSI
|
||||
colors, there are two options:
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Set the
|
||||
.Ic use_color
|
||||
|
@ -157,17 +166,22 @@ option (section
|
|||
.It
|
||||
The
|
||||
.Ev NO_COLOR
|
||||
environmental variable, when present (regardless of its value), prevents the addition of ANSI color.
|
||||
environmental variable, when present (regardless of its value), prevents the addition of
|
||||
.Em ANSI
|
||||
color.
|
||||
When the configuration value
|
||||
.Ic use_color
|
||||
is explicitly set to true by the user,
|
||||
.Ev NO_COLOR
|
||||
is ignored.
|
||||
.El
|
||||
.sp
|
||||
In this mode, cursor locations (i.e., currently selected entries/items) will use the "reverse video" ANSI attribute to invert the terminal's default foreground/background colors.
|
||||
.Pp
|
||||
In this mode, cursor locations (i.e., currently selected entries/items) will use the
|
||||
.Ql reverse video
|
||||
.Em ANSI
|
||||
attribute to invert the terminal's default foreground/background colors.
|
||||
.Sh VALID KEYS
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
theme_default
|
||||
.It
|
||||
|
@ -312,7 +326,7 @@ pager.highlight_search_current
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Aqua:14:_:Black:0
|
||||
Aquamarine1:122:_:Maroon:1
|
||||
Aquamarine2:86:_:Green:2
|
||||
|
@ -348,7 +362,7 @@ DarkMagenta1:91:_:SpringGreen6:29
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
DarkOliveGreen1:192:_:Turquoise4:30
|
||||
DarkOliveGreen2:155:_:DeepSkyBlue3:31
|
||||
DarkOliveGreen3:191:_:DeepSkyBlue4:32
|
||||
|
@ -384,7 +398,7 @@ DeepPink4:125:_:Grey37:59
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
DeepPink6:162:_:MediumPurple6:60
|
||||
DeepPink7:89:_:SlateBlue2:61
|
||||
DeepPink8:53:_:SlateBlue3:62
|
||||
|
@ -420,7 +434,7 @@ Grey19:236:_:DeepPink7:89
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Grey23:237:_:DarkMagenta:90
|
||||
Grey27:238:_:DarkMagenta1:91
|
||||
Grey3:232:_:DarkViolet1:92
|
||||
|
@ -456,7 +470,7 @@ HotPink2:169:_:LightGreen:119
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
HotPink3:132:_:LightGreen1:120
|
||||
HotPink4:168:_:PaleGreen1:121
|
||||
IndianRed:131:_:Aquamarine1:122
|
||||
|
@ -492,7 +506,7 @@ LightSlateGrey:103:_:DarkOliveGreen6:149
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
LightSteelBlue:147:_:DarkSeaGreen6:150
|
||||
LightSteelBlue1:189:_:DarkSeaGreen3:151
|
||||
LightSteelBlue3:146:_:LightCyan3:152
|
||||
|
@ -528,7 +542,7 @@ NavajoWhite3:144:_:LightGoldenrod3:179
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Navy:4:_:Tan:180
|
||||
NavyBlue:17:_:MistyRose3:181
|
||||
Olive:3:_:Thistle3:182
|
||||
|
@ -564,7 +578,7 @@ Purple5:55:_:Salmon1:209
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Red:9:_:LightCoral:210
|
||||
Red1:196:_:PaleVioletRed1:211
|
||||
Red2:124:_:Orchid2:212
|
||||
|
@ -600,7 +614,7 @@ Tan:180:_:Grey30:239
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Teal:6:_:Grey35:240
|
||||
Thistle1:225:_:Grey39:241
|
||||
Thistle3:182:_:Grey42:242
|
||||
|
@ -621,15 +635,34 @@ Yellow6:148:_:Grey93:255
|
|||
.Sh SEE ALSO
|
||||
.Xr meli 1 ,
|
||||
.Xr meli.conf 5
|
||||
.Sh CONFORMING TO
|
||||
TOML Standard v.0.5.0 https://toml.io/en/v0.5.0
|
||||
.sp
|
||||
https://no-color.org/
|
||||
.Sh STANDARDS
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://toml.io/en/v0.5.0 "TOML Standard v.0.5.0"
|
||||
.It
|
||||
.Lk https://no\-color.org/ "NO_COLOR: disabling ANSI color output by default"
|
||||
.El
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2019
|
||||
.An Manos Pitsidianakis Aq epilys@nessuent.xyz
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Aq https://meli.delivery
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
589
meli/docs/meli.1
589
meli/docs/meli.1
|
@ -17,6 +17,10 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.de HorizontalRule
|
||||
.\"\l'\n(.l\(ru1.25'
|
||||
.sp
|
||||
..
|
||||
.de Shortcut
|
||||
.Sm
|
||||
.Aq \\$1
|
||||
|
@ -40,12 +44,13 @@
|
|||
.Ed
|
||||
.sp
|
||||
..
|
||||
.Dd November 11, 2022
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd terminal e-mail client
|
||||
.Nd terminal e\-mail client
|
||||
.Em μέλι
|
||||
is the Greek word for honey
|
||||
.Sh SYNOPSIS
|
||||
|
@ -64,37 +69,61 @@ Start meli with given configuration file.
|
|||
Create configuration file in
|
||||
.Pa path
|
||||
if given, or at
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
|
||||
\&.
|
||||
If
|
||||
.Ar path
|
||||
is
|
||||
.Ar \-
|
||||
the result is printed to the standard output stream.
|
||||
.It Cm test-config Op Ar path
|
||||
Test a configuration for syntax issues or missing options.
|
||||
The configuration is read from
|
||||
.Pa path
|
||||
if given, or from
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
|
||||
\&.
|
||||
If
|
||||
.Ar path
|
||||
is
|
||||
.Ar \-
|
||||
the configuration is read from the standard input stream.
|
||||
.It Cm man Op Ar page
|
||||
Print documentation page and exit (Piping to a pager is recommended).
|
||||
.It Cm install-man Op Ar path
|
||||
Install manual pages to the first location provided by
|
||||
.Ev MANPATH
|
||||
or
|
||||
.Xr manpath 1 ,
|
||||
unless you specify the directory as an argument.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm edit-config
|
||||
Edit configuration files with
|
||||
.Ev EDITOR
|
||||
or
|
||||
.Ev VISUAL Ns
|
||||
\&.
|
||||
.It Cm test-config Op Ar path
|
||||
Test a configuration file for syntax issues or missing options.
|
||||
.It Cm man Op Ar page
|
||||
Print documentation page and exit (Piping to a pager is recommended).
|
||||
.It Cm install-man Op Ar path
|
||||
Install manual pages to the first location provided by
|
||||
.Ar MANPATH
|
||||
or
|
||||
.Xr manpath 1 ,
|
||||
unless you specify the directory as an argument.
|
||||
.It Cm help
|
||||
Prints help information or the help of the given subcommand(s).
|
||||
.It Cm print-app-directories
|
||||
Print all directories that
|
||||
.Ns Nm
|
||||
creates and uses.
|
||||
.It Cm print-config-path
|
||||
Print location of configuration file that will be loaded on normal app startup.
|
||||
.It Cm print-default-theme
|
||||
Print default theme keys and values in TOML syntax, to be used as a blueprint.
|
||||
.It Cm print-loaded-themes
|
||||
Print all loaded themes in TOML syntax.
|
||||
.It Cm print-used-paths
|
||||
Print all paths that are created and used.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm print-log-path
|
||||
Print log file location.
|
||||
.It Cm view
|
||||
View mail from input file.
|
||||
.El
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
is a terminal mail client aiming for extensive and user-friendly configurability.
|
||||
.Bd -literal
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -128,11 +157,28 @@ At any time, you may press
|
|||
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
|
||||
.Pp
|
||||
The main visual navigation tool, the left-side sidebar may be toggled with
|
||||
.ShortcutPeriod ` listing toggle_menu_visibility
|
||||
.ShortcutPeriod \(ga listing toggle_menu_visibility
|
||||
\&.
|
||||
.Pp
|
||||
Each mailbox may be viewed in 4 modes:
|
||||
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Tg index-style-plain
|
||||
.Em Plain
|
||||
views each mail individually,
|
||||
.It
|
||||
.Tg index-style-threaded
|
||||
.Em Threaded
|
||||
shows their thread relationship visually,
|
||||
.It
|
||||
.Tg index-style-conversations
|
||||
.Em Conversations
|
||||
collapses each thread of e\-mails into a single entry,
|
||||
.It
|
||||
.Tg index-style-compact
|
||||
.Em Compact
|
||||
shows one row per thread.
|
||||
.El
|
||||
.Pp
|
||||
If you're using a light color palette in your terminal, you should set
|
||||
.Em theme = "light"
|
||||
|
@ -148,6 +194,10 @@ See
|
|||
for a more detailed tutorial on using
|
||||
.Nm Ns
|
||||
\&.
|
||||
.Sh SHORTCUTS
|
||||
See
|
||||
.Xr meli.conf 5 SHORTCUTS
|
||||
for shortcuts and their default values.
|
||||
.Sh VIEWING MAIL
|
||||
Open attachments by typing their index in the attachments list and then
|
||||
.ShortcutPeriod a envelope_view open_attachment
|
||||
|
@ -164,16 +214,32 @@ See
|
|||
for the location of the mailcap files and
|
||||
.Xr mailcap 5
|
||||
for their syntax.
|
||||
You can save individual attachments with the
|
||||
.Command save-attachment Ar INDEX Ar path-to-file
|
||||
command.
|
||||
You can save individual attachments with the following command:
|
||||
.Command save\-attachment Ar INDEX Ar path\-to\-file
|
||||
.Ar INDEX
|
||||
is the attachment's index in the listing.
|
||||
If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
|
||||
If the 0th index is provided, the entire message is saved.
|
||||
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
|
||||
.Bl -tag -compact -width 8n
|
||||
.It If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
|
||||
.It If the 0th index is provided, the entire message is saved.
|
||||
.It If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message\-id.
|
||||
.El
|
||||
.Pp
|
||||
You can pipe individual attachments to binaries with the following command:
|
||||
.Command pipe\-attachment Ar INDEX Ar binary Ar ARGS
|
||||
Example usage with the
|
||||
.Xr less 1
|
||||
pager:
|
||||
.D1 pipe\-attachment 0 less
|
||||
If the binary does not wait for your input before exiting, you will probably
|
||||
not see its output since you will return back to the user interface
|
||||
immediately.
|
||||
You can write a wrapper script that pipes your binary's output to
|
||||
.Dl less
|
||||
or
|
||||
.Dl less \-r
|
||||
if you want to preserve the ANSI escape codes in the pager's output.
|
||||
.Sh SEARCH
|
||||
Each e-mail storage backend has a default search method assigned.
|
||||
Each e\-mail storage backend has a default search method assigned.
|
||||
.Em IMAP
|
||||
uses the SEARCH command,
|
||||
.Em notmuch
|
||||
|
@ -200,7 +266,7 @@ To enable sqlite3 indexing for an account set
|
|||
to
|
||||
.Em sqlite3
|
||||
in the configuration file and to create the sqlite3 index issue command:
|
||||
.Command index Ar ACCOUNT_NAME Ns
|
||||
.Command reindex Ar ACCOUNT_NAME Ns
|
||||
To search in the message body type your keywords without any special formatting.
|
||||
To search in specific fields, prepend your search keyword with "field:" like so:
|
||||
.Pp
|
||||
|
@ -222,9 +288,8 @@ alias:
|
|||
.Pc
|
||||
String keywords with spaces must be quoted.
|
||||
Quotes should always be escaped.
|
||||
.sp
|
||||
.Sy Important Notice about IMAP/JMAP
|
||||
.sp
|
||||
.Ss Important Notice about IMAP/JMAP
|
||||
.HorizontalRule
|
||||
To prevent downloading all your messages from your IMAP/JMAP server, don't set
|
||||
.Em search_backend
|
||||
to
|
||||
|
@ -233,14 +298,17 @@ to
|
|||
.Nm
|
||||
will relay your queries to the IMAP server.
|
||||
Expect a delay between query and response.
|
||||
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
|
||||
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable delay.
|
||||
.Ss QUERY ABNF SYNTAX
|
||||
.Bl -bullet
|
||||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
|
||||
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | message_id | in_reply_to | references | header | all_addresses | subject | flags | has_attachment | query \&"or\&" query | query \&"and\&" query | not query
|
||||
.It
|
||||
.Li not = \&"not\&" | \&"!\&"
|
||||
.It
|
||||
.Li has_attachment = \&"has:attachment\&" | \&"has:attachments\&"
|
||||
.It
|
||||
.Li quoted = ALPHA / SP *(ALPHA / DIGIT / SP)
|
||||
.It
|
||||
.Li term = ALPHA *(ALPHA / DIGIT) | DQUOTE quoted DQUOTE
|
||||
|
@ -251,6 +319,8 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
|
|||
.It
|
||||
.Li flagterm = flagval | flagval \&",\&" flagterm
|
||||
.It
|
||||
.Li flags = \&"flag:\&" flag | \&"flags:\&" flag | \&"tag:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
|
||||
.It
|
||||
.Li from = \&"from:\&" term
|
||||
.It
|
||||
.Li to = \&"to:\&" term
|
||||
|
@ -259,16 +329,39 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
|
|||
.It
|
||||
.Li bcc = \&"bcc:\&" term
|
||||
.It
|
||||
.Li alladdresses = \&"alladdresses:\&" term
|
||||
.Li message_id = \&"message-id:\&" term | \&"msg-id:\&" term
|
||||
.It
|
||||
.Li in_reply_to = \&"in-reply-to:\&" term
|
||||
.It
|
||||
.Li references = \&"references:\&" term
|
||||
.It
|
||||
.Li header = \&"header:\&" field_name \&",\&" field_value
|
||||
.It
|
||||
.Li field_name = term
|
||||
.It
|
||||
.Li field_value = term
|
||||
.It
|
||||
.Li all_addresses = \&"all-addresses:\&" term
|
||||
.It
|
||||
.Li subject = \&"subject:\&" term
|
||||
.It
|
||||
.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
|
||||
.El
|
||||
.Sh FLAGS
|
||||
.Nm
|
||||
supports the basic maildir flags: passed, replied, seen, trashed, draft and flagged.
|
||||
Flags can be searched with the
|
||||
.Ns Ql flags:
|
||||
prefix in a search query, and can be modified by
|
||||
.Command flag set FLAG
|
||||
and
|
||||
.Command flag unset FLAG
|
||||
.Sh TAGS
|
||||
.Nm
|
||||
supports tagging in notmuch and IMAP/JMAP backends.
|
||||
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
|
||||
Tags can be searched with the
|
||||
.Ns Ql tags:
|
||||
or
|
||||
.Ns Ql flags:
|
||||
prefix in a search query, and can be modified by
|
||||
.Command tag add TAG
|
||||
and
|
||||
.Command tag remove TAG
|
||||
|
@ -289,7 +382,8 @@ To reply to a mail, press
|
|||
\&.
|
||||
Both these actions open the mail composer view in a new tab.
|
||||
.Ss Editing text
|
||||
.Bl -bullet -compact
|
||||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Edit the header fields by selecting with the arrow keys and pressing
|
||||
.Shortcut Enter general focus_in_text_field
|
||||
|
@ -332,12 +426,14 @@ and to resume editing press the
|
|||
command again.
|
||||
.El
|
||||
.Ss Attachments
|
||||
.HorizontalRule
|
||||
Attachments may be handled with the
|
||||
.Cm add-attachment Ns
|
||||
,
|
||||
.Cm remove-attachment
|
||||
commands (see below).
|
||||
.Ss Sending
|
||||
.HorizontalRule
|
||||
Finally, pressing
|
||||
.Shortcut s composing send_mail
|
||||
will send your message according to your settings
|
||||
|
@ -355,6 +451,7 @@ On complete failure to save your draft or sent message it will be saved in your
|
|||
.Em tmp
|
||||
directory instead and you will be notified of its location.
|
||||
.Ss Drafts
|
||||
.HorizontalRule
|
||||
To save your draft without sending it, issue
|
||||
.Em COMMAND
|
||||
.Cm close
|
||||
|
@ -366,11 +463,10 @@ To open a draft for further editing, select your draft in the mail listing and p
|
|||
.Sh CONTACTS
|
||||
.Nm
|
||||
supports three kinds of contact backends:
|
||||
.sp
|
||||
.Bl -enum -compact -offset indent
|
||||
.Bl -enum -compact
|
||||
.It
|
||||
an internal format that gets saved under
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/contacts Ns
|
||||
\&.
|
||||
.It
|
||||
vCard files (v3, v4) through the
|
||||
|
@ -389,7 +485,7 @@ compatible alias file in the option
|
|||
.sp
|
||||
See
|
||||
.Xr meli.conf 5 ACCOUNTS
|
||||
for the complete account configuration values.
|
||||
for the complete account contact configuration values.
|
||||
.Sh MODES
|
||||
.Bl -tag -compact -width 8n
|
||||
.It NORMAL
|
||||
|
@ -409,8 +505,9 @@ captures all input as text input, and is exited with
|
|||
.Cm Esc
|
||||
key.
|
||||
.El
|
||||
.Ss COMMAND Mode
|
||||
.Sh COMMAND
|
||||
.Ss Mail listing commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm set Ar plain | threaded | compact | conversations
|
||||
set the way mailboxes are displayed
|
||||
|
@ -424,9 +521,9 @@ threaded:shows threads as a tree structure
|
|||
plain:shows one row per mail, regardless of threading
|
||||
.TE
|
||||
.Bl -tag -width 36n
|
||||
.It Cm sort Ar subject | date \ Ar asc | desc
|
||||
.It Cm sort Oo Ar subject | date Oc Ar asc | desc
|
||||
sort mail listing
|
||||
.It Cm subsort Ar subject | date \ Ar asc | desc
|
||||
.It Cm subsort Oo Ar subject | date Oc Ar asc | desc
|
||||
sorts only the first level of replies.
|
||||
.It Cm go Ar n
|
||||
where
|
||||
|
@ -445,37 +542,49 @@ Escape exits search results.
|
|||
select threads matching
|
||||
.Ar STRING
|
||||
query.
|
||||
.It Cm set seen, set unseen
|
||||
.It Cm clear-selection
|
||||
Clear current selection.
|
||||
.It Cm set Ar seen | unseen
|
||||
Set seen status of message.
|
||||
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
|
||||
Import mail from file into given mailbox.
|
||||
.It Cm copyto, moveto Ar MAILBOX_PATH
|
||||
Copy or move to other mailbox.
|
||||
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
|
||||
Copy or move to another account's mailbox.
|
||||
Copy or move to another account's mailbox.
|
||||
.It Cm delete
|
||||
Delete selected threads.
|
||||
Delete selected entries.
|
||||
.It Cm export-mbox Ar FILEPATH
|
||||
Export selected threads to mboxcl2 file.
|
||||
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm create\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
create mailbox with given path.
|
||||
Be careful with backends and separator sensitivity (eg IMAP)
|
||||
.It Cm subscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm subscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
subscribe to mailbox with given path
|
||||
.It Cm unsubscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm unsubscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
unsubscribe to mailbox with given path
|
||||
.It Cm rename-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
|
||||
.It Cm rename\-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
|
||||
rename mailbox
|
||||
.It Cm delete-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm delete\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
deletes mailbox in the mail backend.
|
||||
This action is unreversible.
|
||||
This action is irreversible.
|
||||
.El
|
||||
.Ss Mail view commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm pipe Ar EXECUTABLE Ar ARGS
|
||||
.It Cm pipe Ar EXECUTABLE Oo Ar ARGS Oc
|
||||
pipe pager contents to binary
|
||||
.It Cm filter Ar EXECUTABLE Ar ARGS
|
||||
.It Cm filter Ar EXECUTABLE Oo Ar ARGS Oc
|
||||
filter and display pager contents through command
|
||||
.It Cm filter
|
||||
select a filter from
|
||||
.Ic pager.named_filters
|
||||
configuration value
|
||||
.Po
|
||||
See
|
||||
.Xr meli.conf 5 PAGER
|
||||
for its syntax
|
||||
.Pc
|
||||
.It Cm list-post
|
||||
post in list of viewed envelope
|
||||
.It Cm list-unsubscribe
|
||||
|
@ -484,11 +593,12 @@ unsubscribe automatically from list of viewed envelope
|
|||
open list archive with
|
||||
.Cm xdg-open
|
||||
.El
|
||||
.Ss composing mail commands
|
||||
.Ss Composing mail commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm mailto Ar MAILTO_ADDRESS
|
||||
Opens a composer tab with initial values parsed from the
|
||||
.Li mailto:
|
||||
.Li mailto :
|
||||
address.
|
||||
.It Cm add-attachment Ar PATH
|
||||
in composer, add
|
||||
|
@ -507,7 +617,11 @@ in
|
|||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
The command should print file paths in stdout, separated by NUL bytes.
|
||||
Example usage with
|
||||
.Xr fzf 1 Ns
|
||||
:
|
||||
.D1 add-attachment-file-picker < fzf --print0
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
|
@ -519,7 +633,8 @@ for PGP configuration.
|
|||
.It Cm save-draft
|
||||
saves a copy of the draft in the Draft folder
|
||||
.El
|
||||
.Ss generic commands
|
||||
.Ss Generic commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm open-in-tab
|
||||
opens envelope view in new tab
|
||||
|
@ -543,10 +658,6 @@ Useful if you want to reload some settings without restarting
|
|||
.Nm Ns
|
||||
\&.
|
||||
.El
|
||||
.Sh SHORTCUTS
|
||||
See
|
||||
.Xr meli.conf 5 SHORTCUTS
|
||||
for shortcuts and their default values.
|
||||
.Sh EXIT STATUS
|
||||
.Nm
|
||||
exits with 0 on a successful run.
|
||||
|
@ -558,13 +669,15 @@ catchall for general errors
|
|||
process panic
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.It Ev EDITOR
|
||||
Specifies the editor to use
|
||||
.It Ev MELI_CONFIG
|
||||
Override the configuration file
|
||||
.It Ev NO_COLOR
|
||||
When present (regardless of its value), prevents the addition of ANSI color.
|
||||
When defined (regardless of its value), prevents the addition of
|
||||
.Em ANSI
|
||||
color.
|
||||
The configuration value
|
||||
.Ic use_color
|
||||
overrides this.
|
||||
|
@ -572,7 +685,7 @@ overrides this.
|
|||
.Sh FILES
|
||||
.Nm
|
||||
uses the following parts of the XDG standard:
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.It Ev XDG_CONFIG_HOME
|
||||
defaults to
|
||||
.Pa ~/.config/
|
||||
|
@ -582,17 +695,13 @@ defaults to
|
|||
.El
|
||||
.Pp
|
||||
and appropriates the following locations:
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.It Pa $XDG_CONFIG_HOME/meli/
|
||||
User configuration directory
|
||||
.It Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
User configuration file, see
|
||||
.Xr meli.conf 5
|
||||
for its syntax and values.
|
||||
.It Pa $XDG_CONFIG_HOME/meli/hooks/*
|
||||
Reserved for event hooks.
|
||||
.It Pa $XDG_CONFIG_HOME/meli/plugins/*
|
||||
Reserved for plugin files.
|
||||
.It Pa $XDG_CACHE_HOME/meli/*
|
||||
Internal cached data used by meli.
|
||||
.It Pa $XDG_DATA_HOME/meli/*
|
||||
|
@ -621,75 +730,281 @@ Mailcap entries are searched for in the following files, in this order:
|
|||
.It
|
||||
.Pa /usr/local/etc/mailcap
|
||||
.El
|
||||
.Sh STANDARDS
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Rs
|
||||
.%B XDG Base Directory Specification
|
||||
.%O Version 0.8
|
||||
.%A Waldo Bastian
|
||||
.%A Allison Karlitskaya
|
||||
.%A Lennart Poettering
|
||||
.%A Johannes Löthberg
|
||||
.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
.%D May 08, 2021
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B maildir
|
||||
.%A Daniel J. Bernstein
|
||||
.%U https://cr.yp.to/proto/maildir.html
|
||||
.%D 1995
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC1524 A User Agent Configuration Mechanism For Multimedia Mail Format Information
|
||||
.%O mailcap file
|
||||
.%I Legacy
|
||||
.%D September 01, 1993
|
||||
.%A Dr. Nathaniel S. Borenstein
|
||||
.%U https://datatracker.ietf.org/doc/rfc1524/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2047 MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
|
||||
.%I IETF
|
||||
.%D November 01, 1996
|
||||
.%A Keith Moore
|
||||
.%U https://datatracker.ietf.org/doc/rfc2047/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2183 Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
|
||||
.%I Legacy
|
||||
.%D August 01, 1997
|
||||
.%A Rens Troost
|
||||
.%A Steve Dorner
|
||||
.%A Keith Moore
|
||||
.%U https://datatracker.ietf.org/doc/rfc2183/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2369 The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
|
||||
.%I Legacy
|
||||
.%D July 01, 1998
|
||||
.%A Joshua D. Baer
|
||||
.%A Grant Neufeld
|
||||
.%U https://datatracker.ietf.org/doc/rfc2369/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2426 vCard MIME Directory Profile
|
||||
.%O vCard Version 3
|
||||
.%I IETF
|
||||
.%D September 01, 1998
|
||||
.%A Frank Dawson
|
||||
.%A Tim Howes
|
||||
.%U https://datatracker.ietf.org/doc/rfc2426/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2971 IMAP4 ID extension
|
||||
.%I IETF
|
||||
.%D October 01, 2000
|
||||
.%A Tim Showalter
|
||||
.%U https://datatracker.ietf.org/doc/rfc2971/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3156 MIME Security with OpenPGP
|
||||
.%I IETF
|
||||
.%D August 01, 2001
|
||||
.%A Thomas Roessler
|
||||
.%A Michael Elkins
|
||||
.%A Raph Levien
|
||||
.%A Dave Del Torto
|
||||
.%U https://datatracker.ietf.org/doc/rfc3156/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3461 Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
|
||||
.%I IETF
|
||||
.%D January 23, 2003
|
||||
.%A Keith Moore
|
||||
.%U https://datatracker.ietf.org/doc/rfc3461/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
|
||||
.%I IETF
|
||||
.%D March 18, 2003
|
||||
.%A Mark Crispin
|
||||
.%U https://datatracker.ietf.org/doc/rfc3501/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3676 The Text/Plain Format and DelSp Parameters
|
||||
.%I IETF
|
||||
.%D February 19, 2004
|
||||
.%A Randall Gellens
|
||||
.%U https://datatracker.ietf.org/doc/rfc3676/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3691 Internet Message Access Protocol (IMAP) UNSELECT command
|
||||
.%I IETF
|
||||
.%D February 20, 2004
|
||||
.%A Alexey Melnikov
|
||||
.%U https://datatracker.ietf.org/doc/rfc3691/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3977 Network News Transfer Protocol (NNTP)
|
||||
.%I IETF
|
||||
.%D October 26, 2006
|
||||
.%A Clive Feather
|
||||
.%U https://datatracker.ietf.org/doc/rfc3977/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4505 Anonymous Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.%I IETF
|
||||
.%D June 12, 2006
|
||||
.%A Kurt Zeilenga
|
||||
.%U https://datatracker.ietf.org/doc/rfc4505/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
|
||||
.%I IETF
|
||||
.%D June 16, 2006
|
||||
.%A Alexey Melnikov
|
||||
.%U https://datatracker.ietf.org/doc/rfc4549/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4616 The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.%I IETF
|
||||
.%D August 31, 2006
|
||||
.%A Kurt Zeilenga
|
||||
.%U https://datatracker.ietf.org/doc/rfc4616/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4954 SMTP Service Extension for Authentication
|
||||
.%I IETF
|
||||
.%D July 23, 2007
|
||||
.%A Rob Siemborski
|
||||
.%A Alexey Melnikov
|
||||
.%U https://datatracker.ietf.org/doc/rfc4954/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC5321 Simple Mail Transfer Protocol
|
||||
.%I IETF
|
||||
.%D October 01, 2008
|
||||
.%A Dr. John C. Klensin
|
||||
.%U https://datatracker.ietf.org/doc/rfc5321/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC5322 Internet Message Format
|
||||
.%I IETF
|
||||
.%D October 01, 2008
|
||||
.%A Pete Resnick
|
||||
.%U https://datatracker.ietf.org/doc/rfc5322/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6048 Network News Transfer Protocol (NNTP) Additions to LIST Command
|
||||
.%I IETF
|
||||
.%D November 22, 2010
|
||||
.%A Julien ÉLIE
|
||||
.%U https://datatracker.ietf.org/doc/rfc6048/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6152 SMTP Service Extension for 8-bit MIME Transport
|
||||
.%I IETF
|
||||
.%D March 07, 2011
|
||||
.%A Dave Crocker
|
||||
.%A Dr. John C. Klensin
|
||||
.%A Dr. Marshall T. Rose
|
||||
.%A Ned Freed
|
||||
.%U https://datatracker.ietf.org/doc/rfc6152/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6350 vCard Format Specification
|
||||
.%O vCard Version 4
|
||||
.%I IETF
|
||||
.%D August 31, 2011
|
||||
.%A Simon Perreault
|
||||
.%U https://datatracker.ietf.org/doc/rfc6350/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6532 Internationalized Email Headers
|
||||
.%I IETF
|
||||
.%D February 17, 2012
|
||||
.%A Abel Yang
|
||||
.%A Shawn Steele
|
||||
.%A Ned Freed
|
||||
.%U https://datatracker.ietf.org/doc/rfc6532/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6868 Parameter Value Encoding in iCalendar and vCard
|
||||
.%I IETF
|
||||
.%D February 14, 2013
|
||||
.%A Cyrus Daboo
|
||||
.%U https://datatracker.ietf.org/doc/rfc6868/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC7162 IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
|
||||
.%I IETF
|
||||
.%D May 23, 2014
|
||||
.%A Alexey Melnikov
|
||||
.%A Dave Cridland
|
||||
.%U https://datatracker.ietf.org/doc/rfc7162/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC8620 The JSON Meta Application Protocol (JMAP)
|
||||
.%I IETF
|
||||
.%D July 18, 2019
|
||||
.%A Neil Jenkins
|
||||
.%A Chris Newman
|
||||
.%U https://datatracker.ietf.org/doc/rfc8620/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC8621 The JSON Meta Application Protocol (JMAP) for Mail
|
||||
.%I IETF
|
||||
.%D August 08, 2019
|
||||
.%A Neil Jenkins
|
||||
.%A Chris Newman
|
||||
.%U https://datatracker.ietf.org/doc/rfc8621/
|
||||
.Re
|
||||
.El
|
||||
.Sh SEE ALSO
|
||||
.Xr meli.conf 5 ,
|
||||
.Xr meli-themes 5 ,
|
||||
.Xr meli 7 ,
|
||||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh CONFORMING TO
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
XDG Standard
|
||||
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
|
||||
\&.
|
||||
.It
|
||||
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
|
||||
.It
|
||||
RFC 5322: Internet Message Format
|
||||
.It
|
||||
RFC 6532: Internationalized Email Headers
|
||||
.It
|
||||
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
|
||||
.It
|
||||
RFC 3676: The Text/Plain Format and DelSp Parameters
|
||||
.It
|
||||
RFC 3156: MIME Security with OpenPGP
|
||||
.It
|
||||
RFC 2183: Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
|
||||
.It
|
||||
RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
|
||||
.It
|
||||
.Li maildir
|
||||
.Lk https://cr.yp.to/proto/maildir.html Ns
|
||||
\&.
|
||||
.It
|
||||
RFC 5321: Simple Mail Transfer Protocol
|
||||
.It
|
||||
RFC 3461: Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
|
||||
.It
|
||||
RFC 4954: SMTP Service Extension for Authentication
|
||||
.It
|
||||
RFC 6152: SMTP Service Extension for 8-bit MIME Transport
|
||||
.It
|
||||
RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.It
|
||||
RFC 3501: INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
|
||||
.It
|
||||
RFC 3691: Internet Message Access Protocol (IMAP) UNSELECT command
|
||||
.It
|
||||
RFC 4549: Synch Ops for Disconnected IMAP4 Clients
|
||||
.It
|
||||
RFC 7162: IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
|
||||
.It
|
||||
RFC 8620: The JSON Meta Application Protocol (JMAP)
|
||||
.It
|
||||
RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail
|
||||
.It
|
||||
RFC 3977: Network News Transfer Protocol (NNTP)
|
||||
.It
|
||||
RFC 6048: Network News Transfer Protocol (NNTP) Additions to LIST Command
|
||||
.It
|
||||
vCard Version 3, RFC 2426: vCard MIME Directory Profile
|
||||
.It
|
||||
vCard Version 4, RFC 6350: vCard Format Specification
|
||||
.It
|
||||
RFC 6868 Parameter Value Encoding in iCalendar and vCard
|
||||
.El
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Mt manos@pitsidianak.is
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
138
meli/docs/meli.7
138
meli/docs/meli.7
|
@ -40,22 +40,23 @@
|
|||
.Pc Ns
|
||||
..
|
||||
.de Command
|
||||
.Bd -offset 1n -ragged
|
||||
.Bd -ragged -offset 1n
|
||||
.Cm \\$*
|
||||
.Ed
|
||||
..
|
||||
.Dd November 11, 2022
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI 7
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd Tutorial for the meli terminal e-mail client
|
||||
.Nd Tutorial for the meli terminal e\-mail client
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op ...
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
is a terminal mail client aiming for extensive and user\-friendly configurability.
|
||||
.Bd -literal -offset center
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -158,9 +159,9 @@ key.
|
|||
.It EMBED
|
||||
This is the mode of the embed terminal emulator.
|
||||
To exit an embedded application, issue
|
||||
.Aq Ctrl-C
|
||||
.Aq Ctrl\-C
|
||||
to kill it or
|
||||
.Aq Ctrl-Z
|
||||
.Aq Ctrl\-Z
|
||||
to stop the program and follow the instructions on
|
||||
.Nm
|
||||
to exit.
|
||||
|
@ -229,7 +230,7 @@ This is the view you will spend more time with in
|
|||
\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut ` listing toggle_menu_visibility
|
||||
.Shortcut \(ga listing toggle_menu_visibility
|
||||
to toggle the sidebars visibility.
|
||||
.Pp
|
||||
Press
|
||||
|
@ -237,16 +238,16 @@ Press
|
|||
to switch focus on the sidebar menu.
|
||||
Press
|
||||
.Shortcut Right listing focus_left
|
||||
to switch focus on the e-mail list.
|
||||
to switch focus on the e\-mail list.
|
||||
.Pp
|
||||
On the e-mail list, press
|
||||
On the e\-mail list, press
|
||||
.Shortcut k listing scroll_up
|
||||
to scroll up, and
|
||||
.Shortcut j listing scroll_down
|
||||
to scroll down.
|
||||
Press
|
||||
.Shortcut Enter listing open_entry
|
||||
to open an e-mail entry and
|
||||
to open an e\-mail entry and
|
||||
.Shortcut i listing exit_entry
|
||||
to exit it.
|
||||
.Bd -ragged
|
||||
|
@ -294,9 +295,9 @@ See
|
|||
for details.
|
||||
.Pp
|
||||
You can increase the sidebar's width with
|
||||
.Shortcut Ctrl-p listing increase_sidebar
|
||||
.Shortcut Ctrl\-p listing increase_sidebar
|
||||
and decrease with
|
||||
.ShortcutPeriod Ctrl-o listing decrease_sidebar
|
||||
.ShortcutPeriod Ctrl\-o listing decrease_sidebar
|
||||
\&.
|
||||
.Bd -ragged
|
||||
.Sy The status bar.
|
||||
|
@ -310,7 +311,7 @@ and decrease with
|
|||
The status bar shows which mode you are, and the status message of the current view.
|
||||
In the pictured example, it shows the status of a mailbox called
|
||||
.Dq Inbox
|
||||
with lots of e-mails.
|
||||
with lots of e\-mails.
|
||||
.Bd -ragged
|
||||
.Sy The number modifier buffer.
|
||||
.Ed
|
||||
|
@ -330,7 +331,7 @@ entries.
|
|||
Another use of the number buffer is opening URLs inside the pager.
|
||||
See
|
||||
.Sx PAGER
|
||||
for an explanation of interacting with URLs in e-mails.
|
||||
for an explanation of interacting with URLs in e\-mails.
|
||||
.Pp
|
||||
Pressing numbers in
|
||||
.Sy NORMAL
|
||||
|
@ -343,16 +344,16 @@ There are four different list styles:
|
|||
.Bl -hyphen -compact
|
||||
.It
|
||||
.Qq plain
|
||||
which shows one line per e-mail.
|
||||
which shows one line per e\-mail.
|
||||
.It
|
||||
.Qq threaded
|
||||
which shows a threaded view with drawn tree structure.
|
||||
.It
|
||||
.Qq compact
|
||||
which shows one line per thread which can include multiple e-mails.
|
||||
which shows one line per thread which can include multiple e\-mails.
|
||||
.It
|
||||
.Qq conversations
|
||||
which shows more than one line per thread which can include multiple e-mails with more details about the thread.
|
||||
which shows more than one line per thread which can include multiple e\-mails with more details about the thread.
|
||||
.El
|
||||
.Bd -ragged
|
||||
.Sy Plain view\&.
|
||||
|
@ -411,9 +412,9 @@ which shows more than one line per thread which can include multiple e-mails wit
|
|||
.Sy Performing actions on entries and/or selections\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut v listing select_entry
|
||||
.Shortcut V listing select_entry
|
||||
to toggle the selection of a single entry.
|
||||
.Qq select_entry
|
||||
.Shortcut v listing select_motion
|
||||
can be prefixed by a number modifier and affixed by a scrolling motion (up or down) to select multiple entries.
|
||||
.Tg number-modifier
|
||||
Simple set operations can be performed on a selection with these shortcut modifiers:
|
||||
|
@ -421,13 +422,13 @@ Simple set operations can be performed on a selection with these shortcut modifi
|
|||
.Bl -hyphen -compact
|
||||
.It
|
||||
Union modifier:
|
||||
.Shortcut Ctrl-u listing union_modifier
|
||||
.Shortcut Ctrl\-u listing union_modifier
|
||||
.It
|
||||
Difference modifier:
|
||||
.Shortcut Ctrl-d listing diff_modifier
|
||||
.Shortcut Ctrl\-d listing diff_modifier
|
||||
.It
|
||||
Intersection modifier:
|
||||
.Shortcut Ctrl-i listing intersection_modifier
|
||||
.Shortcut Ctrl\-i listing intersection_modifier
|
||||
.El
|
||||
.Pp
|
||||
To set an entry as
|
||||
|
@ -445,7 +446,11 @@ which also has its complement
|
|||
.sp
|
||||
action.
|
||||
.Pp
|
||||
For e-mail backends that support tags
|
||||
For e\-mail backends that support flags you can use the following commands on entries and selections to modify them:
|
||||
.Command flag set FLAG
|
||||
.Command flag unset FLAG
|
||||
.Pp
|
||||
For e\-mail backends that support tags
|
||||
.Po
|
||||
like
|
||||
.Qq IMAP
|
||||
|
@ -463,10 +468,13 @@ you can use the following commands on entries and selections to modify them:
|
|||
and
|
||||
.Ic ignore_tags
|
||||
for how to set tag colors and tag visibility)
|
||||
You can clear the selection with the
|
||||
.Aq Esc
|
||||
key.
|
||||
.Sh PAGER
|
||||
You can open an e-mail entry by pressing
|
||||
You can open an e\-mail entry by pressing
|
||||
.ShortcutPeriod Enter listing open_entry
|
||||
\&. This brings up the e-mail view with the e-mail content inside a pager.
|
||||
\&. This brings up the e\-mail view with the e\-mail content inside a pager.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│Date: Sat, 21 May 2022 16:16:11 +0300 ▀│
|
||||
|
@ -494,14 +502,14 @@ You can open an e-mail entry by pressing
|
|||
└────────────────────────────────────────────────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Em The\ pager\ displaying\ an\ e-mail\&.
|
||||
.Em The\ pager\ displaying\ an\ e\-mail\&.
|
||||
.Ed
|
||||
.Pp
|
||||
The pager is simple to use.
|
||||
Scroll with the following:
|
||||
.Bl -hang -width 27n
|
||||
.It Go to next pager page
|
||||
.Shortcut PageDown pager page_down
|
||||
.Shortcut PageDown pager page_down
|
||||
.It Go to previous pager page
|
||||
.Shortcut PageUp pager page_up
|
||||
.It Scroll down pager.
|
||||
|
@ -516,7 +524,7 @@ which will act as a multiplier.
|
|||
.Pp
|
||||
The pager can enter a special
|
||||
.Em url
|
||||
mode which will prefix all detected hyperlinks and e-mail addresses with a number inside square brackets
|
||||
mode which will prefix all detected hyperlinks and e\-mail addresses with a number inside square brackets
|
||||
.ShortcutPeriod u pager toggle_url_mode
|
||||
\&.
|
||||
Writing down a chosen number as a number modifier
|
||||
|
@ -547,13 +555,13 @@ for more details
|
|||
.Pc Ns
|
||||
\&.
|
||||
.Sh MAIL VIEW
|
||||
Other things you can do when viewing e-mail:
|
||||
.Bl -bullet -compact
|
||||
Other things you can do when viewing e\-mail:
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Most importantly, you can exit the mail view with:
|
||||
.Shortcut i listing exit_entry
|
||||
.It
|
||||
Add addresses from the e-mail headers to contacts:
|
||||
Add addresses from the e\-mail headers to contacts:
|
||||
.Shortcut c envelope_view add_addresses_to_contacts
|
||||
.It
|
||||
Open an attachment by entering its index as a number modifier and pressing:
|
||||
|
@ -569,39 +577,39 @@ Reply to envelope:
|
|||
.Shortcut R envelope_view reply
|
||||
.It
|
||||
Reply to author:
|
||||
.Shortcut Ctrl-r envelope_view reply_to_author
|
||||
.Shortcut Ctrl\-r envelope_view reply_to_author
|
||||
.It
|
||||
Reply to all/Reply to list/Follow up:
|
||||
.Shortcut Ctrl-g envelope_view reply_to_all
|
||||
.Shortcut Ctrl\-g envelope_view reply_to_all
|
||||
.It
|
||||
Forward email:
|
||||
.Shortcut Ctrl-f envelope_view forward
|
||||
Forward e\-mail:
|
||||
.Shortcut Ctrl\-f envelope_view forward
|
||||
.It
|
||||
Expand extra headers: (References and others)
|
||||
.Shortcut h envelope_view toggle_expand_headerk
|
||||
.Shortcut h envelope_view toggle_expand_headers
|
||||
.It
|
||||
View envelope source in a pager: (toggles between raw and decoded source)
|
||||
.Shortcut M-r envelope_view view_raw_source
|
||||
.Shortcut M\-r envelope_view view_raw_source
|
||||
.It
|
||||
Return to envelope_view if viewing raw source or attachment:
|
||||
.Shortcut r envelope_view return_to_normal_view
|
||||
.El
|
||||
.Sh COMPOSING
|
||||
To compose an e-mail, you can either start with an empty draft by pressing
|
||||
To compose an e\-mail, you can either start with an empty draft by pressing
|
||||
.Shortcut m listing new_mail
|
||||
which opens a composer view in a new tab.
|
||||
To reply to a specific e-mail, when in envelope view you can select the specific action you want to take:
|
||||
To reply to a specific e\-mail, when in envelope view you can select the specific action you want to take:
|
||||
.sp
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Reply to envelope.
|
||||
.Shortcut R envelope_view reply
|
||||
.It
|
||||
Reply to author.
|
||||
.Shortcut Ctrl-r envelope_view reply_to_author
|
||||
.Shortcut Ctrl\-r envelope_view reply_to_author
|
||||
.It
|
||||
Reply to all.
|
||||
.Shortcut Ctrl-g envelope_view reply_to_all
|
||||
.Shortcut Ctrl\-g envelope_view reply_to_all
|
||||
.El
|
||||
.sp
|
||||
To launch your editor, press
|
||||
|
@ -688,25 +696,29 @@ the\ actual\ embedding\ is\ seamless\&.
|
|||
.Ed
|
||||
.Ss composing mail commands
|
||||
.Bl -tag -width 36n
|
||||
.It Cm add-attachment Ar PATH
|
||||
.It Cm add\-attachment Ar PATH
|
||||
in composer, add
|
||||
.Ar PATH
|
||||
as an attachment
|
||||
.It Cm add-attachment < Ar CMD Ar ARGS
|
||||
.It Cm add\-attachment < Ar CMD Ar ARGS
|
||||
in composer, pipe
|
||||
.Ar CMD Ar ARGS
|
||||
output into an attachment
|
||||
.It Cm add-attachment-file-picker
|
||||
.It Cm add\-attachment\-file\-picker
|
||||
Launch command defined in the configuration value
|
||||
.Ic file_picker_command
|
||||
in
|
||||
.Xr meli.conf 5 TERMINAL
|
||||
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
|
||||
.It Cm add\-attachment\-file\-picker < Ar CMD Ar ARGS
|
||||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
The command should print file paths in stdout, separated by NUL bytes.
|
||||
Example usage with
|
||||
.Xr fzf Ns
|
||||
:
|
||||
.D1 add-attachment-file-picker < fzf --print0
|
||||
.It Cm remove\-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
toggle between signing and not signing this message.
|
||||
|
@ -714,7 +726,7 @@ If the gpg invocation fails then the mail won't be sent.
|
|||
See
|
||||
.Xr meli.conf 5 PGP
|
||||
for PGP configuration.
|
||||
.It Cm save-draft
|
||||
.It Cm save\-draft
|
||||
saves a copy of the draft in the Draft folder
|
||||
.El
|
||||
.\" [ref:TODO]: add contacts section
|
||||
|
@ -731,12 +743,26 @@ for documentation on how to theme
|
|||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Mt manos@pitsidianak.is
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
.Lk https://github.com/meli/meli
|
||||
.Lk https://crates.io/crates/meli
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
File diff suppressed because it is too large
Load diff
227
meli/docs/meli.conf.examples.5
Normal file
227
meli/docs/meli.conf.examples.5
Normal file
|
@ -0,0 +1,227 @@
|
|||
.\" meli - meli.conf.examples.5
|
||||
.\"
|
||||
.\" Copyright 2024 Manos Pitsidianakis
|
||||
.\"
|
||||
.\" This file is part of meli.
|
||||
.\"
|
||||
.\" meli is free software: you can redistribute it and/or modify
|
||||
.\" it under the terms of the GNU General Public License as published by
|
||||
.\" the Free Software Foundation, either version 3 of the License, or
|
||||
.\" (at your option) any later version.
|
||||
.\"
|
||||
.\" meli is distributed in the hope that it will be useful,
|
||||
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
.\" GNU General Public License for more details.
|
||||
.\"
|
||||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.\" SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
.de HorizontalRule
|
||||
.\"\l'\n(.l\(ru1.25'
|
||||
.sp
|
||||
..
|
||||
.de LiteralStringValue
|
||||
.Sm
|
||||
.Po Qo
|
||||
.Em Li \\$1
|
||||
.Qc Pc
|
||||
.Sm
|
||||
..
|
||||
.de LiteralStringValueRenders
|
||||
.LiteralStringValue \\$1
|
||||
.shift 1
|
||||
.Bo
|
||||
.Sm
|
||||
Rendered as:
|
||||
.Li r##
|
||||
.Qo
|
||||
\\$1
|
||||
.Qc
|
||||
.Li ##
|
||||
.Bc
|
||||
.Sm
|
||||
..
|
||||
.\".Dd November 11, 2022
|
||||
.Dd November 22, 2024
|
||||
.Dt MELI.CONF.EXAMPLES 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli.conf examples
|
||||
.Nd Example configurations for various mail backends supported by the
|
||||
.Xr meli 1
|
||||
terminal e-mail client
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\".Sh SYNOPSIS
|
||||
.\".Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".\"
|
||||
.\".Sh DESCRIPTION
|
||||
.Sh MAILDIR ACCOUNT
|
||||
An example configuration:
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Bd -literal
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/path/to/root/folder"
|
||||
format = "Maildir"
|
||||
listing.index_style = "Compact"
|
||||
identity="email@example.com"
|
||||
display_name = "Name"
|
||||
send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
|
||||
# Set mailbox-specific settings
|
||||
[accounts.account-name.mailboxes]
|
||||
"INBOX" = { alias="Inbox" } #inline table
|
||||
"drafts" = { alias="Drafts" } #inline table
|
||||
[accounts.account-name.mailboxes."foobar-devel"] # or a regular table
|
||||
ignore = true # don't show notifications for this mailbox
|
||||
.Ed
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Sh MBOX ACCOUNT
|
||||
An example configuration:
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Bd -literal
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/var/mail/username"
|
||||
format = "mbox"
|
||||
listing.index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
composing.send_mail = '/bin/false'
|
||||
.Ed
|
||||
.Sh IMAP ACCOUNT
|
||||
.Bd -literal
|
||||
[accounts."account-name"]
|
||||
root_mailbox = "INBOX"
|
||||
format = "imap"
|
||||
server_hostname="mail.example.com"
|
||||
server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
|
||||
server_username="username@example.com"
|
||||
#server_port="993" # imaps
|
||||
server_port="143" # STARTTLS
|
||||
use_starttls=true #optional
|
||||
send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
display_name = "Name Name"
|
||||
identity = "username@example.com"
|
||||
## show only specific mailboxes:
|
||||
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
.Ed
|
||||
.Ss Gmail account example
|
||||
.Bd -literal
|
||||
[accounts."account-name"]
|
||||
root_mailbox = '[Gmail]'
|
||||
format = "imap"
|
||||
send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
server_hostname='imap.gmail.com'
|
||||
server_password="password"
|
||||
server_username="username@gmail.com"
|
||||
server_port="993"
|
||||
listing.index_style = "Conversations"
|
||||
identity = "username@gmail.com"
|
||||
display_name = "Name Name"
|
||||
# Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
|
||||
composing.store_sent_mail = false
|
||||
.Ed
|
||||
|
||||
.Sh JMAP ACCOUNT
|
||||
The
|
||||
.Ic server_url
|
||||
option must hold the address of the server's session endpoint.
|
||||
.Bd -literal
|
||||
[accounts."account-name"]
|
||||
root_mailbox = "INBOX"
|
||||
format = "jmap"
|
||||
send_mail = 'server_submission'
|
||||
server_url="http://localhost:8080"
|
||||
server_username="user@hostname.local"
|
||||
server_password="changeme"
|
||||
identity = "user@hostname.local"
|
||||
.Ed
|
||||
.Ss fastmail.com account example
|
||||
.Lk https://fastmail.com/ Fastmail
|
||||
uses the
|
||||
.Em Bearer token
|
||||
authentication mechanism, so the option
|
||||
.Ic use_token
|
||||
must be enabled:
|
||||
.Bd -literal
|
||||
[accounts."fastmail-jmap"]
|
||||
root_mailbox = "INBOX"
|
||||
format = "jmap"
|
||||
server_url="https://api.fastmail.com/jmap/session"
|
||||
server_username="user@fastmail.com"
|
||||
server_password="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
use_token=true
|
||||
identity = "My Name <user@fastmail.com>"
|
||||
send_mail = "server_submission"
|
||||
.Ed
|
||||
.Sh NOTMUCH ACCOUNT
|
||||
TODO
|
||||
.Sh NNTP / USENET ACCOUNT
|
||||
TODO
|
||||
.Sh SEE ALSO
|
||||
.Xr meli.conf 5 ,
|
||||
.Xr meli 1 ,
|
||||
.Xr meli-themes 5
|
||||
.Sh AUTHORS
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
.\" [pager]
|
||||
.\" filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
|
||||
.\" html_filter = "w3m -I utf-8 -T text/html"
|
||||
|
||||
.\" [notifications]
|
||||
.\" script = "notify-send"
|
||||
|
||||
.\" [composing]
|
||||
.\" # required for sending e-mail
|
||||
.\" send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
.\" #send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
.\" editor_command = 'vim +/^$'
|
||||
|
||||
.\" [shortcuts]
|
||||
.\" [shortcuts.composing]
|
||||
.\" edit = 'e'
|
||||
|
||||
.\" [shortcuts.listing]
|
||||
.\" new_mail = 'm'
|
||||
.\" set_seen = 'n'
|
||||
|
||||
.\" [terminal]
|
||||
.\" theme = "light"
|
|
@ -11,9 +11,12 @@
|
|||
#[accounts.account-name]
|
||||
#root_mailbox = "/path/to/root/mailbox"
|
||||
#format = "Maildir"
|
||||
#send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#listing.index_style = "Conversations" # or [plain, threaded, compact]
|
||||
#identity="email@example.com"
|
||||
#display_name = "Name"
|
||||
## Need to explicitly list mailboxes of interest:
|
||||
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
#
|
||||
## Set mailbox-specific settings
|
||||
|
@ -26,6 +29,7 @@
|
|||
#[accounts.mbox]
|
||||
#root_mailbox = "/var/mail/username"
|
||||
#format = "mbox"
|
||||
#send_mail = 'false'
|
||||
#listing.index_style = "Compact"
|
||||
#identity="username@hostname.local"
|
||||
#
|
||||
|
@ -33,6 +37,7 @@
|
|||
#[accounts."imap"]
|
||||
#root_mailbox = "INBOX"
|
||||
#format = "imap"
|
||||
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#server_hostname="mail.example.com"
|
||||
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
|
||||
#server_username="username@example.com"
|
||||
|
@ -42,15 +47,14 @@
|
|||
#listing.index_style = "Conversations"
|
||||
#identity = "username@example.com"
|
||||
#display_name = "Name Name"
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
### match specific mailboxes:
|
||||
### show only specific mailboxes, overriding the server's subscribed status.
|
||||
##subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
#
|
||||
## Setting up an account for an already existing notmuch database
|
||||
##[accounts.notmuch]
|
||||
##root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
|
||||
##format = "notmuch"
|
||||
##send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##listing.index_style = "conversations"
|
||||
##identity="username@example.com"
|
||||
##display_name = "Name Name"
|
||||
|
@ -64,6 +68,7 @@
|
|||
#[accounts."gmail"]
|
||||
#root_mailbox = '[Gmail]'
|
||||
#format = "imap"
|
||||
#send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#server_hostname='imap.gmail.com'
|
||||
#server_password="password"
|
||||
#server_username="username@gmail.com"
|
||||
|
@ -71,22 +76,18 @@
|
|||
#listing.index_style = "Conversations"
|
||||
#identity = "username@gmail.com"
|
||||
#display_name = "Name Name"
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
#composing.send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
|
||||
## Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
|
||||
#composing.store_sent_mail = false
|
||||
#
|
||||
##[accounts."jmap account"]
|
||||
##root_mailbox = "INBOX"
|
||||
##format = "jmap"
|
||||
##send_mail = 'server_submission'
|
||||
##server_url="http://localhost:8080"
|
||||
##server_username="user@hostname.local"
|
||||
##server_password="changeme"
|
||||
##listing.index_style = "Conversations"
|
||||
##identity = "user@hostname.local"
|
||||
##subscribed_mailboxes = ["*", ]
|
||||
##composing.send_mail = 'server_submission'
|
||||
#
|
||||
#[pager]
|
||||
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
|
||||
|
@ -128,12 +129,8 @@
|
|||
#page_down = "PageDown"
|
||||
#
|
||||
#[composing]
|
||||
##required for sending e-mail
|
||||
#send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
|
||||
#
|
||||
#
|
||||
#[pgp]
|
||||
#auto_sign = false # always sign sent messages
|
||||
#auto_verify_signatures = true # always verify signatures when reading signed e-mails
|
||||
|
|
73
meli/docs/samples/themes/ibm-modern.toml
Normal file
73
meli/docs/samples/themes/ibm-modern.toml
Normal file
|
@ -0,0 +1,73 @@
|
|||
[terminal.themes.ibm-modern]
|
||||
"theme_default" = { fg = "$Black100", bg = "$White0", attrs = "Default" }
|
||||
"status.bar" = { fg = "$Black100", bg = "$Magenta40", attrs = "theme_default" }
|
||||
"status.notification" = { fg = "$Black100", bg = "$Magenta40", attrs = "theme_default" }
|
||||
"tab.focused" = { fg = "$White0", bg = "$Purple40", attrs = "theme_default" }
|
||||
"tab.unfocused" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"widgets.list.header" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"widgets.form.highlighted" = { fg = "theme_default", bg = "$Cyan30", attrs = "theme_default" }
|
||||
"widgets.options.highlighted" = { fg = "$Cyan10", bg = "$Teal30", attrs = "theme_default" }
|
||||
"mail.sidebar" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.sidebar_account_name" = { fg = "mail.sidebar", attrs = "Bold" }
|
||||
"mail.sidebar_unread_count" = { fg = "$Magenta40", bg = "$CoolGray10" }
|
||||
"mail.sidebar_index" = { fg = "theme_default", bg = "theme_default" }
|
||||
"mail.sidebar_highlighted" = { fg = "theme_default", bg = "$CoolGray10" }
|
||||
"mail.sidebar_highlighted_unread_count" = { from = "mail.sidebar_highlighted" }
|
||||
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_name" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "Bold" }
|
||||
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_unread_count", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.listing.compact.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_unseen" = { fg = "$Black100", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_unseen" = { fg = "$Black100", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_selected" = { fg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_selected" = { fg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.date" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "Bold" }
|
||||
"mail.listing.conversations.selected" = { fg = "$CoolGray10", bg = "$CoolGray30", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "mail.listing.compact.even", bg = "mail.listing.compact.even", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "mail.listing.compact.odd", bg = "mail.listing.compact.odd", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "$Black100", bg = "$CoolGray30", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_unseen" = { fg = "$Black100", bg = "$CoolGray30", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
|
||||
"mail.view.headers" = { fg = "$Black100", bg = "$Purple40", attrs = "theme_default" }
|
||||
"mail.view.headers_names" = { fg = "$Black100", bg = "$Magenta40", attrs = "mail.view.headers" }
|
||||
"mail.view.headers_area" = { fg = "theme_default", bg = "$Purple40", attrs = "theme_default" }
|
||||
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "CornflowerBlue", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "Red1", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "Pink3", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "Gold1", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "Orange3", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "CadetBlue", attrs = "theme_default" }
|
||||
"mail.listing.attachment_flag" = { fg = "$CoolGray30", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.thread_snooze_flag" = { fg = "$Magenta40", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.tag_default" = { fg = "$White0", bg = "$Black100", attrs = "Bold" }
|
||||
"pager.highlight_search" = { fg = "theme_default", bg = "$Teal30", attrs = "Bold" }
|
||||
"pager.highlight_search_current" = { fg = "$CoolGray10", bg = "$Teal30", attrs = "Bold" }
|
||||
|
||||
[terminal.themes.ibm-modern.color_aliases]
|
||||
"Blue60" = "#0f62fe"
|
||||
"Black100" = "#000000"
|
||||
"White0" = "#ffffff"
|
||||
"Cyan30" = "#82cfff"
|
||||
"Purple40" = "#be95ff"
|
||||
"Magenta40" = "#ff7eb6"
|
||||
"Teal30" = "#3ddbd9"
|
||||
"Cyan10" = "#e5f6ff"
|
||||
"CoolGray10" = "#f2f4f8"
|
||||
"CoolGray30" = "#c1c7cd"
|
1929
meli/src/accounts.rs
1929
meli/src/accounts.rs
File diff suppressed because it is too large
Load diff
92
meli/src/accounts/backend_ops.rs
Normal file
92
meli/src/accounts/backend_ops.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* meli - accounts module.
|
||||
*
|
||||
* Copyright 2023 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Account mail backend operations.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Account {
|
||||
pub fn set_flags(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
) -> Result<JobId> {
|
||||
let fut = self.backend.write().unwrap().set_flags(
|
||||
env_hashes.clone(),
|
||||
mailbox_hash,
|
||||
flags.clone(),
|
||||
)?;
|
||||
let handle =
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.spawn("set-flags".into(), fut, self.is_async());
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
job_id,
|
||||
JobRequest::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
handle,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(job_id)
|
||||
}
|
||||
|
||||
// #[cfg(not(feature = "sqlite3"))]
|
||||
// pub(super) fn update_cached_env(&mut self, _: Envelope, _:
|
||||
// Option<EnvelopeHash>) {}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub(super) fn update_cached_env(&mut self, env: Envelope, old_hash: Option<EnvelopeHash>) {
|
||||
if self.settings.conf.search_backend == SearchBackend::Sqlite3 {
|
||||
let msg_id = env.message_id().to_string();
|
||||
let name = self.name.clone();
|
||||
let backend = self.backend.clone();
|
||||
let fut = async move {
|
||||
crate::sqlite3::AccountCache::remove(
|
||||
name.clone(),
|
||||
old_hash.unwrap_or_else(|| env.hash()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::sqlite3::AccountCache::insert(env, backend, name).await?;
|
||||
Ok(())
|
||||
};
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"sqlite3::remove".into(),
|
||||
fut,
|
||||
crate::sqlite3::AccountCache::is_async(),
|
||||
);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
|
||||
handle,
|
||||
log_level: LogLevel::TRACE,
|
||||
on_finish: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
247
meli/src/accounts/jobs.rs
Normal file
247
meli/src/accounts/jobs.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
//
|
||||
// meli - accounts module.
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, pin::Pin};
|
||||
|
||||
use futures::stream::Stream;
|
||||
use melib::{backends::*, email::*, error::Result, LogLevel};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{is_variant, jobs::JoinHandle, StatusEvent};
|
||||
|
||||
pub enum MailboxJobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
RenameMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_path: String,
|
||||
handle: JoinHandle<Result<Mailbox>>,
|
||||
},
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MailboxJobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
Self::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
Self::RenameMailbox {
|
||||
mailbox_hash,
|
||||
new_path,
|
||||
..
|
||||
} => {
|
||||
write!(f, "JobRequest::RenameMailbox {mailbox_hash} to {new_path} ")
|
||||
}
|
||||
Self::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
Self::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailboxJobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
Self::RenameMailbox { new_path, .. } => write!(f, "Rename mailbox to {new_path}"),
|
||||
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxJobRequest {
|
||||
pub fn cancel(&self) -> Option<StatusEvent> {
|
||||
match self {
|
||||
Self::Mailboxes { handle } => handle.cancel(),
|
||||
Self::CreateMailbox { handle, .. } => handle.cancel(),
|
||||
Self::DeleteMailbox { handle, .. } => handle.cancel(),
|
||||
Self::RenameMailbox { handle, .. } => handle.cancel(),
|
||||
Self::SetMailboxPermissions { handle, .. } => handle.cancel(),
|
||||
Self::SetMailboxSubscription { handle, .. } => handle.cancel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum JobRequest {
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handle: JoinHandle<(
|
||||
Option<Result<Vec<Envelope>>>,
|
||||
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
|
||||
)>,
|
||||
},
|
||||
Generic {
|
||||
name: Cow<'static, str>,
|
||||
log_level: LogLevel,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
on_finish: Option<crate::types::CallbackFn>,
|
||||
},
|
||||
IsOnline {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Refresh {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetFlags {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SaveMessage {
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SendMessage,
|
||||
SendMessageBackground {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
DeleteMessages {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Watch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Mailbox(MailboxJobRequest),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
|
||||
Self::Mailbox(inner) => std::fmt::Debug::fmt(inner, f),
|
||||
Self::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
Self::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
|
||||
Self::Refresh { .. } => write!(f, "JobRequest::Refresh"),
|
||||
Self::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
..
|
||||
} => f
|
||||
.debug_struct(stringify!(JobRequest::SetFlags))
|
||||
.field("env_hashes", &env_hashes)
|
||||
.field("mailbox_hash", &mailbox_hash)
|
||||
.field("flags", &flags)
|
||||
.finish(),
|
||||
Self::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
|
||||
Self::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
|
||||
Self::Watch { .. } => write!(f, "JobRequest::Watch"),
|
||||
Self::SendMessage => write!(f, "JobRequest::SendMessage"),
|
||||
Self::SendMessageBackground { .. } => {
|
||||
write!(f, "JobRequest::SendMessageBackground")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "{}", name),
|
||||
Self::Mailbox(inner) => std::fmt::Display::fmt(inner, f),
|
||||
Self::Fetch { .. } => write!(f, "Mailbox fetch"),
|
||||
Self::IsOnline { .. } => write!(f, "Online status check"),
|
||||
Self::Refresh { .. } => write!(f, "Refresh mailbox"),
|
||||
Self::SetFlags {
|
||||
env_hashes, flags, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Set flags for {} message{}: {:?}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" },
|
||||
flags
|
||||
),
|
||||
Self::SaveMessage { .. } => write!(f, "Save message"),
|
||||
Self::DeleteMessages { env_hashes, .. } => write!(
|
||||
f,
|
||||
"Delete {} message{}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
Self::Watch { .. } => write!(f, "Background watch"),
|
||||
Self::SendMessageBackground { .. } | Self::SendMessage => {
|
||||
write!(f, "Sending message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRequest {
|
||||
is_variant! { is_watch, Watch { .. } }
|
||||
is_variant! { is_online, IsOnline { .. } }
|
||||
is_variant! { is_any_fetch, Fetch { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, Self::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
|
||||
pub fn cancel(&self) -> Option<StatusEvent> {
|
||||
match self {
|
||||
Self::Generic { handle, .. } => handle.cancel(),
|
||||
Self::Mailbox(inner) => inner.cancel(),
|
||||
Self::Fetch { handle, .. } => handle.cancel(),
|
||||
Self::IsOnline { handle, .. } => handle.cancel(),
|
||||
Self::Refresh { handle, .. } => handle.cancel(),
|
||||
Self::SetFlags { handle, .. } => handle.cancel(),
|
||||
Self::SaveMessage { handle, .. } => handle.cancel(),
|
||||
Self::DeleteMessages { handle, .. } => handle.cancel(),
|
||||
Self::Watch { handle, .. } => handle.cancel(),
|
||||
Self::SendMessage => None,
|
||||
Self::SendMessageBackground { handle, .. } => handle.cancel(),
|
||||
}
|
||||
}
|
||||
}
|
261
meli/src/accounts/mailbox.rs
Normal file
261
meli/src/accounts/mailbox.rs
Normal file
|
@ -0,0 +1,261 @@
|
|||
//
|
||||
// meli - accounts module.
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::{Mailbox, MailboxHash},
|
||||
error::Error,
|
||||
log,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{conf::FileMailboxConf, is_variant};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum MailboxStatus {
|
||||
Available,
|
||||
Failed(Error),
|
||||
/// first argument is done work, and second is total work
|
||||
Parsing(usize, usize),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl MailboxStatus {
|
||||
is_variant! { is_available, Available }
|
||||
is_variant! { is_parsing, Parsing(_, _) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MailboxEntry {
|
||||
pub status: MailboxStatus,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub ref_mailbox: Mailbox,
|
||||
pub conf: FileMailboxConf,
|
||||
}
|
||||
|
||||
impl MailboxEntry {
|
||||
pub fn new(
|
||||
status: MailboxStatus,
|
||||
name: String,
|
||||
ref_mailbox: Mailbox,
|
||||
conf: FileMailboxConf,
|
||||
) -> Self {
|
||||
let mut ret = Self {
|
||||
status,
|
||||
name,
|
||||
path: ref_mailbox.path().into(),
|
||||
ref_mailbox,
|
||||
conf,
|
||||
};
|
||||
match ret.conf.mailbox_conf.extra.get("encoding") {
|
||||
None => {}
|
||||
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
|
||||
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
|
||||
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
|
||||
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
|
||||
}
|
||||
Some(other) => {
|
||||
log::warn!(
|
||||
"mailbox `{}`: unrecognized mailbox name charset: {}",
|
||||
&ret.name,
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
match self.status {
|
||||
MailboxStatus::Available => format!(
|
||||
"{} [{} messages]",
|
||||
self.name(),
|
||||
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
|
||||
),
|
||||
MailboxStatus::Failed(ref e) => e.to_string(),
|
||||
MailboxStatus::None => "Retrieving mailbox.".to_string(),
|
||||
MailboxStatus::Parsing(done, total) => {
|
||||
format!("Parsing messages. [{}/{}]", done, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
|
||||
name
|
||||
} else {
|
||||
self.ref_mailbox.name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MailboxNode {
|
||||
pub hash: MailboxHash,
|
||||
pub depth: usize,
|
||||
pub indentation: u32,
|
||||
pub has_sibling: bool,
|
||||
pub children: Vec<MailboxNode>,
|
||||
}
|
||||
|
||||
pub fn build_mailboxes_order(
|
||||
tree: &mut Vec<MailboxNode>,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mailboxes_order: &mut Vec<MailboxHash>,
|
||||
) {
|
||||
tree.clear();
|
||||
mailboxes_order.clear();
|
||||
for (h, f) in mailbox_entries.iter() {
|
||||
if f.ref_mailbox.parent().is_none() {
|
||||
fn rec(
|
||||
h: MailboxHash,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
depth: usize,
|
||||
) -> MailboxNode {
|
||||
let mut node = MailboxNode {
|
||||
hash: h,
|
||||
children: Vec::new(),
|
||||
depth,
|
||||
indentation: 0,
|
||||
has_sibling: false,
|
||||
};
|
||||
for &c in mailbox_entries[&h].ref_mailbox.children() {
|
||||
if mailbox_entries.contains_key(&c) {
|
||||
node.children.push(rec(c, mailbox_entries, depth + 1));
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
tree.push(rec(*h, mailbox_entries, 0));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mailbox_eq_key {
|
||||
($mailbox:expr) => {{
|
||||
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
|
||||
(0, sort_order, $mailbox.ref_mailbox.path())
|
||||
} else {
|
||||
(1, 0, $mailbox.ref_mailbox.path())
|
||||
}
|
||||
}};
|
||||
}
|
||||
tree.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
|
||||
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
|
||||
for n in tree.iter_mut() {
|
||||
mailboxes_order.push(n.hash);
|
||||
n.children.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
stack.extend(n.children.iter().rev().map(Some));
|
||||
while let Some(Some(next)) = stack.pop() {
|
||||
mailboxes_order.push(next.hash);
|
||||
stack.extend(next.children.iter().rev().map(Some));
|
||||
}
|
||||
}
|
||||
drop(stack);
|
||||
for node in tree.iter_mut() {
|
||||
fn rec(
|
||||
node: &mut MailboxNode,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mut indentation: u32,
|
||||
has_sibling: bool,
|
||||
) {
|
||||
node.indentation = indentation;
|
||||
node.has_sibling = has_sibling;
|
||||
let mut iter = (0..node.children.len())
|
||||
.filter(|i| {
|
||||
mailbox_entries[&node.children[*i].hash]
|
||||
.ref_mailbox
|
||||
.is_subscribed()
|
||||
})
|
||||
.collect::<SmallVec<[_; 8]>>()
|
||||
.into_iter()
|
||||
.peekable();
|
||||
indentation <<= 1;
|
||||
if has_sibling {
|
||||
indentation |= 1;
|
||||
}
|
||||
while let Some(i) = iter.next() {
|
||||
let c = &mut node.children[i];
|
||||
rec(c, mailbox_entries, indentation, iter.peek().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
rec(node, mailbox_entries, 0, false);
|
||||
}
|
||||
}
|
439
meli/src/accounts/mailbox_ops.rs
Normal file
439
meli/src/accounts/mailbox_ops.rs
Normal file
|
@ -0,0 +1,439 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use melib::conf::ToggleFlag;
|
||||
|
||||
use super::*;
|
||||
use crate::command::actions::MailboxOperation;
|
||||
|
||||
impl Account {
|
||||
pub fn mailbox_operation(&mut self, op: MailboxOperation) -> Result<JobId> {
|
||||
if self.settings.account.read_only {
|
||||
return Err(Error::new("Account is read-only."));
|
||||
}
|
||||
match op {
|
||||
MailboxOperation::Create(path) => {
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.create_mailbox(path.to_string())?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"create_mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::CreateMailbox { path, handle }),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Delete(path) => {
|
||||
if self.mailbox_entries.len() == 1 {
|
||||
return Err(Error::new("Cannot delete only mailbox."));
|
||||
}
|
||||
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"delete-mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::DeleteMailbox {
|
||||
mailbox_hash,
|
||||
handle,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Subscribe(path) => {
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_mailbox_subscription(mailbox_hash, true)?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"subscribe-mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
|
||||
mailbox_hash,
|
||||
new_value: true,
|
||||
handle,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Unsubscribe(path) => {
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_mailbox_subscription(mailbox_hash, false)?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"unsubscribe-mailbox".into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
|
||||
mailbox_hash,
|
||||
new_value: false,
|
||||
handle,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::Rename(path, new_path) => {
|
||||
let mailbox_hash = self.mailbox_by_path(&path)?;
|
||||
let job = self
|
||||
.backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.rename_mailbox(mailbox_hash, new_path.clone())?;
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
format!("rename-mailbox {path} to {new_path}").into(),
|
||||
job,
|
||||
self.is_async(),
|
||||
);
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(
|
||||
job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::RenameMailbox {
|
||||
handle,
|
||||
mailbox_hash,
|
||||
new_path,
|
||||
}),
|
||||
);
|
||||
Ok(job_id)
|
||||
}
|
||||
MailboxOperation::SetPermissions(_) => Err(Error::new("Not implemented.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_mailbox_event(&mut self, job_id: JobId, mut job: MailboxJobRequest) {
|
||||
macro_rules! try_handle {
|
||||
($handle:ident, $binding:pat => $then:block) => {{
|
||||
try_handle! { $handle, Err(err) => {
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: None,
|
||||
body: format!("{}: {} failed", &self.name, job).into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
source: Some(err),
|
||||
}));
|
||||
return;
|
||||
},
|
||||
$binding => $then
|
||||
}
|
||||
}};
|
||||
($handle:ident, Err($err:pat) => $then_err: block, $binding:pat => $then:block) => {{
|
||||
match $handle.chan.try_recv() {
|
||||
_err @ Ok(None) | _err @ Err(_) => {
|
||||
/* canceled */
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!(
|
||||
"handle.chan.try_recv() for job {} returned {:?}",
|
||||
job_id,
|
||||
_err
|
||||
);
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
}
|
||||
Ok(Some(Err($err))) => $then_err,
|
||||
Ok(Some(Ok($binding))) => $then,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
match job {
|
||||
MailboxJobRequest::Mailboxes { ref mut handle } => {
|
||||
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
|
||||
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
|
||||
if !err.is_recoverable() {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::Notification {
|
||||
title: Some(self.name.to_string().into()),
|
||||
source: Some(err.clone()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
},
|
||||
));
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(
|
||||
self.hash,
|
||||
Some(err.to_string().into()),
|
||||
),
|
||||
));
|
||||
self.is_online.set_err(err);
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
return;
|
||||
}
|
||||
let mailboxes_job = self.backend.read().unwrap().mailboxes();
|
||||
if let Ok(mailboxes_job) = mailboxes_job {
|
||||
let handle = self.main_loop_handler.job_executor.spawn(
|
||||
"list-mailboxes".into(),
|
||||
mailboxes_job,
|
||||
self.is_async(),
|
||||
);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Mailbox(MailboxJobRequest::Mailboxes { handle }),
|
||||
);
|
||||
};
|
||||
} else {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(
|
||||
self.hash,
|
||||
Some("Loaded mailboxes.".into()),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
MailboxJobRequest::CreateMailbox { ref mut handle, .. } => {
|
||||
try_handle! { handle, (mailbox_hash, mut mailboxes) => {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::MailboxCreate((self.hash, mailbox_hash)),
|
||||
));
|
||||
let mut new = FileMailboxConf::default();
|
||||
new.mailbox_conf.subscribe = ToggleFlag::InternalVal(true);
|
||||
new.mailbox_conf.usage = if mailboxes[&mailbox_hash].special_usage()
|
||||
!= SpecialUsageMailbox::Normal
|
||||
{
|
||||
Some(mailboxes[&mailbox_hash].special_usage())
|
||||
} else {
|
||||
let tmp = SpecialUsageMailbox::detect_usage(
|
||||
mailboxes[&mailbox_hash].name(),
|
||||
);
|
||||
if let Some(tmp) = tmp.filter(|&v| v != SpecialUsageMailbox::Normal)
|
||||
{
|
||||
mailboxes.entry(mailbox_hash).and_modify(|entry| {
|
||||
let _ = entry.set_special_usage(tmp);
|
||||
});
|
||||
}
|
||||
tmp
|
||||
};
|
||||
// if new mailbox has parent, we need to update its children field
|
||||
if let Some(parent_hash) = mailboxes[&mailbox_hash].parent() {
|
||||
self.mailbox_entries
|
||||
.entry(parent_hash)
|
||||
.and_modify(|parent| {
|
||||
parent.ref_mailbox =
|
||||
mailboxes.remove(&parent_hash).unwrap();
|
||||
});
|
||||
}
|
||||
let status = MailboxStatus::default();
|
||||
|
||||
self.mailbox_entries.insert(
|
||||
mailbox_hash,
|
||||
MailboxEntry::new(
|
||||
status,
|
||||
mailboxes[&mailbox_hash].path().to_string(),
|
||||
mailboxes.remove(&mailbox_hash).unwrap(),
|
||||
new,
|
||||
),
|
||||
);
|
||||
self.collection
|
||||
.threads
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, Threads::default());
|
||||
self.collection
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, Default::default());
|
||||
build_mailboxes_order(
|
||||
&mut self.tree,
|
||||
&self.mailbox_entries,
|
||||
&mut self.mailboxes_order,
|
||||
);
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::DeleteMailbox {
|
||||
mailbox_hash,
|
||||
ref mut handle,
|
||||
..
|
||||
} => {
|
||||
try_handle! { handle, mut mailboxes => {
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::MailboxDelete((
|
||||
self.hash,
|
||||
mailbox_hash,
|
||||
))));
|
||||
if let Some(pos) =
|
||||
self.mailboxes_order.iter().position(|&h| h == mailbox_hash)
|
||||
{
|
||||
self.mailboxes_order.remove(pos);
|
||||
}
|
||||
if let Some(pos) = self.tree.iter().position(|n| n.hash == mailbox_hash) {
|
||||
self.tree.remove(pos);
|
||||
}
|
||||
if self.settings.sent_mailbox == Some(mailbox_hash) {
|
||||
self.settings.sent_mailbox = None;
|
||||
}
|
||||
self.collection
|
||||
.threads
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&mailbox_hash);
|
||||
let deleted_mailbox =
|
||||
self.mailbox_entries.shift_remove(&mailbox_hash).unwrap();
|
||||
// if deleted mailbox had parent, we need to update its children field
|
||||
if let Some(parent_hash) = deleted_mailbox.ref_mailbox.parent() {
|
||||
self.mailbox_entries
|
||||
.entry(parent_hash)
|
||||
.and_modify(|parent| {
|
||||
parent.ref_mailbox = mailboxes.remove(&parent_hash).unwrap();
|
||||
});
|
||||
}
|
||||
self.collection
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&mailbox_hash);
|
||||
build_mailboxes_order(
|
||||
&mut self.tree,
|
||||
&self.mailbox_entries,
|
||||
&mut self.mailboxes_order,
|
||||
);
|
||||
// [ref:FIXME] remove from settings as well
|
||||
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("{}: mailbox deleted successfully", &self.name).into(),
|
||||
),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
}));
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::RenameMailbox {
|
||||
ref mut handle,
|
||||
mailbox_hash,
|
||||
ref mut new_path,
|
||||
} => {
|
||||
use indexmap::map::MutableKeys;
|
||||
try_handle! { handle, mailbox => {
|
||||
let new_hash = mailbox.hash();
|
||||
if let Some((_, key, entry)) = self.mailbox_entries.get_full_mut2(&mailbox_hash) {
|
||||
*key = new_hash;
|
||||
*entry = MailboxEntry::new(entry.status.clone(), std::mem::take(new_path), mailbox, entry.conf.clone());
|
||||
}
|
||||
if let Some(key) = self.mailboxes_order.iter_mut().find(|k| **k == mailbox_hash) {
|
||||
*key = new_hash;
|
||||
}
|
||||
if let Some((_, key, _)) = self.event_queue.get_full_mut2(&mailbox_hash) {
|
||||
*key = new_hash;
|
||||
}
|
||||
{
|
||||
let mut threads = self.collection.threads.write().unwrap();
|
||||
if let Some(entry) = threads.remove(&mailbox_hash) {
|
||||
threads.insert(new_hash, entry);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut mailboxes = self.collection.mailboxes.write().unwrap();
|
||||
if let Some(entry) = mailboxes.remove(&mailbox_hash) {
|
||||
mailboxes.insert(new_hash, entry);
|
||||
}
|
||||
}
|
||||
build_mailboxes_order(
|
||||
&mut self.tree,
|
||||
&self.mailbox_entries,
|
||||
&mut self.mailboxes_order,
|
||||
);
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::SetMailboxPermissions { ref mut handle, .. } => {
|
||||
try_handle! { handle, _ => {
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("{}: mailbox permissions set successfully", &self.name)
|
||||
.into(),
|
||||
),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
}));
|
||||
}}
|
||||
}
|
||||
MailboxJobRequest::SetMailboxSubscription {
|
||||
ref mut handle,
|
||||
ref mailbox_hash,
|
||||
ref new_value,
|
||||
} => {
|
||||
try_handle! { handle, () => {
|
||||
if self.mailbox_entries.contains_key(mailbox_hash) {
|
||||
self.mailbox_entries.entry(*mailbox_hash).and_modify(|m| {
|
||||
m.conf.mailbox_conf.subscribe = if *new_value {
|
||||
ToggleFlag::True
|
||||
} else {
|
||||
ToggleFlag::False
|
||||
};
|
||||
let _ = m.ref_mailbox.set_is_subscribed(*new_value);
|
||||
});
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!(
|
||||
"{}: `{}` has been {}subscribed.",
|
||||
&self.name,
|
||||
self.mailbox_entries[mailbox_hash].name(),
|
||||
if *new_value { "" } else { "un" }
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
source: None,
|
||||
body: "".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
}));
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
565
meli/src/accounts/tests.rs
Normal file
565
meli/src/accounts/tests.rs
Normal file
|
@ -0,0 +1,565 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use melib::{
|
||||
backends::{prelude::*, Mailbox, MailboxHash},
|
||||
error::Result,
|
||||
maildir::MaildirType,
|
||||
smol, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{
|
||||
accounts::{AccountConf, FileMailboxConf, MailboxEntry, MailboxStatus},
|
||||
command::actions::MailboxOperation,
|
||||
utilities::tests::{eprint_step_fn, eprintln_ok_fn},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_mailbox_utf7() {
|
||||
#[derive(Debug)]
|
||||
struct TestMailbox(String);
|
||||
|
||||
impl melib::BackendMailbox for TestMailbox {
|
||||
fn hash(&self) -> MailboxHash {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<MailboxHash> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn permissions(&self) -> MailboxPermissions {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_subscribed(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
for (n, d) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
|
||||
] {
|
||||
let ref_mbox = TestMailbox(n.to_string());
|
||||
let mut conf: melib::MailboxConf = Default::default();
|
||||
conf.extra.insert("encoding".to_string(), "utf7".into());
|
||||
|
||||
let entry = MailboxEntry::new(
|
||||
MailboxStatus::None,
|
||||
n.to_string(),
|
||||
Box::new(ref_mbox),
|
||||
FileMailboxConf {
|
||||
mailbox_conf: conf,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(&entry.path, d);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_maildir_backend(
|
||||
temp_dir: &TempDir,
|
||||
acc_name: &str,
|
||||
event_consumer: BackendEventConsumer,
|
||||
with_root_mailbox: bool,
|
||||
) -> Result<(PathBuf, AccountConf, Box<MaildirType>)> {
|
||||
let root_mailbox = temp_dir.path().join("inbox");
|
||||
{
|
||||
std::fs::create_dir(&root_mailbox).expect("Could not create root mailbox directory.");
|
||||
if with_root_mailbox {
|
||||
for d in &["cur", "new", "tmp"] {
|
||||
std::fs::create_dir(root_mailbox.join(d))
|
||||
.expect("Could not create root mailbox directory contents.");
|
||||
}
|
||||
}
|
||||
}
|
||||
let subscribed_mailboxes = if with_root_mailbox {
|
||||
vec!["inbox".into()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let mailboxes = if with_root_mailbox {
|
||||
vec![(
|
||||
"inbox".into(),
|
||||
melib::conf::MailboxConf {
|
||||
extra: indexmap::indexmap! {
|
||||
"path".into() => root_mailbox.display().to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
indexmap::indexmap! {}
|
||||
};
|
||||
let extra = if with_root_mailbox {
|
||||
indexmap::indexmap! {
|
||||
"root_mailbox".into() => root_mailbox.display().to_string(),
|
||||
}
|
||||
} else {
|
||||
indexmap::indexmap! {}
|
||||
};
|
||||
|
||||
let account_conf = melib::AccountSettings {
|
||||
name: acc_name.to_string(),
|
||||
root_mailbox: root_mailbox.display().to_string(),
|
||||
format: "maildir".to_string(),
|
||||
identity: "user@localhost".to_string(),
|
||||
extra_identities: vec![],
|
||||
read_only: false,
|
||||
display_name: None,
|
||||
order: Default::default(),
|
||||
subscribed_mailboxes,
|
||||
mailboxes,
|
||||
manual_refresh: true,
|
||||
extra,
|
||||
};
|
||||
|
||||
let maildir = MaildirType::new(&account_conf, Default::default(), event_consumer)?;
|
||||
Ok((root_mailbox, account_conf.into(), maildir))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_accounts_mailbox_by_path_error_msg() {
|
||||
const ACCOUNT_NAME: &str = "test";
|
||||
|
||||
let eprintln_ok = eprintln_ok_fn();
|
||||
let mut eprint_step_closure = eprint_step_fn();
|
||||
macro_rules! eprint_step {
|
||||
($($arg:tt)+) => {{
|
||||
eprint_step_closure(format_args!($($arg)+));
|
||||
}};
|
||||
}
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
{
|
||||
eprint_step!(
|
||||
"Create maildir backend with a root mailbox, \"inbox\" which will be a valid maildir \
|
||||
folder because it will contain cur, new, tmp subdirectories..."
|
||||
);
|
||||
let mut ctx = crate::Context::new_mock(&temp_dir);
|
||||
let backend_event_queue = Arc::new(std::sync::Mutex::new(
|
||||
std::collections::VecDeque::with_capacity(16),
|
||||
));
|
||||
|
||||
let backend_event_consumer = {
|
||||
let backend_event_queue = Arc::clone(&backend_event_queue);
|
||||
|
||||
BackendEventConsumer::new(Arc::new(move |ah, be| {
|
||||
backend_event_queue.lock().unwrap().push_back((ah, be));
|
||||
}))
|
||||
};
|
||||
|
||||
let (root_mailbox, settings, maildir) =
|
||||
new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, true).unwrap();
|
||||
eprintln_ok();
|
||||
let name = maildir.account_name.to_string();
|
||||
let account_hash = maildir.account_hash;
|
||||
let backend = maildir as Box<dyn MailBackend>;
|
||||
let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
|
||||
let contacts = melib::contacts::Contacts::new(name.to_string());
|
||||
|
||||
let mut account = super::Account {
|
||||
hash: account_hash,
|
||||
name: name.into(),
|
||||
is_online: super::IsOnline::True,
|
||||
mailbox_entries: Default::default(),
|
||||
mailboxes_order: Default::default(),
|
||||
tree: Default::default(),
|
||||
contacts,
|
||||
collection: backend.collection(),
|
||||
settings,
|
||||
main_loop_handler: ctx.main_loop_handler.clone(),
|
||||
active_jobs: HashMap::default(),
|
||||
active_job_instants: std::collections::BTreeMap::default(),
|
||||
event_queue: IndexMap::default(),
|
||||
backend_capabilities: backend.capabilities(),
|
||||
backend: Arc::new(std::sync::RwLock::new(backend)),
|
||||
};
|
||||
account.init(ref_mailboxes).unwrap();
|
||||
while let Ok(thread_event) = ctx.receiver.try_recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprint_step!("Assert that mailbox_by_path(\"inbox\") returns the root mailbox...");
|
||||
account.mailbox_by_path("inbox").unwrap();
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"box\") returns an error mentioning the root mailbox..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("box").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"inbox\"]. You can inspect the list \
|
||||
of mailbox paths of an account with the manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
|
||||
macro_rules! wait_for_job {
|
||||
($job_id:expr) => {{
|
||||
let wait_for = $job_id;
|
||||
while let Ok(thread_event) = ctx.receiver.recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
} else if job_id == wait_for {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
eprint_step!(
|
||||
"Create new mailboxes: \"Sent\", \"Trash\", \"Drafts\", \"Archive\", \"Outbox\", \
|
||||
\"Archive/Archive (old)\"..."
|
||||
);
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Sent".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Trash".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Drafts".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Archive".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create("Outbox".to_string()))
|
||||
.unwrap());
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create(
|
||||
"inbox/Archive/Archive (old)".to_string(),
|
||||
))
|
||||
.unwrap());
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"rchive\") returns an error and mentions matching \
|
||||
archives with mailboxes with the least depth in the tree hierarchy of mailboxes \
|
||||
mentioned first..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("rchive").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"inbox/Archive\", \
|
||||
\"inbox/Archive/Archive (old)\"]. You can inspect the list of mailbox paths \
|
||||
of an account with the manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!("Create \"inbox/Archive/Archive{{1,2,3,4,5,6,7,8,9,10}}\" mailboxes...");
|
||||
for i in 1..=10 {
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create(format!(
|
||||
"inbox/Archive/Archive{i}"
|
||||
)))
|
||||
.unwrap());
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox/Archive/Archive{{n}}\") works, i.e. we have to \
|
||||
specify the root prefix \"inbox\"..."
|
||||
);
|
||||
for i in 1..=10 {
|
||||
account
|
||||
.mailbox_by_path(&format!("inbox/Archive/Archive{i}"))
|
||||
.unwrap();
|
||||
account
|
||||
.mailbox_by_path(&format!("Archive/Archive{i}"))
|
||||
.unwrap_err();
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"rchive\") returns and error and truncates the matching \
|
||||
mailbox paths to 5 maximum..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("rchive").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"inbox/Archive\", \
|
||||
\"inbox/Archive/Archive1\", \"inbox/Archive/Archive2\", \
|
||||
\"inbox/Archive/Archive3\", \"inbox/Archive/Archive4\"] and 7 others. You \
|
||||
can inspect the list of mailbox paths of an account with the \
|
||||
manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox/Archive\") returns a valid result (since the \
|
||||
root mailbox is a valid maildir folder)..."
|
||||
);
|
||||
account.mailbox_by_path("inbox/Archive").unwrap();
|
||||
eprintln_ok();
|
||||
|
||||
eprint_step!("Cleanup maildir account with valid root mailbox...");
|
||||
std::fs::remove_dir_all(root_mailbox).unwrap();
|
||||
eprintln_ok();
|
||||
}
|
||||
|
||||
{
|
||||
eprint_step!(
|
||||
"Create maildir backend with a root mailbox, \"inbox\" which will NOT be a valid \
|
||||
maildir folder because it will NOT contain cur, new, tmp subdirectories..."
|
||||
);
|
||||
let mut ctx = crate::Context::new_mock(&temp_dir);
|
||||
let backend_event_queue = Arc::new(std::sync::Mutex::new(
|
||||
std::collections::VecDeque::with_capacity(16),
|
||||
));
|
||||
|
||||
let backend_event_consumer = {
|
||||
let backend_event_queue = Arc::clone(&backend_event_queue);
|
||||
|
||||
BackendEventConsumer::new(Arc::new(move |ah, be| {
|
||||
backend_event_queue.lock().unwrap().push_back((ah, be));
|
||||
}))
|
||||
};
|
||||
|
||||
let (_root_mailbox, settings, maildir) =
|
||||
new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, false).unwrap();
|
||||
eprintln_ok();
|
||||
let name = maildir.account_name.to_string();
|
||||
let account_hash = maildir.account_hash;
|
||||
let backend = maildir as Box<dyn MailBackend>;
|
||||
let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
|
||||
eprint_step!("Assert that created account has no mailboxes at all...");
|
||||
assert!(
|
||||
ref_mailboxes.is_empty(),
|
||||
"ref_mailboxes were not empty: {:?}",
|
||||
ref_mailboxes
|
||||
);
|
||||
eprintln_ok();
|
||||
let contacts = melib::contacts::Contacts::new(name.to_string());
|
||||
|
||||
let mut account = super::Account {
|
||||
hash: account_hash,
|
||||
name: name.into(),
|
||||
is_online: super::IsOnline::True,
|
||||
mailbox_entries: Default::default(),
|
||||
mailboxes_order: Default::default(),
|
||||
tree: Default::default(),
|
||||
contacts,
|
||||
collection: backend.collection(),
|
||||
settings,
|
||||
main_loop_handler: ctx.main_loop_handler.clone(),
|
||||
active_jobs: HashMap::default(),
|
||||
active_job_instants: std::collections::BTreeMap::default(),
|
||||
event_queue: IndexMap::default(),
|
||||
backend_capabilities: backend.capabilities(),
|
||||
backend: Arc::new(std::sync::RwLock::new(backend)),
|
||||
};
|
||||
account.init(ref_mailboxes).unwrap();
|
||||
while let Ok(thread_event) = ctx.receiver.try_recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox\") does not return a valid result (there are no \
|
||||
mailboxes)..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("inbox").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"You can inspect the list of mailbox paths of an account with the \
|
||||
manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Create multiple maildir folders \"inbox/Archive{{1,2,3,4,5,6,7,8,9,10}}\"..."
|
||||
);
|
||||
macro_rules! wait_for_job {
|
||||
($job_id:expr) => {{
|
||||
let wait_for = $job_id;
|
||||
while let Ok(thread_event) = ctx.receiver.recv() {
|
||||
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
|
||||
if !account.process_event(&job_id) {
|
||||
assert!(
|
||||
ctx.accounts[0].process_event(&job_id),
|
||||
"unclaimed job id: {:?}",
|
||||
job_id
|
||||
);
|
||||
} else if job_id == wait_for {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
for i in 1..=10 {
|
||||
wait_for_job!(account
|
||||
.mailbox_operation(MailboxOperation::Create(format!("inbox/Archive{i}")))
|
||||
.unwrap());
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"Archive{{n}}\") works, and that we don't have to \
|
||||
specify the root prefix \"inbox\"..."
|
||||
);
|
||||
for i in 1..=10 {
|
||||
account.mailbox_by_path(&format!("Archive{i}")).unwrap();
|
||||
}
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"rchive\") returns an error message with matches..."
|
||||
);
|
||||
assert_eq!(
|
||||
account.mailbox_by_path("rchive").unwrap_err().to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"Some matching paths that were found: [\"Archive1\", \"Archive2\", \
|
||||
\"Archive3\", \"Archive4\", \"Archive5\"] and 5 others. You can inspect the \
|
||||
list of mailbox paths of an account with the manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
eprint_step!(
|
||||
"Assert that mailbox_by_path(\"inbox/Archive{{n}}\") does not return a valid result..."
|
||||
);
|
||||
assert_eq!(
|
||||
account
|
||||
.mailbox_by_path("inbox/Archive1")
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
Error {
|
||||
summary: "Mailbox with that path not found.".into(),
|
||||
details: Some(
|
||||
"You can inspect the list of mailbox paths of an account with the \
|
||||
manage-mailboxes command."
|
||||
.into()
|
||||
),
|
||||
source: None,
|
||||
inner: None,
|
||||
related_path: None,
|
||||
kind: ErrorKind::NotFound
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
eprintln_ok();
|
||||
}
|
||||
}
|
322
meli/src/args.rs
322
meli/src/args.rs
|
@ -21,7 +21,28 @@
|
|||
|
||||
//! Command line arguments.
|
||||
|
||||
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "cli-docs")]
|
||||
use crate::manpages;
|
||||
|
||||
fn try_path_or_stdio(input: &OsStr) -> PathOrStdio {
|
||||
if input.as_bytes() == b"-" {
|
||||
PathOrStdio::Stdio
|
||||
} else {
|
||||
PathOrStdio::Path(PathBuf::from(input))
|
||||
}
|
||||
}
|
||||
|
||||
/// `Pathbuf` or standard stream (`-` operand).
|
||||
#[derive(Debug)]
|
||||
pub enum PathOrStdio {
|
||||
/// Path
|
||||
Path(PathBuf),
|
||||
/// standard stream (`-` operand)
|
||||
Stdio,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
|
||||
|
@ -40,39 +61,50 @@ pub enum SubCommand {
|
|||
PrintDefaultTheme,
|
||||
/// print loaded themes in full to stdout and exit.
|
||||
PrintLoadedThemes,
|
||||
/// print all paths that meli creates/uses.
|
||||
PrintUsedPaths,
|
||||
/// print all directories that meli creates/uses.
|
||||
PrintAppDirectories,
|
||||
/// print location of configuration file that will be loaded on normal app
|
||||
/// startup.
|
||||
PrintConfigPath,
|
||||
/// edit configuration files with `$EDITOR`/`$VISUAL`.
|
||||
EditConfig,
|
||||
/// create a sample configuration file with available configuration options.
|
||||
/// If PATH is not specified, meli will try to create it in
|
||||
/// $XDG_CONFIG_HOME/meli/config.toml
|
||||
/// If `PATH` is not specified, meli will try to create it in
|
||||
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will output to standard
|
||||
/// output instead.
|
||||
#[structopt(display_order = 1)]
|
||||
CreateConfig {
|
||||
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
|
||||
path: Option<PathBuf>,
|
||||
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
|
||||
path: Option<PathOrStdio>,
|
||||
},
|
||||
/// test a configuration file for syntax issues or missing options.
|
||||
/// If `PATH` is not specified, meli will try to read it from
|
||||
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will read input from
|
||||
/// standard input instead.
|
||||
#[structopt(display_order = 2)]
|
||||
TestConfig {
|
||||
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
|
||||
path: Option<PathBuf>,
|
||||
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
|
||||
path: Option<PathOrStdio>,
|
||||
},
|
||||
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
|
||||
#[structopt(display_order = 3)]
|
||||
/// Testing tools such as IMAP, SMTP shells for debugging.
|
||||
Tools(ToolOpt),
|
||||
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
|
||||
#[structopt(display_order = 4)]
|
||||
/// print documentation page and exit (Piping to a pager is recommended.).
|
||||
Man(ManOpt),
|
||||
|
||||
#[structopt(display_order = 4)]
|
||||
/// Install manual pages to the first location provided by $MANPATH /
|
||||
/// manpath(1), unless you specify the directory as an argument.
|
||||
#[structopt(display_order = 5)]
|
||||
/// Install manual pages to the first location provided by `$MANPATH` /
|
||||
/// `manpath(1)`, unless you specify the directory as an argument.
|
||||
InstallMan {
|
||||
#[structopt(value_name = "DESTINATION_PATH", parse(from_os_str))]
|
||||
destination_path: Option<PathBuf>,
|
||||
},
|
||||
#[structopt(display_order = 5)]
|
||||
/// print compile time feature flags of this binary
|
||||
#[structopt(display_order = 6)]
|
||||
/// Print compile time feature flags of this binary
|
||||
CompiledWith,
|
||||
/// Print log file location.
|
||||
PrintLogPath,
|
||||
/// View mail from input file.
|
||||
View {
|
||||
#[structopt(value_name = "INPUT", parse(from_os_str))]
|
||||
|
@ -82,126 +114,156 @@ pub enum SubCommand {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ManOpt {
|
||||
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = manpages::parse_manpage))]
|
||||
/// If set, output text in stdout instead of spawning `$PAGER`.
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(long = "no-raw", alias = "no-raw"))]
|
||||
pub no_raw: bool,
|
||||
/// If set, output compressed gzip manpage in binary form in stdout.
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(long = "gzipped"))]
|
||||
pub gzipped: bool,
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(default_value = "meli", possible_values=manpages::POSSIBLE_VALUES, value_name="PAGE", parse(try_from_str = manpages::parse_manpage)))]
|
||||
/// Name of manual page.
|
||||
pub page: manpages::ManPages,
|
||||
/// If true, output text in stdout instead of spawning $PAGER.
|
||||
#[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")]
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub no_raw: Option<Option<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub mod manpages {
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum ToolOpt {
|
||||
ImapShell {
|
||||
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
|
||||
account: String,
|
||||
},
|
||||
#[cfg(feature = "smtp")]
|
||||
SmtpShell {
|
||||
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
|
||||
account: String,
|
||||
},
|
||||
#[cfg(feature = "jmap")]
|
||||
JmapShell {
|
||||
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
|
||||
account: String,
|
||||
},
|
||||
}
|
||||
|
||||
use melib::log;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub fn parse_manpage(src: &str) -> Result<ManPages> {
|
||||
match src {
|
||||
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
|
||||
"meli.7" | "guide" => Ok(ManPages::Guide),
|
||||
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
|
||||
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => {
|
||||
Ok(ManPages::Themes)
|
||||
}
|
||||
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
/// Choose manpage
|
||||
pub enum ManPages {
|
||||
/// meli(1)
|
||||
Main = 0,
|
||||
/// meli.conf(5)
|
||||
Conf = 1,
|
||||
/// meli-themes(5)
|
||||
Themes = 2,
|
||||
/// meli(7)
|
||||
Guide = 3,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ManPages {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Main => "meli.1",
|
||||
Self::Conf => "meli.conf.5",
|
||||
Self::Themes => "meli-themes.5",
|
||||
Self::Guide => "meli.7",
|
||||
}
|
||||
fn print_path(path: &std::path::Path) {
|
||||
if let Some(hostname) = nix::unistd::gethostname()
|
||||
.ok()
|
||||
.and_then(|s| s.into_string().ok())
|
||||
{
|
||||
println!(
|
||||
"{}",
|
||||
Hyperlink::new(
|
||||
&path.display(),
|
||||
&format_args!("file://{hostname}{}", path.display())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManPages {
|
||||
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
|
||||
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
|
||||
tries.push(p.into());
|
||||
p.exists()
|
||||
&& p.is_dir()
|
||||
&& fs::metadata(p)
|
||||
.ok()
|
||||
.map(|m| !m.permissions().readonly())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
let mut tries = vec![];
|
||||
let Some(mut path) = destination
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
.or_else(|| {
|
||||
if let Some(paths) = env::var_os("MANPATH") {
|
||||
if let Some(path) =
|
||||
env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
env::home_dir()
|
||||
.map(|p| p.join("local").join("share"))
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
})
|
||||
else {
|
||||
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
|
||||
};
|
||||
|
||||
for (p, dir) in [
|
||||
(ManPages::Main, "man1"),
|
||||
(ManPages::Conf, "man5"),
|
||||
(ManPages::Themes, "man5"),
|
||||
(ManPages::Guide, "man7"),
|
||||
] {
|
||||
let text = crate::subcommands::man(p, true)?;
|
||||
path.push(dir);
|
||||
std::fs::create_dir_all(&path).map_err(|err| {
|
||||
Error::new(format!("Could not create {} directory.", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
path.push(&p.to_string());
|
||||
|
||||
fs::write(&path, text.as_bytes()).map_err(|err| {
|
||||
Error::new(format!("Could not write to {}", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
log::trace!("Installed {} to {}", p, path.display());
|
||||
path.pop();
|
||||
path.pop();
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
);
|
||||
} else {
|
||||
println!("{}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
/// Execute `self.subcommand` if any, and return its result. Otherwise
|
||||
/// return `None`.
|
||||
pub fn execute(self) -> Option<Result<()>> {
|
||||
macro_rules! ret_err {
|
||||
($sth:expr) => {
|
||||
match $sth {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Some(Err(err.into())),
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(match self.subcommand? {
|
||||
SubCommand::View { .. } => { return None ; }
|
||||
SubCommand::TestConfig { path } => {
|
||||
subcommands::test_config(path)
|
||||
}
|
||||
SubCommand::Tools(toolopt) => {
|
||||
subcommands::tool(self.config, toolopt)
|
||||
}
|
||||
SubCommand::CreateConfig { path } => {
|
||||
subcommands::create_config(path)
|
||||
}
|
||||
SubCommand::EditConfig => {
|
||||
subcommands::edit_config()
|
||||
}
|
||||
SubCommand::PrintConfigPath => {
|
||||
let config_path = ret_err!(crate::conf::get_config_file());
|
||||
print_path(&config_path);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
SubCommand::Man(ManOpt {}) => {
|
||||
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
SubCommand::Man(ManOpt {
|
||||
page,
|
||||
no_raw,
|
||||
gzipped: true,
|
||||
}) => {
|
||||
use std::io::Write;
|
||||
|
||||
ret_err!(std::io::stdout().write_all(if no_raw {
|
||||
page.text_gz()
|
||||
} else {
|
||||
page.mdoc_gz()
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
SubCommand::Man(ManOpt {
|
||||
page,
|
||||
no_raw,
|
||||
gzipped: false,
|
||||
}) => {
|
||||
subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw))
|
||||
}
|
||||
SubCommand::CompiledWith => {
|
||||
subcommands::compiled_with()
|
||||
}
|
||||
SubCommand::PrintLoadedThemes => {
|
||||
let s = ret_err!(conf::FileSettings::new());
|
||||
print!("{}", s.terminal.themes);
|
||||
Ok(())
|
||||
}
|
||||
SubCommand::PrintDefaultTheme => {
|
||||
print!("{}", conf::Themes::default().key_to_string("dark", false));
|
||||
Ok(())
|
||||
}
|
||||
SubCommand::PrintAppDirectories => {
|
||||
print_path(&xdg::BaseDirectories::with_prefix("meli")
|
||||
.expect(
|
||||
"Could not find your XDG directories. If this is unexpected, please \
|
||||
report it as a bug."
|
||||
)
|
||||
.get_data_file(""));
|
||||
let mut temp_dir = std::env::temp_dir();
|
||||
temp_dir.push("meli");
|
||||
print_path(&temp_dir);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
SubCommand::InstallMan {
|
||||
destination_path: _,
|
||||
} => {
|
||||
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
SubCommand::InstallMan { destination_path } => {
|
||||
match crate::manpages::ManPages::install(destination_path) {
|
||||
Ok(p) => println!("Installed at {}.", p.display()),
|
||||
Err(err) => return Some(Err(err)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SubCommand::PrintLogPath => {
|
||||
let settings = ret_err!(crate::conf::Settings::new());
|
||||
print_path(&settings._logger.log_dest());
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ use melib::{
|
|||
SortField, SortOrder,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod actions;
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
|
@ -55,14 +58,15 @@ pub use crate::actions::{
|
|||
AccountAction::{self, *},
|
||||
Action::{self, *},
|
||||
ComposeAction::{self, *},
|
||||
ComposerTabAction, FlagAction,
|
||||
ListingAction::{self, *},
|
||||
MailingListAction::{self, *},
|
||||
TabAction::{self, *},
|
||||
TagAction::{self, *},
|
||||
TagAction,
|
||||
ViewAction::{self, *},
|
||||
};
|
||||
|
||||
/// Helper macro to convert an array of tokens into a TokenStream
|
||||
/// Helper macro to convert an array of tokens into a `TokenStream`
|
||||
macro_rules! to_stream {
|
||||
($token: expr) => {
|
||||
TokenStream {
|
||||
|
@ -111,7 +115,7 @@ pub fn quoted_argument(input: &[u8]) -> IResult<&[u8], &str> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TokenStream {
|
||||
tokens: &'static [TokenAdicity],
|
||||
}
|
||||
|
@ -142,6 +146,11 @@ impl TokenStream {
|
|||
tokens.append(&mut m);
|
||||
}
|
||||
}
|
||||
AlternativeStrings(v) => {
|
||||
for t in v.iter() {
|
||||
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, t));
|
||||
}
|
||||
}
|
||||
Seq(_s) => {}
|
||||
RestOfStringValue => {
|
||||
sugg.insert(String::new());
|
||||
|
@ -195,6 +204,20 @@ impl TokenStream {
|
|||
*s = "";
|
||||
}
|
||||
}
|
||||
AlternativeStrings(v) => {
|
||||
for lit in v.iter() {
|
||||
if lit.starts_with(*s) && lit.len() != s.len() {
|
||||
sugg.insert(lit[s.len()..].to_string());
|
||||
tokens.push((s, *t.inner()));
|
||||
return tokens;
|
||||
} else if s.starts_with(lit) {
|
||||
tokens.push((&s[..lit.len()], *t.inner()));
|
||||
*s = &s[lit.len()..];
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
Seq(_s) => {
|
||||
return vec![];
|
||||
}
|
||||
|
@ -225,7 +248,7 @@ impl TokenStream {
|
|||
|
||||
/// `Token` wrapper that defines how many times a token is expected to be
|
||||
/// repeated
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TokenAdicity {
|
||||
ZeroOrOne(Token),
|
||||
ZeroOrMore(Token),
|
||||
|
@ -245,11 +268,12 @@ impl TokenAdicity {
|
|||
}
|
||||
|
||||
/// A token encountered in the UI's command execution bar
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Token {
|
||||
Literal(&'static str),
|
||||
Filepath,
|
||||
Alternatives(&'static [TokenStream]),
|
||||
AlternativeStrings(&'static [&'static str]),
|
||||
Seq(&'static [TokenAdicity]),
|
||||
AccountName,
|
||||
MailboxPath,
|
||||
|
@ -339,6 +363,11 @@ define_commands!([
|
|||
tokens: &[One(Literal("search")), One(RestOfStringValue)],
|
||||
parser: parser::search
|
||||
},
|
||||
{ tags: ["clear-selection"],
|
||||
desc: "clear-selection",
|
||||
tokens: &[One(Literal("clear-selection"))],
|
||||
parser: parser::select
|
||||
},
|
||||
{ tags: ["select"],
|
||||
desc: "select <TERM>, selects envelopes matching with given term",
|
||||
tokens: &[One(Literal("select")), One(RestOfStringValue)],
|
||||
|
@ -397,6 +426,11 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
tokens: &[One(Literal("save-draft"))],
|
||||
parser: parser::save_draft
|
||||
},
|
||||
{ tags: ["discard-draft"],
|
||||
desc: "discard draft",
|
||||
tokens: &[One(Literal("discard-draft"))],
|
||||
parser: parser::discard_draft
|
||||
},
|
||||
{ tags: ["toggle sign "],
|
||||
desc: "switch between sign/unsign for this draft",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("sign"))],
|
||||
|
@ -482,6 +516,18 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
tokens: &[One(Literal("manage-mailboxes"))],
|
||||
parser: parser::manage_mailboxes
|
||||
},
|
||||
{ tags: ["man"],
|
||||
desc: "read documentation",
|
||||
tokens: {
|
||||
#[cfg(feature = "cli-docs")]
|
||||
{
|
||||
&[One(Literal("man")), One(AlternativeStrings(crate::manpages::POSSIBLE_VALUES))]
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
{ &[] }
|
||||
},
|
||||
parser: parser::view_manpage
|
||||
},
|
||||
{ tags: ["manage-jobs"],
|
||||
desc: "view and manage jobs",
|
||||
tokens: &[One(Literal("manage-jobs"))],
|
||||
|
@ -502,7 +548,7 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
/// Get command suggestions for input
|
||||
pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
||||
use crate::melib::ShellExpandTrait;
|
||||
let mut sugg = Default::default();
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
let _m = tokens.matches(&mut &(*input), &mut sugg);
|
||||
if _m.is_empty() {
|
||||
|
@ -510,175 +556,10 @@ pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
|||
}
|
||||
if let Some((s, Filepath)) = _m.last() {
|
||||
let p = std::path::Path::new(s);
|
||||
sugg.extend(p.complete(true).into_iter());
|
||||
sugg.extend(p.complete(true, s.ends_with('/')).into_iter());
|
||||
}
|
||||
}
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()])
|
||||
.collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_parser_interactive() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
if input.trim() == "quit" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
println!("alright");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_parser_all() {
|
||||
use CommandError::*;
|
||||
|
||||
for cmd in [
|
||||
"set unseen",
|
||||
"set seen",
|
||||
"delete",
|
||||
"copyto somewhere",
|
||||
"moveto somewhere",
|
||||
"import fpath mpath",
|
||||
"close ",
|
||||
"go 5",
|
||||
] {
|
||||
parse_command(cmd.as_bytes()).unwrap_or_else(|err| panic!("{} failed {}", cmd, err));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_command(b"setfafsfoo").unwrap_err().to_string(),
|
||||
Parsing {
|
||||
inner: "setfafsfoo".into(),
|
||||
kind: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"set foo").unwrap_err().to_string(),
|
||||
BadValue {
|
||||
inner: "Bad argument for `set`. Accepted arguments are [seen, unseen, plain, \
|
||||
threaded, compact, conversations]."
|
||||
.into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"moveto ").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (1, Some(1)),
|
||||
given: 0,
|
||||
__func__: "moveto",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (1, Some(1)),
|
||||
given: 2,
|
||||
__func__: "reindex",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,19 +21,25 @@
|
|||
|
||||
//! User actions that need to be handled by the UI
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use melib::{email::mailto::Mailto, SortField, SortOrder};
|
||||
use melib::{email::mailto::Mailto, Flag, SortField, SortOrder};
|
||||
|
||||
use crate::components::{Component, ComponentId};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum FlagAction {
|
||||
Set(Flag),
|
||||
Unset(Flag),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum TagAction {
|
||||
Add(String),
|
||||
Remove(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ListingAction {
|
||||
SetPlain,
|
||||
SetThreaded,
|
||||
|
@ -43,6 +49,7 @@ pub enum ListingAction {
|
|||
Select(String),
|
||||
SetSeen,
|
||||
SetUnseen,
|
||||
SendToTrash,
|
||||
CopyTo(MailboxPath),
|
||||
CopyToOtherAccount(AccountName, MailboxPath),
|
||||
MoveTo(MailboxPath),
|
||||
|
@ -52,53 +59,66 @@ pub enum ListingAction {
|
|||
Delete,
|
||||
OpenInNewTab,
|
||||
Tag(TagAction),
|
||||
Flag(FlagAction),
|
||||
ClearSelection,
|
||||
ToggleThreadSnooze,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ComposerTabAction {
|
||||
DiscardDraft,
|
||||
SaveDraft,
|
||||
#[cfg(feature = "gpgme")]
|
||||
ToggleSign,
|
||||
#[cfg(feature = "gpgme")]
|
||||
ToggleEncrypt,
|
||||
AddAttachment(String),
|
||||
AddAttachmentFilePicker(Option<String>),
|
||||
AddAttachmentPipe(String),
|
||||
RemoveAttachment(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TabAction {
|
||||
ComposerAction(ComposerTabAction),
|
||||
Close,
|
||||
Kill(ComponentId),
|
||||
New(Option<Box<dyn Component>>),
|
||||
ManageMailboxes,
|
||||
ManageJobs,
|
||||
#[cfg(feature = "cli-docs")]
|
||||
Man(crate::manpages::ManPages),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum MailingListAction {
|
||||
ListPost,
|
||||
ListArchive,
|
||||
ListUnsubscribe,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ViewAction {
|
||||
Pipe(String, Vec<String>),
|
||||
Filter(String),
|
||||
Filter(Option<String>),
|
||||
SaveAttachment(usize, String),
|
||||
PipeAttachment(usize, String, Vec<String>),
|
||||
ExportMail(String),
|
||||
AddAddressesToContacts,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ComposeAction {
|
||||
AddAttachment(String),
|
||||
AddAttachmentFilePicker(Option<String>),
|
||||
AddAttachmentPipe(String),
|
||||
RemoveAttachment(usize),
|
||||
SaveDraft,
|
||||
ToggleSign,
|
||||
ToggleEncrypt,
|
||||
Mailto(Mailto),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum AccountAction {
|
||||
ReIndex,
|
||||
PrintAccountSetting(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum MailboxOperation {
|
||||
Create(NewMailboxPath),
|
||||
Delete(MailboxPath),
|
||||
|
@ -109,7 +129,7 @@ pub enum MailboxOperation {
|
|||
SetPermissions(MailboxPath),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Action {
|
||||
Listing(ListingAction),
|
||||
ViewMailbox(usize),
|
||||
|
@ -136,10 +156,10 @@ impl Action {
|
|||
pub fn needs_confirmation(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Action::Listing(ListingAction::Delete)
|
||||
| Action::MailingListAction(_)
|
||||
| Action::Mailbox(_, _)
|
||||
| Action::Quit
|
||||
Self::Listing(ListingAction::Delete)
|
||||
| Self::MailingListAction(_)
|
||||
| Self::Mailbox(_, _)
|
||||
| Self::Quit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +169,7 @@ type MailboxPath = String;
|
|||
type NewMailboxPath = String;
|
||||
|
||||
macro_rules! impl_into_action {
|
||||
($({$t:ty => $var:tt}),*) => {
|
||||
($({$t:ty => $var:tt}),*$(,)?) => {
|
||||
$(
|
||||
impl From<$t> for Action {
|
||||
fn from(v: $t) -> Self {
|
||||
|
@ -160,11 +180,11 @@ macro_rules! impl_into_action {
|
|||
};
|
||||
}
|
||||
macro_rules! impl_tuple_into_action {
|
||||
($({$a:ty,$b:ty => $var:tt}),*) => {
|
||||
($({$a:ty,$b:ty => $var:tt}),*$(,)?) => {
|
||||
$(
|
||||
impl From<($a,$b)> for Action {
|
||||
fn from((a, b): ($a,$b)) -> Self {
|
||||
Self::$var(a, b)
|
||||
Self::$var(a.to_string(), b)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
@ -180,5 +200,7 @@ impl_into_action!(
|
|||
);
|
||||
impl_tuple_into_action!(
|
||||
{ AccountName, MailboxOperation => Mailbox },
|
||||
{ AccountName, AccountAction => AccountAction }
|
||||
{ AccountName, AccountAction => AccountAction },
|
||||
{ Arc<str>, MailboxOperation => Mailbox },
|
||||
{ Arc<str>, AccountAction => AccountAction },
|
||||
);
|
||||
|
|
|
@ -47,9 +47,9 @@ impl<const MIN: u8, const MAX: u8> ArgCheck<MIN, MAX> {
|
|||
}
|
||||
);
|
||||
};
|
||||
let is_empty = dbg!(input.trim().is_empty());
|
||||
if is_empty && dbg!(MIN) > 0 {
|
||||
return dbg!(Err(CommandError::WrongNumberOfArguments {
|
||||
let is_empty = input.trim().is_empty();
|
||||
if is_empty && MIN > 0 {
|
||||
return Err(CommandError::WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (MIN, MAX.into()),
|
||||
given: 0,
|
||||
|
@ -60,7 +60,7 @@ impl<const MIN: u8, const MAX: u8> ArgCheck<MIN, MAX> {
|
|||
MIN
|
||||
)
|
||||
.into(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
*self = Self::BeforeArgument {
|
||||
so_far: 0,
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CommandError {
|
||||
Parsing {
|
||||
inner: Cow<'static, str>,
|
||||
|
@ -29,6 +29,7 @@ pub enum CommandError {
|
|||
},
|
||||
BadValue {
|
||||
inner: Cow<'static, str>,
|
||||
suggestions: Option<&'static [&'static str]>,
|
||||
},
|
||||
WrongNumberOfArguments {
|
||||
too_many: bool,
|
||||
|
@ -63,7 +64,25 @@ impl std::fmt::Display for CommandError {
|
|||
Self::Parsing { inner, kind: _ } => {
|
||||
write!(fmt, "Could not parse command: {}", inner)
|
||||
}
|
||||
Self::BadValue { inner } => {
|
||||
Self::BadValue {
|
||||
inner,
|
||||
suggestions: Some(suggs),
|
||||
} => {
|
||||
write!(fmt, "Bad value/argument: {}. Possible values are: ", inner)?;
|
||||
let len = suggs.len();
|
||||
for (i, val) in suggs.iter().enumerate() {
|
||||
if i == len.saturating_sub(1) {
|
||||
write!(fmt, "{}", val)?;
|
||||
} else {
|
||||
write!(fmt, "{}, ", val)?;
|
||||
}
|
||||
}
|
||||
write!(fmt, "")
|
||||
}
|
||||
Self::BadValue {
|
||||
inner,
|
||||
suggestions: None,
|
||||
} => {
|
||||
write!(fmt, "Bad value/argument: {}", inner)
|
||||
}
|
||||
Self::WrongNumberOfArguments {
|
||||
|
|
|
@ -24,22 +24,37 @@
|
|||
use super::*;
|
||||
use crate::command::{argcheck::*, error::*};
|
||||
|
||||
const FLAG_SUGGESTIONS: &[&str] = &[
|
||||
"passed",
|
||||
"replied",
|
||||
"seen or read",
|
||||
"junk or trash or trashed",
|
||||
"draft",
|
||||
"flagged",
|
||||
];
|
||||
|
||||
macro_rules! command_err {
|
||||
(nom $b:expr, $input: expr, $msg:literal) => {{
|
||||
(nom $b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
|
||||
let evaluated: IResult<&'_ [u8], _> = { $b };
|
||||
match evaluated {
|
||||
Err(_) => {
|
||||
let err = CommandError::BadValue { inner: $msg.into() };
|
||||
let err = CommandError::BadValue {
|
||||
inner: $msg.into(),
|
||||
suggestions: $suggs,
|
||||
};
|
||||
return Ok(($input, Err(err)));
|
||||
}
|
||||
Ok(v) => v,
|
||||
}
|
||||
}};
|
||||
($b:expr, $input: expr, $msg:literal) => {{
|
||||
($b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
|
||||
let evaluated = { $b };
|
||||
match evaluated {
|
||||
Err(_) => {
|
||||
let err = CommandError::BadValue { inner: $msg.into() };
|
||||
let err = CommandError::BadValue {
|
||||
inner: $msg.into(),
|
||||
suggestions: $suggs,
|
||||
};
|
||||
return Ok(($input, Err(err)));
|
||||
}
|
||||
Ok(v) => v,
|
||||
|
@ -101,11 +116,18 @@ pub fn listing_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErro
|
|||
open_in_new_tab,
|
||||
export_mbox,
|
||||
_tag,
|
||||
flag,
|
||||
))(input)
|
||||
}
|
||||
|
||||
pub fn compose_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
alt((add_attachment, mailto, remove_attachment, save_draft))(input)
|
||||
alt((
|
||||
add_attachment,
|
||||
mailto,
|
||||
remove_attachment,
|
||||
save_draft,
|
||||
discard_draft,
|
||||
))(input)
|
||||
}
|
||||
|
||||
pub fn account_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
|
@ -117,13 +139,14 @@ pub fn view(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
filter,
|
||||
pipe,
|
||||
save_attachment,
|
||||
pipe_attachment,
|
||||
export_mail,
|
||||
add_addresses_to_contacts,
|
||||
))(input)
|
||||
}
|
||||
|
||||
pub fn new_tab(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
alt((manage_mailboxes, manage_jobs, compose_action))(input)
|
||||
alt((manage_mailboxes, manage_jobs, compose_action, view_manpage))(input)
|
||||
}
|
||||
|
||||
pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
|
||||
|
@ -154,6 +177,125 @@ pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
|
|||
.and_then(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// Set/unset a flag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use meli::{melib::Flag, command::{Action,ListingAction, FlagAction, parser}};
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag set junk").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(
|
||||
/// matches!(
|
||||
/// parsed,
|
||||
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
|
||||
/// Flag::TRASHED
|
||||
/// ))))
|
||||
/// ),
|
||||
/// "{:?}",
|
||||
/// parsed
|
||||
/// );
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag unset junk").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(
|
||||
/// matches!(
|
||||
/// parsed,
|
||||
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Unset(
|
||||
/// Flag::TRASHED
|
||||
/// ))))
|
||||
/// ),
|
||||
/// "{:?}",
|
||||
/// parsed
|
||||
/// );
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag set draft").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(
|
||||
/// matches!(
|
||||
/// parsed,
|
||||
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
|
||||
/// Flag::DRAFT
|
||||
/// ))))
|
||||
/// ),
|
||||
/// "{:?}",
|
||||
/// parsed
|
||||
/// );
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag set xunk").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert_eq!(
|
||||
/// &parsed.unwrap_err().to_string(),
|
||||
/// "Bad value/argument: xunk is not a valid flag name. Possible values are: passed, replied, \
|
||||
/// seen or read, junk or trash or trashed, draft, flagged"
|
||||
/// );
|
||||
/// ```
|
||||
pub fn flag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
use melib::Flag;
|
||||
|
||||
fn parse_flag(s: &str) -> Option<Flag> {
|
||||
match s {
|
||||
o if o.eq_ignore_ascii_case("passed") => Some(Flag::PASSED),
|
||||
o if o.eq_ignore_ascii_case("replied") => Some(Flag::REPLIED),
|
||||
o if o.eq_ignore_ascii_case("seen") => Some(Flag::SEEN),
|
||||
o if o.eq_ignore_ascii_case("read") => Some(Flag::SEEN),
|
||||
o if o.eq_ignore_ascii_case("junk") => Some(Flag::TRASHED),
|
||||
o if o.eq_ignore_ascii_case("trash") => Some(Flag::TRASHED),
|
||||
o if o.eq_ignore_ascii_case("trashed") => Some(Flag::TRASHED),
|
||||
o if o.eq_ignore_ascii_case("draft") => Some(Flag::DRAFT),
|
||||
o if o.eq_ignore_ascii_case("flagged") => Some(Flag::FLAGGED),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
preceded(
|
||||
tag("flag"),
|
||||
alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
|
||||
let (input, _) = tag("set")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, flag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
let Some(flag) = parse_flag(flag) else {
|
||||
return Ok((
|
||||
b"",
|
||||
Err(CommandError::BadValue {
|
||||
inner: format!("{flag} is not a valid flag name").into(),
|
||||
suggestions: Some(FLAG_SUGGESTIONS),
|
||||
}),
|
||||
));
|
||||
};
|
||||
Ok((input, Ok(Listing(Flag(FlagAction::Set(flag))))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
|
||||
let (input, _) = tag("unset")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, flag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
let Some(flag) = parse_flag(flag) else {
|
||||
return Ok((
|
||||
b"",
|
||||
Err(CommandError::BadValue {
|
||||
inner: format!("{flag} is not a valid flag name").into(),
|
||||
suggestions: Some(FLAG_SUGGESTIONS),
|
||||
}),
|
||||
));
|
||||
};
|
||||
Ok((input, Ok(Listing(Flag(FlagAction::Unset(flag))))))
|
||||
},
|
||||
)),
|
||||
)(input.trim())
|
||||
}
|
||||
|
||||
pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, set};
|
||||
|
@ -173,9 +315,13 @@ pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, ret) = command_err!(nom
|
||||
alt((map(tag("seen"), |_| Listing(SetSeen)), map(tag("unseen"), |_| Listing(SetUnseen))))(input),
|
||||
alt((
|
||||
map(tag("seen"), |_| Listing(SetSeen)),
|
||||
map(tag("unseen"), |_| Listing(SetUnseen)
|
||||
)))(input),
|
||||
input,
|
||||
"Bad argument for `set`. Accepted arguments are [seen, unseen, plain, threaded, compact, conversations].");
|
||||
String::from_utf8_lossy(input.trim()).to_string(),
|
||||
Some(&["seen", "unseen", "plain", "threaded", "compact", "conversations"]));
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(ret)))
|
||||
|
@ -280,7 +426,8 @@ pub fn goto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
let (input, nth) = command_err!(nom
|
||||
usize_c(input),
|
||||
input,
|
||||
"Argument must be an integer.");
|
||||
"Argument must be an integer.",
|
||||
None);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Action::ViewMailbox(nth))))
|
||||
|
@ -336,6 +483,26 @@ pub fn search(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
Ok((input, Ok(Listing(Search(String::from(string))))))
|
||||
}
|
||||
pub fn select(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
#[inline]
|
||||
fn clear_selection(input: &[u8]) -> Option<IResult<&[u8], Result<Action, CommandError>>> {
|
||||
if !input.trim().starts_with(b"clear-selection") {
|
||||
return None;
|
||||
}
|
||||
#[inline]
|
||||
fn inner(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, clear_selection};
|
||||
let (input, _) = tag("clear-selection")(input.ltrim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Listing(ListingAction::ClearSelection))))
|
||||
}
|
||||
Some(inner(input))
|
||||
}
|
||||
if let Some(retval) = clear_selection(input) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
let mut check = arg_init! { min_arg:1, max_arg: {u8::MAX}, select};
|
||||
let (input, _) = tag("select")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
|
@ -402,15 +569,15 @@ pub fn printenv(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
Ok((input, Ok(PrintEnv(key.to_string()))))
|
||||
}
|
||||
pub fn currentdir(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, currentdir};
|
||||
let (input, _) = tag("cwd")(input.ltrim())?;
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, pwd};
|
||||
let (input, _) = alt((tag("cwd"), tag("pwd")))(input.ltrim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(CurrentDirectory)))
|
||||
}
|
||||
pub fn change_currentdir(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, change_currentdir};
|
||||
let mut check = arg_init! { min_arg: 1, max_arg: 1, cd};
|
||||
let (input, _) = tag("cd")(input.ltrim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
|
@ -433,7 +600,8 @@ pub fn mailto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
let (input, val) = command_err!(
|
||||
parser(val.as_bytes()),
|
||||
val.as_bytes(),
|
||||
"Could not parse mailto value. If the value is valid, please report this bug."
|
||||
"Could not parse mailto value. If the value is valid, please report this bug.",
|
||||
None
|
||||
);
|
||||
Ok((input, Ok(Compose(Mailto(val)))))
|
||||
}
|
||||
|
@ -473,14 +641,18 @@ pub fn pipe<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
|
|||
))(input)
|
||||
}
|
||||
pub fn filter(input: &'_ [u8]) -> IResult<&'_ [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, filter};
|
||||
let mut check = arg_init! { min_arg:0, max_arg:255, filter};
|
||||
let (input, _) = tag("filter")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
if let Ok((input, _)) = eof(input) {
|
||||
arg_chk!(finish check, input);
|
||||
return Ok((input, Ok(View(Filter(None)))));
|
||||
}
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, cmd) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
Ok((input, Ok(View(Filter(cmd.to_string())))))
|
||||
Ok((input, Ok(View(Filter(Some(cmd.to_string()))))))
|
||||
}
|
||||
pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
alt((
|
||||
|
@ -495,7 +667,12 @@ pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, C
|
|||
let (input, cmd) = quoted_argument(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(AddAttachmentPipe(cmd.to_string())))))
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::AddAttachmentPipe(
|
||||
cmd.to_string(),
|
||||
)))),
|
||||
))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment};
|
||||
|
@ -506,32 +683,46 @@ pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, C
|
|||
let (input, path) = quoted_argument(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(AddAttachment(path.to_string())))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, add_attachment};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(AddAttachmentFilePicker(None)))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment_file_picker};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("<")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Compose(AddAttachmentFilePicker(Some(shell.to_string())))),
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::AddAttachment(
|
||||
path.to_string(),
|
||||
)))),
|
||||
))
|
||||
},
|
||||
alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment_file_picker};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("<")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(
|
||||
ComposerTabAction::AddAttachmentFilePicker(Some(shell.to_string())),
|
||||
))),
|
||||
))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, add_attachment};
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(
|
||||
ComposerTabAction::AddAttachmentFilePicker(None),
|
||||
))),
|
||||
))
|
||||
},
|
||||
)),
|
||||
))(input)
|
||||
}
|
||||
pub fn remove_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
|
@ -543,7 +734,12 @@ pub fn remove_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandE
|
|||
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(RemoveAttachment(idx)))))
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::RemoveAttachment(
|
||||
idx,
|
||||
)))),
|
||||
))
|
||||
}
|
||||
pub fn save_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, save_draft };
|
||||
|
@ -551,7 +747,18 @@ pub fn save_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
|
|||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Compose(SaveDraft))))
|
||||
Ok((input, Ok(Tab(ComposerAction(ComposerTabAction::SaveDraft)))))
|
||||
}
|
||||
pub fn discard_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, discard_draft };
|
||||
let (input, _) = tag("discard-draft")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Ok(Tab(ComposerAction(ComposerTabAction::DiscardDraft))),
|
||||
))
|
||||
}
|
||||
pub fn create_mailbox(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, create_malbox};
|
||||
|
@ -689,6 +896,35 @@ pub fn save_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErr
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(View(SaveAttachment(idx, path.to_string())))))
|
||||
}
|
||||
pub fn pipe_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg:{u8::MAX}, pipe_attachment};
|
||||
let (input, _) = tag("pipe-attachment")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, bin) = quoted_argument(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, args) = alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, args) = separated_list1(is_a(" "), quoted_argument)(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((
|
||||
input,
|
||||
args.into_iter().map(String::from).collect::<Vec<String>>(),
|
||||
))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Vec::with_capacity(0)))
|
||||
},
|
||||
))(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
Ok((input, Ok(View(PipeAttachment(idx, bin.to_string(), args)))))
|
||||
}
|
||||
pub fn export_mail(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, export_mail};
|
||||
let (input, _) = tag("export-mail")(input.trim())?;
|
||||
|
@ -708,6 +944,19 @@ pub fn add_addresses_to_contacts(input: &[u8]) -> IResult<&[u8], Result<Action,
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(View(AddAddressesToContacts))))
|
||||
}
|
||||
|
||||
/// Set/unset a tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use meli::command::{Action,ListingAction, TagAction, parser::_tag};
|
||||
///
|
||||
/// let (rest, parsed) = _tag(b"tag add newsletters").unwrap();
|
||||
/// println!("parsed is {:?}", parsed);
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(matches!(parsed, Ok(Action::Listing(ListingAction::Tag(TagAction::Add(ref tagname)))) if tagname == "newsletters"), "{:?}", parsed);
|
||||
/// ```
|
||||
pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
preceded(
|
||||
tag("tag"),
|
||||
|
@ -721,7 +970,7 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
|
|||
let (input, tag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Listing(Tag(Add(tag.to_string()))))))
|
||||
Ok((input, Ok(Listing(Tag(TagAction::Add(tag.to_string()))))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, tag};
|
||||
|
@ -732,11 +981,12 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
|
|||
let (input, tag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Listing(Tag(Remove(tag.to_string()))))))
|
||||
Ok((input, Ok(Listing(Tag(TagAction::Remove(tag.to_string()))))))
|
||||
},
|
||||
)),
|
||||
)(input.trim())
|
||||
}
|
||||
|
||||
pub fn print_account_setting(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, print};
|
||||
let (input, _) = tag("print")(input.trim())?;
|
||||
|
@ -782,8 +1032,13 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
for (tok, action) in [
|
||||
("thread_snooze", Listing(ToggleThreadSnooze)),
|
||||
("mouse", ToggleMouse),
|
||||
("sign", Compose(ToggleSign)),
|
||||
("encrypt", Compose(ToggleEncrypt)),
|
||||
#[cfg(feature = "gpgme")]
|
||||
("sign", Tab(ComposerAction(ComposerTabAction::ToggleSign))),
|
||||
#[cfg(feature = "gpgme")]
|
||||
(
|
||||
"encrypt",
|
||||
Tab(ComposerAction(ComposerTabAction::ToggleEncrypt)),
|
||||
),
|
||||
] {
|
||||
if let Ok((inner_input, _)) = tag!()(tok)(input.trim()) {
|
||||
input = inner_input;
|
||||
|
@ -796,7 +1051,8 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
return Ok((
|
||||
input,
|
||||
Err(CommandError::BadValue {
|
||||
inner: "Valid toggle values are thread_snooze, mouse, sign, encrypt.".into(),
|
||||
inner: String::from_utf8_lossy(input).to_string().into(),
|
||||
suggestions: Some(&["thread_snooze", "mouse", "sign", "encrypt"]),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
@ -823,6 +1079,41 @@ pub fn manage_jobs(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Tab(ManageJobs))))
|
||||
}
|
||||
|
||||
pub fn view_manpage(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, view_manpage };
|
||||
let (input, _) = tag("man")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
#[allow(unused_variables)]
|
||||
let (input, manpage) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
#[cfg(feature = "cli-docs")]
|
||||
{
|
||||
match crate::manpages::parse_manpage(manpage) {
|
||||
Ok(m) => Ok((input, Ok(Tab(Man(m))))),
|
||||
Err(err) => Ok((
|
||||
input,
|
||||
Err(CommandError::BadValue {
|
||||
inner: err.to_string().into(),
|
||||
suggestions: Some(crate::manpages::POSSIBLE_VALUES),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
{
|
||||
Ok((
|
||||
input,
|
||||
Err(CommandError::Other {
|
||||
inner: "this meli binary has not been compiled with the cli-docs feature".into(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quit(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, quit};
|
||||
let (input, _) = tag("quit")(input.trim())?;
|
||||
|
|
206
meli/src/command/tests.rs
Normal file
206
meli/src/command/tests.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2017- Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
// //println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let _ = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
// if !m.is_empty() {
|
||||
// //print!("{:?} ", desc);
|
||||
// //println!(" result = {:#?}\n\n", m);
|
||||
// }
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()]).collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_command_parser_interactive() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
if input.trim() == "quit" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
println!("alright");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_parser_all() {
|
||||
use CommandError::*;
|
||||
|
||||
for cmd in [
|
||||
"set unseen",
|
||||
"set seen",
|
||||
"delete",
|
||||
"copyto somewhere",
|
||||
"moveto somewhere",
|
||||
"import fpath mpath",
|
||||
"close ",
|
||||
"go 5",
|
||||
] {
|
||||
parse_command(cmd.as_bytes()).unwrap_or_else(|err| panic!("{} failed {}", cmd, err));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_command(b"setfafsfoo").unwrap_err().to_string(),
|
||||
Parsing {
|
||||
inner: "setfafsfoo".into(),
|
||||
kind: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"set foo").unwrap_err().to_string(),
|
||||
BadValue {
|
||||
inner: "foo".into(),
|
||||
suggestions: Some(&[
|
||||
"seen",
|
||||
"unseen",
|
||||
"plain",
|
||||
"threaded",
|
||||
"compact",
|
||||
"conversations"
|
||||
])
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"moveto ").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (1, Some(1)),
|
||||
given: 0,
|
||||
__func__: "moveto",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (1, Some(1)),
|
||||
given: 2,
|
||||
__func__: "reindex",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_error_display() {
|
||||
assert_eq!(
|
||||
&CommandError::BadValue {
|
||||
inner: "foo".into(),
|
||||
suggestions: Some(&[
|
||||
"seen",
|
||||
"unseen",
|
||||
"plain",
|
||||
"threaded",
|
||||
"compact",
|
||||
"conversations"
|
||||
])
|
||||
}
|
||||
.to_string(),
|
||||
"Bad value/argument: foo. Possible values are: seen, unseen, plain, threaded, compact, \
|
||||
conversations"
|
||||
);
|
||||
}
|
|
@ -31,7 +31,7 @@ use uuid::Uuid;
|
|||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Eq, Deserialize, Hash, Ord, PartialOrd, PartialEq, Serialize)]
|
||||
#[derive(Clone, Copy, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[repr(transparent)]
|
||||
pub struct ComponentId(Uuid);
|
||||
|
||||
|
@ -98,7 +98,7 @@ impl ExtendShortcutsMaps for ShortcutMaps {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PageMovement {
|
||||
Up(usize),
|
||||
Right(usize),
|
||||
|
@ -110,14 +110,14 @@ pub enum PageMovement {
|
|||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ScrollContext {
|
||||
pub shown_lines: usize,
|
||||
pub total_lines: usize,
|
||||
pub has_more_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ScrollUpdate {
|
||||
End(ComponentId),
|
||||
Update {
|
||||
|
@ -126,6 +126,8 @@ pub enum ScrollUpdate {
|
|||
},
|
||||
}
|
||||
|
||||
/// A user interface component (also referred to as a UI widget).
|
||||
///
|
||||
/// Types implementing this Trait can draw on the terminal and receive events.
|
||||
/// If a type wants to skip drawing if it has not changed anything, it can hold
|
||||
/// some flag in its fields (eg `self.dirty = false`) and act upon that in their
|
||||
|
@ -248,6 +250,12 @@ impl Component for Box<dyn Component> {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Box<dyn Component> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self == other || self.id() == other.id()
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Attributes of a [`Component`] widget.
|
||||
///
|
||||
|
@ -266,7 +274,7 @@ impl Default for ComponentAttr {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ComponentPath {
|
||||
pub id: ComponentId,
|
||||
pub tail: SmallVec<[ComponentId; 8]>,
|
||||
|
@ -297,7 +305,7 @@ impl ComponentPath {
|
|||
// log::trace!("continue;");
|
||||
continue;
|
||||
}
|
||||
cursor = cursor.children().remove(id)?;
|
||||
cursor = cursor.children().shift_remove(id)?;
|
||||
}
|
||||
Some(cursor)
|
||||
}
|
||||
|
@ -312,3 +320,22 @@ impl ComponentPath {
|
|||
self.tail.last()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use std::borrow::Cow;
|
||||
|
||||
pub use indexmap::IndexMap;
|
||||
|
||||
pub use crate::{
|
||||
accounts::MailboxEntry,
|
||||
command::*,
|
||||
components::{
|
||||
Component, ComponentAttr, ComponentId, ComponentPath, ExtendShortcutsMaps,
|
||||
ScrollContext, *,
|
||||
},
|
||||
jobs::{JobId, JobMetadata},
|
||||
melib::{text::TextProcessing, utils::datetime, SortOrder},
|
||||
shortcut, AccountHash, Action, Area, Attr, CellBuffer, Context, DataColumns, EnvelopeHash,
|
||||
Key, MailboxHash, Shortcuts, StatusEvent, ThemeAttribute, UIDialog, UIEvent, UIMode,
|
||||
};
|
||||
}
|
||||
|
|
1193
meli/src/conf.rs
1193
meli/src/conf.rs
File diff suppressed because it is too large
Load diff
|
@ -20,22 +20,22 @@
|
|||
*/
|
||||
|
||||
//! Configuration for composing email.
|
||||
use std::collections::HashMap;
|
||||
|
||||
use melib::{email::HeaderName, ToggleFlag};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{
|
||||
default_vals::{ask, false_val, none, true_val},
|
||||
use indexmap::IndexMap;
|
||||
use melib::{conf::ActionFlag, email::HeaderName};
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
|
||||
use crate::conf::{
|
||||
default_values::{ask, false_val, none, true_val},
|
||||
deserializers::non_empty_string,
|
||||
};
|
||||
|
||||
/// Settings for writing and sending new e-mail
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ComposingSettings {
|
||||
/// A command to pipe new emails to
|
||||
/// Required
|
||||
pub send_mail: SendMail,
|
||||
/// Command to launch editor. Can have arguments. Draft filename is given as
|
||||
/// the last argument. If it's missing, the environment variable $EDITOR is
|
||||
/// looked up.
|
||||
|
@ -46,9 +46,10 @@ pub struct ComposingSettings {
|
|||
alias = "editor_cmd"
|
||||
)]
|
||||
pub editor_command: Option<String>,
|
||||
/// Embed editor (for terminal interfaces) instead of forking and waiting.
|
||||
#[serde(default = "false_val")]
|
||||
pub embed: bool,
|
||||
/// Embedded editor (for terminal interfaces) instead of forking and
|
||||
/// waiting.
|
||||
#[serde(default = "false_val", alias = "embed")]
|
||||
pub embedded_pty: bool,
|
||||
/// Set "format=flowed" in plain text attachments.
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "format-flowed")]
|
||||
|
@ -60,24 +61,26 @@ pub struct ComposingSettings {
|
|||
/// Set default header values for new drafts
|
||||
/// Default: empty
|
||||
#[serde(default, alias = "default-header-values")]
|
||||
pub default_header_values: HashMap<HeaderName, String>,
|
||||
/// Wrap header preample when editing a draft in an editor. This allows you
|
||||
pub default_header_values: IndexMap<HeaderName, String>,
|
||||
/// Wrap header preamble when editing a draft in an editor. This allows you
|
||||
/// to write non-plain text email without the preamble creating syntax
|
||||
/// errors. They are stripped when you return from the editor. The
|
||||
/// values should be a two element array of strings, a prefix and suffix.
|
||||
/// Default: None
|
||||
#[serde(default, alias = "wrap-header-preample")]
|
||||
#[serde(default, alias = "wrap-header-preamble")]
|
||||
pub wrap_header_preamble: Option<(String, String)>,
|
||||
/// Store sent mail after successful submission. This setting is meant to be
|
||||
/// disabled for non-standard behaviour in gmail, which auto-saves sent
|
||||
/// mail on its own. Default: true
|
||||
#[serde(default = "true_val")]
|
||||
pub store_sent_mail: bool,
|
||||
/// The attribution line appears above the quoted reply text.
|
||||
/// The attribution line that appears above the quoted reply text.
|
||||
///
|
||||
/// The format specifiers for the replied address are:
|
||||
/// - `%+f` — the sender's name and email address.
|
||||
/// - `%+n` — the sender's name (or email address, if no name is included).
|
||||
/// - `%+a` — the sender's email address.
|
||||
///
|
||||
/// The format string is passed to strftime(3) with the replied envelope's
|
||||
/// date. Default: "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
|
||||
#[serde(default = "none")]
|
||||
|
@ -90,7 +93,7 @@ pub struct ComposingSettings {
|
|||
/// Forward emails as attachment? (Alternative is inline)
|
||||
/// Default: ask
|
||||
#[serde(default = "ask", alias = "forward-as-attachment")]
|
||||
pub forward_as_attachment: ToggleFlag,
|
||||
pub forward_as_attachment: ActionFlag,
|
||||
/// Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip
|
||||
/// Default: `["Re:", "RE:", "Fwd:", "Fw:", "回复:", "回覆:", "SV:", "Sv:",
|
||||
/// "VS:", "Antw:", "Doorst:", "VS:", "VL:", "REF:", "TR:", "TR:", "AW:",
|
||||
|
@ -109,26 +112,56 @@ pub struct ComposingSettings {
|
|||
/// Disabled `compose-hooks`.
|
||||
#[serde(default, alias = "disabled-compose-hooks")]
|
||||
pub disabled_compose_hooks: Vec<String>,
|
||||
/// Plain text file with signature that will pre-populate an email draft.
|
||||
///
|
||||
/// Signatures must be explicitly enabled to be used, otherwise this setting
|
||||
/// will be ignored.
|
||||
///
|
||||
/// Default: `None`
|
||||
#[serde(default, alias = "signature-file")]
|
||||
pub signature_file: Option<PathBuf>,
|
||||
/// Pre-populate email drafts with signature, if any.
|
||||
///
|
||||
/// `meli` will lookup the signature value in this order:
|
||||
///
|
||||
/// 1. The `signature_file` setting.
|
||||
/// 2. `${XDG_CONFIG_DIR}/meli/<account>/signature`
|
||||
/// 3. `${XDG_CONFIG_DIR}/meli/signature`
|
||||
/// 4. `${XDG_CONFIG_DIR}/signature`
|
||||
/// 5. `${HOME}/.signature`
|
||||
/// 6. No signature otherwise.
|
||||
///
|
||||
/// Default: `false`
|
||||
#[serde(default = "false_val", alias = "use-signature")]
|
||||
pub use_signature: bool,
|
||||
/// Signature delimiter, that is, text that will be prefixed to your
|
||||
/// signature to separate it from the email body.
|
||||
///
|
||||
/// Default: `"\n\n-- \n"`
|
||||
#[serde(default, alias = "signature-delimiter")]
|
||||
pub signature_delimiter: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ComposingSettings {
|
||||
fn default() -> Self {
|
||||
ComposingSettings {
|
||||
send_mail: SendMail::ShellCommand("false".into()),
|
||||
Self {
|
||||
editor_command: None,
|
||||
embed: false,
|
||||
embedded_pty: false,
|
||||
format_flowed: true,
|
||||
insert_user_agent: true,
|
||||
default_header_values: HashMap::default(),
|
||||
default_header_values: IndexMap::default(),
|
||||
store_sent_mail: true,
|
||||
wrap_header_preamble: None,
|
||||
attribution_format_string: None,
|
||||
attribution_use_posix_locale: true,
|
||||
forward_as_attachment: ToggleFlag::Ask,
|
||||
forward_as_attachment: ActionFlag::Ask,
|
||||
reply_prefix_list_to_strip: None,
|
||||
reply_prefix: res(),
|
||||
custom_compose_hooks: vec![],
|
||||
disabled_compose_hooks: vec![],
|
||||
signature_file: None,
|
||||
use_signature: false,
|
||||
signature_delimiter: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +208,7 @@ pub mod strings {
|
|||
named_unit_variant!(server_submission);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SendMail {
|
||||
#[cfg(feature = "smtp")]
|
||||
|
@ -185,9 +218,17 @@ pub enum SendMail {
|
|||
ShellCommand(String),
|
||||
}
|
||||
|
||||
impl Default for SendMail {
|
||||
/// Returns the `false` POSIX shell utility, in order to return an error
|
||||
/// when called.
|
||||
fn default() -> Self {
|
||||
Self::ShellCommand("false".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell command compose hooks (See
|
||||
/// [`crate::mail::compose::hooks::Hook`])
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ComposeHook {
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
|
@ -201,3 +242,112 @@ impl From<ComposeHook> for crate::mail::hooks::Hook {
|
|||
Self::new_shell_command(c.name.into(), c.command)
|
||||
}
|
||||
}
|
||||
const SENDMAIL_ERR_HELP: &str = r#"Invalid `send_mail` value.
|
||||
|
||||
Here are some valid examples:
|
||||
|
||||
Use server submission in protocols that support it (JMAP, NNTP)
|
||||
===============================================================
|
||||
|
||||
send_mail = "server_submission"
|
||||
|
||||
Using a shell script
|
||||
====================
|
||||
|
||||
send_mail = "msmtp --read-recipients --read-envelope-from"
|
||||
|
||||
Direct SMTP connection
|
||||
======================
|
||||
|
||||
[accounts.account-name]
|
||||
send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } }
|
||||
|
||||
[accounts.account-name.send_mail]
|
||||
hostname = "mail.example.com"
|
||||
port = 587
|
||||
auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } }
|
||||
security = { type = "TLS", danger_accept_invalid_certs = true } }
|
||||
|
||||
|
||||
`send_mail` direct SMTP connection fields:
|
||||
- hostname: text
|
||||
- port: valid port number
|
||||
- envelope_from: text (optional, default is empty),
|
||||
- auth: ...
|
||||
- security: ... (optional, default is "auto")
|
||||
- extensions: ... (optional, default is PIPELINING, CHUNKING, PRDR, 8BITMIME, BINARYMIME, SMTPUTF8, AUTH and DSN_NOTIFY)
|
||||
|
||||
Possible values for `send_mail.auth`:
|
||||
|
||||
No authentication:
|
||||
|
||||
auth = { type = "none" }
|
||||
|
||||
Regular authentication:
|
||||
Note: `require_auth` and `auth_type` are optional and can be skipped.
|
||||
|
||||
auth = { type = "auto", username = "...", password = "...", require_auth = true, auth_type = ... }
|
||||
|
||||
password can be:
|
||||
password = { type = "raw", value = "..." }
|
||||
password = { type = "command_eval", value = "/path/to/password_script.sh" }
|
||||
|
||||
XOAuth2 authentication:
|
||||
Note: `require_auth` is optional and can be skipped.
|
||||
auth = { type = "xoauth2", token_command = "...", require_auth = true }
|
||||
|
||||
Possible values for `send_mail.auth.auth_type` when `auth.type` is "auto":
|
||||
|
||||
auth_type = { plain = false, login = true }
|
||||
|
||||
Possible values for `send_mail.security`:
|
||||
Note that in all cases field `danger_accept_invalid_certs` is optional and its default value is false.
|
||||
|
||||
security = "none"
|
||||
security = { type = "auto", danger_accept_invalid_certs = false }
|
||||
security = { type = "STARTTLS", danger_accept_invalid_certs = false }
|
||||
security = { type = "TLS", danger_accept_invalid_certs = false }
|
||||
|
||||
Possible values for `send_mail.extensions` (All optional and have default values `true`:
|
||||
pipelining
|
||||
chunking
|
||||
8bitmime
|
||||
prdr
|
||||
binarymime
|
||||
smtputf8
|
||||
auth
|
||||
dsn_notify: Array of options e.g. ["FAILURE"]
|
||||
"#;
|
||||
|
||||
impl<'de> Deserialize<'de> for SendMail {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SendMailInner {
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp(melib::smtp::SmtpServerConf),
|
||||
#[serde(with = "strings::server_submission")]
|
||||
ServerSubmission,
|
||||
ShellCommand(String),
|
||||
}
|
||||
|
||||
match melib::serde_path_to_error::deserialize(deserializer) {
|
||||
#[cfg(feature = "smtp")]
|
||||
Ok(SendMailInner::Smtp(v)) => Ok(Self::Smtp(v)),
|
||||
Ok(SendMailInner::ServerSubmission) => Ok(Self::ServerSubmission),
|
||||
Ok(SendMailInner::ShellCommand(v)) => Ok(Self::ShellCommand(v)),
|
||||
Err(err)
|
||||
if err.inner().to_string() == D::Error::missing_field("send_mail").to_string() =>
|
||||
{
|
||||
// Surely there should be a better way to do this...
|
||||
Err(err.into_inner())
|
||||
}
|
||||
Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
294
meli/src/conf/data_types/dotaddressable.rs
Normal file
294
meli/src/conf/data_types/dotaddressable.rs
Normal file
|
@ -0,0 +1,294 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2017- Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use crate::conf::*;
|
||||
|
||||
pub trait DotAddressable: serde::Serialize {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
if !path.is_empty() {
|
||||
Err(Error::new(format!(
|
||||
"{} has no fields, it is of type {}",
|
||||
parent_field,
|
||||
std::any::type_name::<Self>()
|
||||
)))
|
||||
} else {
|
||||
Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for bool {}
|
||||
|
||||
impl DotAddressable for String {}
|
||||
impl DotAddressable for char {}
|
||||
impl DotAddressable for data_types::IndexStyle {}
|
||||
impl DotAddressable for data_types::SearchBackend {}
|
||||
impl DotAddressable for data_types::ThreadLayout {}
|
||||
impl DotAddressable for u64 {}
|
||||
impl DotAddressable for TagHash {}
|
||||
impl DotAddressable for crate::terminal::Color {}
|
||||
impl DotAddressable for crate::terminal::Attr {}
|
||||
impl DotAddressable for crate::terminal::Key {}
|
||||
impl DotAddressable for usize {}
|
||||
impl DotAddressable for Query {}
|
||||
impl DotAddressable for melib::LogLevel {}
|
||||
impl DotAddressable for PathBuf {}
|
||||
impl DotAddressable for ToggleFlag {}
|
||||
impl DotAddressable for ActionFlag {}
|
||||
impl DotAddressable for melib::SpecialUsageMailbox {}
|
||||
impl<T: DotAddressable> DotAddressable for Option<T> {}
|
||||
impl<T: DotAddressable> DotAddressable for Vec<T> {}
|
||||
// impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash, V: DotAddressable>
|
||||
// DotAddressable for HashMap<K, V>
|
||||
// {
|
||||
// }
|
||||
// impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash> DotAddressable for
|
||||
// HashSet<K> {}
|
||||
impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash, V: DotAddressable> DotAddressable
|
||||
for indexmap::IndexMap<K, V>
|
||||
{
|
||||
}
|
||||
impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash> DotAddressable for indexmap::IndexSet<K> {}
|
||||
impl DotAddressable for (SortField, SortOrder) {}
|
||||
|
||||
impl DotAddressable for LogSettings {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"log_file" => self.log_file.lookup(field, tail),
|
||||
"maximum_level" => self.maximum_level.lookup(field, tail),
|
||||
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for Settings {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"accounts" => self.accounts.lookup(field, tail),
|
||||
"pager" => self.pager.lookup(field, tail),
|
||||
"listing" => self.listing.lookup(field, tail),
|
||||
"notifications" => self.notifications.lookup(field, tail),
|
||||
"shortcuts" => self.shortcuts.lookup(field, tail),
|
||||
"tags" => Err(Error::new("unimplemented")),
|
||||
"composing" => Err(Error::new("unimplemented")),
|
||||
"pgp" => Err(Error::new("unimplemented")),
|
||||
"terminal" => self.terminal.lookup(field, tail),
|
||||
"log" => self.log.lookup(field, tail),
|
||||
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for AccountConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"account" => self.account.lookup(field, tail),
|
||||
"conf" => self.conf.lookup(field, tail),
|
||||
"conf_override" => self.conf_override.lookup(field, tail),
|
||||
"mailbox_confs" => self.mailbox_confs.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for MailUIConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let _tail = &path[1..];
|
||||
match *field {
|
||||
"pager" => Err(Error::new("unimplemented")), /* self.pager.lookup(field, */
|
||||
// tail),
|
||||
"listing" => Err(Error::new("unimplemented")), /* self.listing.lookup(field, */
|
||||
// tail),
|
||||
"notifications" => Err(Error::new("unimplemented")), /* self.notifications.lookup(field, tail), */
|
||||
"shortcuts" => Err(Error::new("unimplemented")), /* self.shortcuts. */
|
||||
// lookup(field,
|
||||
// tail),
|
||||
"composing" => Err(Error::new("unimplemented")), /* self.composing. */
|
||||
// lookup(field, tail),
|
||||
"identity" => Err(Error::new("unimplemented")), /* self.identity. */
|
||||
// lookup(field,
|
||||
// tail)<String>,
|
||||
"tags" => Err(Error::new("unimplemented")), /* self.tags.lookup(field, */
|
||||
// tail),
|
||||
"themes" => Err(Error::new("unimplemented")), /* self.themes. */
|
||||
// lookup(field,
|
||||
// tail)<Themes>,
|
||||
"pgp" => Err(Error::new("unimplemented")), //self.pgp.lookup(field, tail),
|
||||
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for FileMailboxConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"conf_override" => self.conf_override.lookup(field, tail),
|
||||
"mailbox_conf" => self.mailbox_conf.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for FileAccount {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"root_mailbox" => self.root_mailbox.lookup(field, tail),
|
||||
"format" => self.format.lookup(field, tail),
|
||||
"identity" => self.identity.lookup(field, tail),
|
||||
"display_name" => self.display_name.lookup(field, tail),
|
||||
"read_only" => self.read_only.lookup(field, tail),
|
||||
"subscribed_mailboxes" => self.subscribed_mailboxes.lookup(field, tail),
|
||||
"mailboxes" => self.mailboxes.lookup(field, tail),
|
||||
"search_backend" => self.search_backend.lookup(field, tail),
|
||||
"manual_refresh" => self.manual_refresh.lookup(field, tail),
|
||||
"refresh_command" => self.refresh_command.lookup(field, tail),
|
||||
"conf_override" => self.conf_override.lookup(field, tail),
|
||||
"extra" => self.extra.lookup(field, tail),
|
||||
"order" => self.order.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for melib::AccountSettings {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"name" => self.name.lookup(field, tail),
|
||||
"root_mailbox" => self.root_mailbox.lookup(field, tail),
|
||||
"format" => self.format.lookup(field, tail),
|
||||
"identity" => self.identity.lookup(field, tail),
|
||||
"read_only" => self.read_only.lookup(field, tail),
|
||||
"display_name" => self.display_name.lookup(field, tail),
|
||||
"subscribed_mailboxes" => self.subscribed_mailboxes.lookup(field, tail),
|
||||
"mailboxes" => self.mailboxes.lookup(field, tail),
|
||||
"manual_refresh" => self.manual_refresh.lookup(field, tail),
|
||||
"extra" => self.extra.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for melib::MailboxConf {
|
||||
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
|
||||
match path.first() {
|
||||
Some(field) => {
|
||||
let tail = &path[1..];
|
||||
match *field {
|
||||
"alias" => self.alias.lookup(field, tail),
|
||||
"autoload" => self.autoload.lookup(field, tail),
|
||||
"subscribe" => self.subscribe.lookup(field, tail),
|
||||
"ignore" => self.ignore.lookup(field, tail),
|
||||
"usage" => self.usage.lookup(field, tail),
|
||||
"extra" => self.extra.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
163
meli/src/conf/data_types/mod.rs
Normal file
163
meli/src/conf/data_types/mod.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
pub mod dotaddressable;
|
||||
pub mod regex_pattern;
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum IndexStyle {
|
||||
Plain,
|
||||
Threaded,
|
||||
#[default]
|
||||
Compact,
|
||||
Conversations,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for IndexStyle {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
plain if plain.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
|
||||
threaded if threaded.eq_ignore_ascii_case("threaded") => Ok(Self::Threaded),
|
||||
compact if compact.eq_ignore_ascii_case("compact") => Ok(Self::Compact),
|
||||
conversations if conversations.eq_ignore_ascii_case("conversations") => {
|
||||
Ok(Self::Conversations)
|
||||
}
|
||||
_ => Err(de::Error::custom(
|
||||
"invalid `index_style` value, expected one of: \"plain\", \"threaded\", \
|
||||
\"compact\" or \"conversations\".",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for IndexStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Plain => serializer.serialize_str("plain"),
|
||||
Self::Threaded => serializer.serialize_str("threaded"),
|
||||
Self::Compact => serializer.serialize_str("compact"),
|
||||
Self::Conversations => serializer.serialize_str("conversations"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum SearchBackend {
|
||||
None,
|
||||
#[default]
|
||||
Auto,
|
||||
#[cfg(feature = "sqlite3")]
|
||||
Sqlite3,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SearchBackend {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
#[cfg(feature = "sqlite3")]
|
||||
sqlite3
|
||||
if sqlite3.eq_ignore_ascii_case("sqlite3")
|
||||
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
|
||||
{
|
||||
Ok(Self::Sqlite3)
|
||||
}
|
||||
none if none.eq_ignore_ascii_case("none")
|
||||
|| none.eq_ignore_ascii_case("nothing")
|
||||
|| none.is_empty() =>
|
||||
{
|
||||
Ok(Self::None)
|
||||
}
|
||||
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
|
||||
_ => Err(de::Error::custom(if cfg!(feature = "sqlite3") {
|
||||
"invalid `search_backend` value, expected one of: \"sqlite3\", \"sqlite\", \
|
||||
\"none\" or \"auto\"."
|
||||
} else {
|
||||
"invalid `search_backend` value, expected one of: \"none\" or \"auto\"."
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SearchBackend {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
#[cfg(feature = "sqlite3")]
|
||||
Self::Sqlite3 => serializer.serialize_str("sqlite3"),
|
||||
Self::None => serializer.serialize_str("none"),
|
||||
Self::Auto => serializer.serialize_str("auto"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum ThreadLayout {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
#[default]
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ThreadLayout {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
vertical if vertical.eq_ignore_ascii_case("vertical") => Ok(Self::Vertical),
|
||||
horizontal if horizontal.eq_ignore_ascii_case("horizontal") => Ok(Self::Horizontal),
|
||||
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
|
||||
_ => Err(de::Error::custom(
|
||||
"invalid `thread_layout` value, expected one of: \"vertical\", \"horizontal\" or \
|
||||
\"auto\".",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ThreadLayout {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Vertical => serializer.serialize_str("vertical"),
|
||||
Self::Horizontal => serializer.serialize_str("horizontal"),
|
||||
Self::Auto => serializer.serialize_str("auto"),
|
||||
}
|
||||
}
|
||||
}
|
215
meli/src/conf/data_types/regex_pattern.rs
Normal file
215
meli/src/conf/data_types/regex_pattern.rs
Normal file
|
@ -0,0 +1,215 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use melib::error::{Result, WrapResultIntoError};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
const fn lf_val() -> u8 {
|
||||
b'\n'
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RegexValue {
|
||||
Default {
|
||||
pattern: regex::Regex,
|
||||
},
|
||||
Builder {
|
||||
pattern: regex::Regex,
|
||||
options: RegexOptions,
|
||||
},
|
||||
}
|
||||
|
||||
impl RegexValue {
|
||||
pub fn new_with_options(pattern: &str, o: RegexOptions) -> Result<Self> {
|
||||
let mut b = regex::RegexBuilder::new(pattern);
|
||||
b.unicode(o.unicode)
|
||||
.case_insensitive(o.case_insensitive)
|
||||
.multi_line(o.multi_line)
|
||||
.dot_matches_new_line(o.dot_matches_new_line)
|
||||
.crlf(o.crlf)
|
||||
.line_terminator(o.line_terminator)
|
||||
.swap_greed(o.swap_greed)
|
||||
.ignore_whitespace(o.ignore_whitespace)
|
||||
.octal(o.octal);
|
||||
if let Some(v) = o.size_limit {
|
||||
b.size_limit(v);
|
||||
}
|
||||
let pattern = b
|
||||
.build()
|
||||
.wrap_err(|| format!("Could not compile regular expression `{}`", pattern))?;
|
||||
Ok(Self::Builder {
|
||||
pattern,
|
||||
options: o,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_iter<'w, 's>(&'w self, s: &'s str) -> FindIter<'w, 's> {
|
||||
let (Self::Default { pattern } | Self::Builder { pattern, .. }) = self;
|
||||
FindIter {
|
||||
iter: pattern.find_iter(s),
|
||||
char_indices: s.char_indices(),
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct RegexOptions {
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
unicode: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
case_insensitive: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
multi_line: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
dot_matches_new_line: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
crlf: bool,
|
||||
#[serde(default = "lf_val")]
|
||||
line_terminator: u8,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
swap_greed: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
ignore_whitespace: bool,
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
octal: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
size_limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for RegexOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
unicode: true,
|
||||
case_insensitive: false,
|
||||
multi_line: false,
|
||||
dot_matches_new_line: false,
|
||||
crlf: false,
|
||||
line_terminator: b'\n',
|
||||
swap_greed: false,
|
||||
ignore_whitespace: false,
|
||||
octal: false,
|
||||
size_limit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RegexValue {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// [ref:FIXME]: clippy false positive, remove when resolved.
|
||||
#![allow(clippy::collection_is_never_read)]
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Inner<'a> {
|
||||
Default {
|
||||
pattern: &'a str,
|
||||
},
|
||||
Builder {
|
||||
pattern: &'a str,
|
||||
#[serde(flatten)]
|
||||
o: RegexOptions,
|
||||
},
|
||||
}
|
||||
let s = <Inner>::deserialize(deserializer);
|
||||
Ok(
|
||||
match s.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
r#"expected one of "true", "false", "ask", found `{}`"#,
|
||||
err
|
||||
))
|
||||
})? {
|
||||
Inner::Default { pattern } => Self::Default {
|
||||
pattern: regex::Regex::new(pattern).map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Could not compile regular expression `{}`: {}",
|
||||
pattern, err
|
||||
))
|
||||
})?,
|
||||
},
|
||||
Inner::Builder { pattern, o } => {
|
||||
let mut b = regex::RegexBuilder::new(pattern);
|
||||
b.unicode(o.unicode)
|
||||
.case_insensitive(o.case_insensitive)
|
||||
.multi_line(o.multi_line)
|
||||
.dot_matches_new_line(o.dot_matches_new_line)
|
||||
.crlf(o.crlf)
|
||||
.line_terminator(o.line_terminator)
|
||||
.swap_greed(o.swap_greed)
|
||||
.ignore_whitespace(o.ignore_whitespace)
|
||||
.octal(o.octal);
|
||||
if let Some(v) = o.size_limit {
|
||||
b.size_limit(v);
|
||||
}
|
||||
let pattern = b.build().map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Could not compile regular expression `{}`: {}",
|
||||
pattern, err
|
||||
))
|
||||
})?;
|
||||
Self::Builder {
|
||||
pattern,
|
||||
options: o,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FindIter<'r, 's> {
|
||||
iter: regex::Matches<'r, 's>,
|
||||
char_indices: std::str::CharIndices<'s>,
|
||||
char_offset: usize,
|
||||
}
|
||||
|
||||
impl Iterator for FindIter<'_, '_> {
|
||||
type Item = (usize, usize);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let next_byte_offset = self.iter.next()?;
|
||||
|
||||
let mut next_char_index = self.char_indices.next()?;
|
||||
|
||||
while next_byte_offset.start() < next_char_index.0 {
|
||||
self.char_offset += 1;
|
||||
next_char_index = self.char_indices.next()?;
|
||||
}
|
||||
let start = self.char_offset;
|
||||
|
||||
while next_byte_offset.end()
|
||||
> self
|
||||
.char_indices
|
||||
.next()
|
||||
.map(|(v, _)| v)
|
||||
.unwrap_or_else(|| next_byte_offset.end())
|
||||
{
|
||||
self.char_offset += 1;
|
||||
}
|
||||
let end = self.char_offset + 1;
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
}
|
65
meli/src/conf/default_values.rs
Normal file
65
meli/src/conf/default_values.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
//! default value functions for deserializing
|
||||
|
||||
pub fn false_val<T: From<bool>>() -> T {
|
||||
false.into()
|
||||
}
|
||||
|
||||
pub fn true_val<T: From<bool>>() -> T {
|
||||
true.into()
|
||||
}
|
||||
|
||||
pub fn zero_val<T: From<usize>>() -> T {
|
||||
0.into()
|
||||
}
|
||||
|
||||
pub fn eighty_val<T: From<usize>>() -> T {
|
||||
80.into()
|
||||
}
|
||||
|
||||
pub fn none<T>() -> Option<T> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn internal_value_false<T: From<melib::conf::ToggleFlag>>() -> T {
|
||||
melib::conf::ToggleFlag::InternalVal(false).into()
|
||||
}
|
||||
|
||||
pub fn internal_value_true<T: From<melib::conf::ToggleFlag>>() -> T {
|
||||
melib::conf::ToggleFlag::InternalVal(true).into()
|
||||
}
|
||||
|
||||
pub fn action_internal_value_false<T: From<melib::ActionFlag>>() -> T {
|
||||
melib::conf::ActionFlag::InternalVal(false).into()
|
||||
}
|
||||
|
||||
//pub fn action_internal_value_true<
|
||||
// T: From<melib::conf::ActionFlag>,
|
||||
//>() -> T {
|
||||
// melib::conf::ActionFlag::InternalVal(true).into()
|
||||
//}
|
||||
|
||||
pub fn ask<T: From<melib::conf::ActionFlag>>() -> T {
|
||||
melib::conf::ActionFlag::Ask.into()
|
||||
}
|
|
@ -19,9 +19,13 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::{search::Query, Error, Result};
|
||||
use melib::{search::Query, Error, Result, ToggleFlag};
|
||||
|
||||
use super::{default_vals::*, DotAddressable, IndexStyle};
|
||||
use crate::conf::{
|
||||
data_types::{IndexStyle, ThreadLayout},
|
||||
default_values::*,
|
||||
DotAddressable,
|
||||
};
|
||||
|
||||
/// Settings for mail listings
|
||||
///
|
||||
|
@ -55,7 +59,7 @@ use super::{default_vals::*, DotAddressable, IndexStyle};
|
|||
/// const HAS_SIBLING_LEAF: &str = " ├─";
|
||||
/// const NO_SIBLING_LEAF: &str = " ╰─";
|
||||
/// ```
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ListingSettings {
|
||||
/// Number of context lines when going to next page.
|
||||
|
@ -130,6 +134,17 @@ pub struct ListingSettings {
|
|||
#[serde(default)]
|
||||
pub attachment_flag: Option<String>,
|
||||
|
||||
/// Flag to show if any thread entry contains your address as a receiver.
|
||||
/// Useful to make mailing list threads that CC you stand out.
|
||||
/// Default: "✸"
|
||||
#[serde(default)]
|
||||
pub highlight_self_flag: Option<String>,
|
||||
|
||||
/// Show `highlight_self_flag` or not.
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub highlight_self: ToggleFlag,
|
||||
|
||||
/// Should threads with different Subjects show a list of those
|
||||
/// subjects on the entry title?
|
||||
/// Default: "true"
|
||||
|
@ -156,6 +171,14 @@ pub struct ListingSettings {
|
|||
/// Hide sidebar on launch. Default: "false"
|
||||
#[serde(default = "false_val", alias = "hide-sidebar-on-launch")]
|
||||
pub hide_sidebar_on_launch: bool,
|
||||
|
||||
/// Default: ' '
|
||||
#[serde(default = "default_divider")]
|
||||
pub mail_view_divider: char,
|
||||
|
||||
/// Default: "auto"
|
||||
#[serde(default)]
|
||||
pub thread_layout: ThreadLayout,
|
||||
}
|
||||
|
||||
const fn default_divider() -> char {
|
||||
|
@ -185,11 +208,15 @@ impl Default for ListingSettings {
|
|||
thread_snoozed_flag: None,
|
||||
selected_flag: None,
|
||||
attachment_flag: None,
|
||||
highlight_self_flag: None,
|
||||
highlight_self: ToggleFlag::Unset,
|
||||
thread_subject_pack: true,
|
||||
threaded_repeat_identical_from_values: false,
|
||||
relative_menu_indices: true,
|
||||
relative_list_indices: true,
|
||||
hide_sidebar_on_launch: false,
|
||||
mail_view_divider: default_divider(),
|
||||
thread_layout: ThreadLayout::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,6 +251,8 @@ impl DotAddressable for ListingSettings {
|
|||
"thread_snoozed_flag" => self.thread_snoozed_flag.lookup(field, tail),
|
||||
"selected_flag" => self.selected_flag.lookup(field, tail),
|
||||
"attachment_flag" => self.attachment_flag.lookup(field, tail),
|
||||
"highlight_self_flag" => self.highlight_self_flag.lookup(field, tail),
|
||||
"highlight_self" => self.highlight_self.lookup(field, tail),
|
||||
"thread_subject_pack" => self.thread_subject_pack.lookup(field, tail),
|
||||
"threaded_repeat_identical_from_values" => self
|
||||
.threaded_repeat_identical_from_values
|
||||
|
@ -231,13 +260,17 @@ impl DotAddressable for ListingSettings {
|
|||
"relative_menu_indices" => self.relative_menu_indices.lookup(field, tail),
|
||||
"relative_list_indices" => self.relative_list_indices.lookup(field, tail),
|
||||
"hide_sidebar_on_launch" => self.hide_sidebar_on_launch.lookup(field, tail),
|
||||
"mail_view_divider" => self.mail_view_divider.lookup(field, tail),
|
||||
"thread_layout" => self.thread_layout.lookup(field, tail),
|
||||
other => Err(Error::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,13 @@
|
|||
|
||||
use melib::{Error, Result, ToggleFlag};
|
||||
|
||||
use super::{
|
||||
default_vals::{internal_value_false, none, true_val},
|
||||
use crate::conf::{
|
||||
default_values::{internal_value_false, none, true_val},
|
||||
DotAddressable,
|
||||
};
|
||||
|
||||
/// Settings for the notifications function.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NotificationsSettings {
|
||||
/// Enable notifications.
|
||||
|
@ -88,7 +88,9 @@ impl DotAddressable for NotificationsSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -21,12 +21,13 @@
|
|||
|
||||
//! Settings for the pager function.
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{Error, Result, ToggleFlag};
|
||||
|
||||
use super::{default_vals::*, deserializers::*, DotAddressable};
|
||||
use crate::conf::{default_values::*, deserializers::*, DotAddressable};
|
||||
|
||||
/// Settings for the pager function.
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PagerSettings {
|
||||
/// Number of context lines when going to next page.
|
||||
|
@ -60,6 +61,12 @@ pub struct PagerSettings {
|
|||
#[serde(default = "none", deserialize_with = "non_empty_opt_string")]
|
||||
pub filter: Option<String>,
|
||||
|
||||
/// Named filter commands to use at will.
|
||||
///
|
||||
/// Default: empty
|
||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub named_filters: IndexMap<String, String>,
|
||||
|
||||
/// A command to pipe html output before displaying it in a pager
|
||||
/// Default: None
|
||||
#[serde(
|
||||
|
@ -126,6 +133,7 @@ impl Default for PagerSettings {
|
|||
sticky_headers: false,
|
||||
pager_ratio: 80,
|
||||
filter: None,
|
||||
named_filters: IndexMap::default(),
|
||||
html_filter: None,
|
||||
html_open: None,
|
||||
format_flowed: true,
|
||||
|
@ -150,6 +158,7 @@ impl DotAddressable for PagerSettings {
|
|||
"sticky_headers" => self.sticky_headers.lookup(field, tail),
|
||||
"pager_ratio" => self.pager_ratio.lookup(field, tail),
|
||||
"filter" => self.filter.lookup(field, tail),
|
||||
"named_filters" => self.named_filters.lookup(field, tail),
|
||||
"html_filter" => self.html_filter.lookup(field, tail),
|
||||
"html_open" => self.html_open.lookup(field, tail),
|
||||
"format_flowed" => self.format_flowed.lookup(field, tail),
|
||||
|
@ -167,7 +176,9 @@ impl DotAddressable for PagerSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,33 +19,33 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::conf::ToggleFlag;
|
||||
use melib::conf::ActionFlag;
|
||||
|
||||
use super::default_vals::*;
|
||||
use crate::conf::default_values::*;
|
||||
|
||||
/// Settings for digital signing and encryption
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PGPSettings {
|
||||
/// auto verify signed e-mail according to RFC3156
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "auto-verify-signatures")]
|
||||
pub auto_verify_signatures: bool,
|
||||
pub auto_verify_signatures: ActionFlag,
|
||||
|
||||
/// auto decrypt encrypted e-mail
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "auto-decrypt")]
|
||||
pub auto_decrypt: bool,
|
||||
pub auto_decrypt: ActionFlag,
|
||||
|
||||
/// always sign sent e-mail
|
||||
/// Default: false
|
||||
#[serde(default = "false_val", alias = "auto-sign")]
|
||||
pub auto_sign: bool,
|
||||
pub auto_sign: ActionFlag,
|
||||
|
||||
/// Auto encrypt sent e-mail
|
||||
/// Default: false
|
||||
#[serde(default = "false_val", alias = "auto-encrypt")]
|
||||
pub auto_encrypt: bool,
|
||||
pub auto_encrypt: ActionFlag,
|
||||
|
||||
// https://tools.ietf.org/html/rfc4880#section-12.2
|
||||
/// Default: None
|
||||
|
@ -60,10 +60,17 @@ pub struct PGPSettings {
|
|||
#[serde(default = "none", alias = "encrypt-key")]
|
||||
pub encrypt_key: Option<String>,
|
||||
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "encrypt-for-self")]
|
||||
pub encrypt_for_self: bool,
|
||||
|
||||
/// Allow remote lookups
|
||||
/// Default: None
|
||||
#[serde(default = "internal_value_false", alias = "allow-remote-lookups")]
|
||||
pub allow_remote_lookup: ToggleFlag,
|
||||
/// Default: False
|
||||
#[serde(
|
||||
default = "action_internal_value_false",
|
||||
alias = "allow-remote-lookups"
|
||||
)]
|
||||
pub allow_remote_lookup: ActionFlag,
|
||||
|
||||
/// Remote lookup mechanisms.
|
||||
/// Default: "local,wkd"
|
||||
|
@ -91,15 +98,16 @@ fn default_lookup_mechanism() -> melib::gpgme::LocateKey {
|
|||
|
||||
impl Default for PGPSettings {
|
||||
fn default() -> Self {
|
||||
PGPSettings {
|
||||
auto_verify_signatures: true,
|
||||
auto_decrypt: true,
|
||||
auto_sign: false,
|
||||
auto_encrypt: false,
|
||||
Self {
|
||||
auto_verify_signatures: true.into(),
|
||||
auto_decrypt: true.into(),
|
||||
auto_sign: false.into(),
|
||||
auto_encrypt: false.into(),
|
||||
encrypt_for_self: true,
|
||||
sign_key: None,
|
||||
decrypt_key: None,
|
||||
encrypt_key: None,
|
||||
allow_remote_lookup: internal_value_false::<ToggleFlag>(),
|
||||
allow_remote_lookup: action_internal_value_false::<ActionFlag>(),
|
||||
#[cfg(feature = "gpgme")]
|
||||
remote_lookup_mechanisms: default_lookup_mechanism(),
|
||||
#[cfg(not(feature = "gpgme"))]
|
||||
|
|
281
meli/src/conf/preprocessing.rs
Normal file
281
meli/src/conf/preprocessing.rs
Normal file
|
@ -0,0 +1,281 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
//! Preprocess configuration files by unfolding `include` macros.
|
||||
|
||||
use std::{
|
||||
io::{self, BufRead, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use melib::{
|
||||
error::{Error, ErrorKind, Result, ResultIntoError, WrapResultIntoError},
|
||||
utils::parsec::*,
|
||||
ShellExpandTrait,
|
||||
};
|
||||
|
||||
/// Try to parse line into a path to be included.
|
||||
pub fn include_directive<'a>() -> impl Parser<'a, Option<&'a str>> {
|
||||
move |input: &'a str| {
|
||||
enum State {
|
||||
Start,
|
||||
Path,
|
||||
}
|
||||
use State::*;
|
||||
let mut state = State::Start;
|
||||
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match (&state, input.as_bytes()[i]) {
|
||||
(Start, b'#') => {
|
||||
return Ok(("", None));
|
||||
}
|
||||
(Start, b) if (b as char).is_whitespace() => { /* consume */ }
|
||||
(Start, _) if input.as_bytes()[i..].starts_with(b"include(") => {
|
||||
i += "include(".len();
|
||||
state = Path;
|
||||
continue;
|
||||
}
|
||||
(Start, _) => {
|
||||
return Ok(("", None));
|
||||
}
|
||||
(Path, b'"') | (Path, b'\'') | (Path, b'`') => {
|
||||
let mut end = i + 1;
|
||||
while end < input.len() && input.as_bytes()[end] != input.as_bytes()[i] {
|
||||
end += 1;
|
||||
}
|
||||
if end == input.len() {
|
||||
return Err(input);
|
||||
}
|
||||
let ret = &input[i + 1..end];
|
||||
end += 1;
|
||||
if end < input.len() && input.as_bytes()[end] != b')' {
|
||||
/* Nothing else allowed in line */
|
||||
return Err(input);
|
||||
}
|
||||
end += 1;
|
||||
while end < input.len() {
|
||||
if !(input.as_bytes()[end] as char).is_whitespace() {
|
||||
/* Nothing else allowed in line */
|
||||
return Err(input);
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
return Ok(("", Some(ret)));
|
||||
}
|
||||
(Path, _) => return Err(input),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(("", None))
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands `include` macros in path.
|
||||
fn pp_helper(path: &Path, level: u8) -> Result<String> {
|
||||
if level > 7 {
|
||||
return Err(Error::new(format!(
|
||||
"Maximum recursion limit reached while unfolding include directives in {}. Have you \
|
||||
included a config file within itself?",
|
||||
path.display()
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
}
|
||||
let mut contents = String::new();
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
file.read_to_string(&mut contents)?;
|
||||
let mut ret = String::with_capacity(contents.len());
|
||||
|
||||
for (i, l) in contents.lines().enumerate() {
|
||||
if let (_, Some(sub_path)) = include_directive().parse(l).map_err(|l| {
|
||||
Error::new(format!(
|
||||
"Malformed include directive in line {} of file {}: {}\nConfiguration uses the \
|
||||
standard m4 macro include(\"filename\").",
|
||||
i,
|
||||
path.display(),
|
||||
l
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError)
|
||||
})? {
|
||||
let mut p = Path::new(sub_path).expand();
|
||||
if p.is_relative() {
|
||||
/* We checked that path is ok above so we can do unwrap here */
|
||||
let prefix = path.parent().unwrap();
|
||||
p = prefix.join(p)
|
||||
}
|
||||
|
||||
ret.push_str(&pp_helper(&p, level + 1).chain_err_related_path(&p)?);
|
||||
} else {
|
||||
ret.push_str(l);
|
||||
ret.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn pp_inner(path: &Path) -> Result<String> {
|
||||
let p_buf: PathBuf = if path.is_relative() {
|
||||
path.expand().canonicalize()?
|
||||
} else {
|
||||
path.expand()
|
||||
};
|
||||
|
||||
let mut ret = expand_config(&p_buf)?;
|
||||
if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("meli") {
|
||||
for theme_mailbox in xdg_dirs.find_config_files("themes") {
|
||||
let read_dir =
|
||||
std::fs::read_dir(&theme_mailbox).chain_err_related_path(&theme_mailbox)?;
|
||||
for theme in read_dir {
|
||||
let theme_path = theme?.path();
|
||||
if let Some(extension) = theme_path.extension() {
|
||||
if extension == "toml" {
|
||||
ret.push_str(
|
||||
&pp_helper(&theme_path, 0).chain_err_related_path(&theme_path)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Expands `include` macros in configuration file and other configuration
|
||||
/// files (eg. themes) in the filesystem.
|
||||
pub fn pp(path: &Path) -> Result<String> {
|
||||
pp_inner(path)
|
||||
.wrap_err(|| "Could not preprocess configuration file")
|
||||
.chain_err_related_path(path)
|
||||
.chain_err_kind(ErrorKind::Configuration)
|
||||
}
|
||||
|
||||
pub fn expand_config(conf_path: &Path) -> Result<String> {
|
||||
fn inner(conf_path: &Path) -> Result<String> {
|
||||
let _paths = get_included_configs(conf_path)?;
|
||||
const M4_PREAMBLE: &str = r#"define(`builtin_include', defn(`include'))dnl
|
||||
define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
|
||||
"#;
|
||||
let mut contents = String::new();
|
||||
contents.clear();
|
||||
let mut file = std::fs::File::open(conf_path)?;
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let mut handle = Command::new("m4")
|
||||
.current_dir(conf_path.parent().unwrap_or_else(|| Path::new("/")))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
let mut stdin = handle.stdin.take().unwrap();
|
||||
stdin.write_all(M4_PREAMBLE.as_bytes())?;
|
||||
stdin.write_all(contents.as_bytes())?;
|
||||
drop(stdin);
|
||||
let stdout = handle.wait_with_output()?.stdout;
|
||||
Ok(String::from_utf8_lossy(&stdout).to_string())
|
||||
}
|
||||
|
||||
inner(conf_path).chain_err_related_path(conf_path)
|
||||
}
|
||||
|
||||
pub fn get_included_configs(conf_path: &Path) -> Result<Vec<PathBuf>> {
|
||||
const M4_PREAMBLE: &str = r#"divert(-1)dnl
|
||||
define(`include', `divert(0)$1
|
||||
divert(-1)
|
||||
')dnl
|
||||
changequote(`"', `"')dnl
|
||||
"#;
|
||||
let mut ret = vec![];
|
||||
let prefix = conf_path.parent().unwrap().to_path_buf();
|
||||
let mut stack = vec![(None::<PathBuf>, conf_path.to_path_buf())];
|
||||
let mut contents = String::new();
|
||||
while let Some((parent, p)) = stack.pop() {
|
||||
if !p.exists() || p.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Path {}{included}{in_parent} {msg}.",
|
||||
p.display(),
|
||||
included = if parent.is_some() {
|
||||
" which is included in "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
in_parent = if let Some(parent) = parent {
|
||||
std::borrow::Cow::Owned(parent.display().to_string())
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed("")
|
||||
},
|
||||
msg = if !p.exists() {
|
||||
"does not exist"
|
||||
} else {
|
||||
"is a directory, not a text file"
|
||||
}
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
}
|
||||
contents.clear();
|
||||
let mut file = std::fs::File::open(&p).chain_err_related_path(&p)?;
|
||||
file.read_to_string(&mut contents)
|
||||
.chain_err_related_path(&p)?;
|
||||
|
||||
let mut handle = match Command::new("m4")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(handle) => handle,
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => {
|
||||
return Err(Error::new(
|
||||
"`m4` executable not found in PATH. Please provide an m4 binary.",
|
||||
)
|
||||
.set_kind(ErrorKind::Platform))
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new("Could not process configuration with `m4`")
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Platform))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut stdin = handle.stdin.take().unwrap();
|
||||
stdin.write_all(M4_PREAMBLE.as_bytes())?;
|
||||
stdin.write_all(contents.as_bytes())?;
|
||||
drop(stdin);
|
||||
let stdout = handle.wait_with_output()?.stdout.clone();
|
||||
for subpath in stdout.lines() {
|
||||
let subpath = subpath?;
|
||||
let path = &Path::new(&subpath);
|
||||
if path.is_absolute() {
|
||||
stack.push((Some(p.to_path_buf()), path.to_path_buf()));
|
||||
} else {
|
||||
stack.push((Some(p.to_path_buf()), prefix.join(path)));
|
||||
}
|
||||
}
|
||||
ret.push(p.to_path_buf());
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
|
@ -35,7 +35,7 @@ macro_rules! shortcut {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Shortcuts {
|
||||
#[serde(default)]
|
||||
|
@ -83,11 +83,19 @@ impl DotAddressable for Shortcuts {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CommandShortcut {
|
||||
pub shortcut: Key,
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
/// Create a struct holding all of a Component's shortcuts.
|
||||
#[macro_export]
|
||||
macro_rules! shortcut_key_values {
|
||||
|
@ -100,6 +108,7 @@ macro_rules! shortcut_key_values {
|
|||
#[serde(default)]
|
||||
#[serde(rename = $cname)]
|
||||
pub struct $name {
|
||||
pub commands: Vec<CommandShortcut>,
|
||||
$(pub $fname : Key),*
|
||||
}
|
||||
|
||||
|
@ -122,6 +131,7 @@ macro_rules! shortcut_key_values {
|
|||
impl Default for $name {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commands : vec![],
|
||||
$($fname: $default),*
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +150,7 @@ macro_rules! shortcut_key_values {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self).map_err(|err| err.to_string())?.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,21 +163,23 @@ shortcut_key_values! { "listing",
|
|||
scroll_up |> "Scroll up list." |> Key::Char('k'),
|
||||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
new_mail |> "Start new mail draft in new tab." |> Key::Char('m'),
|
||||
next_account |> "Go to next account." |> Key::Char('h'),
|
||||
next_account |> "Go to next account." |> Key::Char('H'),
|
||||
next_mailbox |> "Go to next mailbox." |> Key::Char('J'),
|
||||
next_page |> "Go to next page." |> Key::PageDown,
|
||||
prev_account |> "Go to previous account." |> Key::Char('l'),
|
||||
prev_account |> "Go to previous account." |> Key::Char('L'),
|
||||
prev_mailbox |> "Go to previous mailbox." |> Key::Char('K'),
|
||||
open_mailbox |> "Open selected mailbox" |> Key::Char('\n'),
|
||||
open_mailbox |> "Open selected mailbox." |> Key::Char('\n'),
|
||||
toggle_mailbox_collapse |> "Toggle mailbox collapse in menu." |> Key::Char(' '),
|
||||
prev_page |> "Go to previous page." |> Key::PageUp,
|
||||
search |> "Search within list of e-mails." |> Key::Char('/'),
|
||||
refresh |> "Manually request a mailbox refresh." |> Key::F(5),
|
||||
set_seen |> "Set thread as seen." |> Key::Char('n'),
|
||||
send_to_trash |> "Send entry to trash folder." |> Key::Char('D'),
|
||||
union_modifier |> "Union modifier." |> Key::Ctrl('u'),
|
||||
diff_modifier |> "Difference modifier." |> Key::Ctrl('d'),
|
||||
intersection_modifier |> "Intersection modifier." |> Key::Ctrl('i'),
|
||||
select_entry |> "Select thread entry." |> Key::Char('v'),
|
||||
select_entry |> "Select thread entry." |> Key::Char('V'),
|
||||
select_motion |> "Perform select motion with a movement." |> Key::Char('v'),
|
||||
increase_sidebar |> "Increase sidebar width." |> Key::Ctrl('f'),
|
||||
decrease_sidebar |> "Decrease sidebar width." |> Key::Ctrl('d'),
|
||||
next_entry |> "Focus on next entry." |> Key::Ctrl('n'),
|
||||
|
@ -187,10 +199,11 @@ shortcut_key_values! { "contact-list",
|
|||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
create_contact |> "Create new contact." |> Key::Char('c'),
|
||||
edit_contact |> "Edit contact under cursor." |> Key::Char('e'),
|
||||
export_contact |> "Export contact under cursor to .vcf." |> Key::Char('E'),
|
||||
delete_contact |> "Delete contact under cursor." |> Key::Char('d'),
|
||||
mail_contact |> "Mail contact under cursor." |> Key::Char('m'),
|
||||
next_account |> "Go to next account." |> Key::Char('h'),
|
||||
prev_account |> "Go to previous account." |> Key::Char('l'),
|
||||
next_account |> "Go to next account." |> Key::Char('H'),
|
||||
prev_account |> "Go to previous account." |> Key::Char('L'),
|
||||
toggle_menu_visibility |> "Toggle visibility of side menu in mail list." |> Key::Char('`')
|
||||
}
|
||||
}
|
||||
|
@ -198,10 +211,11 @@ shortcut_key_values! { "contact-list",
|
|||
shortcut_key_values! { "pager",
|
||||
/// Shortcut listing for the text pager
|
||||
pub struct PagerShortcuts {
|
||||
page_down |> "Go to next pager page" |> Key::PageDown,
|
||||
page_up |> "Go to previous pager page" |> Key::PageUp,
|
||||
page_down |> "Go to next pager page." |> Key::PageDown,
|
||||
page_up |> "Go to previous pager page." |> Key::PageUp,
|
||||
scroll_down |> "Scroll down pager." |> Key::Char('j'),
|
||||
scroll_up |> "Scroll up pager." |> Key::Char('k')
|
||||
scroll_up |> "Scroll up pager." |> Key::Char('k'),
|
||||
select_filter |> "Select content filter." |> Key::Char('f')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,10 +224,10 @@ shortcut_key_values! { "general",
|
|||
toggle_help |> "Toggle help and shortcuts view." |> Key::Char('?'),
|
||||
enter_command_mode |> "Enter COMMAND mode." |> Key::Char(':'),
|
||||
quit |> "Quit meli." |> Key::Char('q'),
|
||||
go_to_tab |> "Go to the nth tab" |> Key::Alt('n'),
|
||||
next_tab |> "Next tab." |> Key::Char('T'),
|
||||
scroll_right |> "Generic scroll right (catch-all setting)" |> Key::Right,
|
||||
scroll_left |> "Generic scroll left (catch-all setting)" |> Key::Left,
|
||||
go_to_tab |> "Go to the nth tab." |> Key::Alt('n'),
|
||||
next_tab |> "Go to the next tab." |> Key::Char('T'),
|
||||
scroll_right |> "Generic scroll right (catch-all setting)" |> Key::Char('l'),
|
||||
scroll_left |> "Generic scroll left (catch-all setting)" |>Key::Char('h'),
|
||||
scroll_up |> "Generic scroll up (catch-all setting)" |> Key::Char('k'),
|
||||
scroll_down |> "Generic scroll down (catch-all setting)" |> Key::Char('j'),
|
||||
next_page |> "Go to next page. (catch-all setting)" |> Key::PageDown,
|
||||
|
@ -221,16 +235,18 @@ shortcut_key_values! { "general",
|
|||
home_page |> "Go to first page. (catch-all setting)" |> Key::Home,
|
||||
end_page |> "Go to last page. (catch-all setting)" |> Key::End,
|
||||
open_entry |> "Open list entry. (catch-all setting)" |> Key::Char('\n'),
|
||||
info_message_next |> "Show next info message, if any" |> Key::Alt('>'),
|
||||
info_message_previous |> "Show previous info message, if any" |> Key::Alt('<'),
|
||||
focus_in_text_field |> "Focus on a text field." |> Key::Char('\n')
|
||||
info_message_next |> "Show next info message, if any." |> Key::Char('>'),
|
||||
info_message_previous |> "Show previous info message, if any." |> Key::Char('<'),
|
||||
focus_in_text_field |> "Focus on a text field." |> Key::Char('\n'),
|
||||
next_search_result |> "Scroll to next search result." |> Key::Char('n'),
|
||||
previous_search_result |> "Scroll to previous search result." |> Key::Char('N')
|
||||
}
|
||||
}
|
||||
|
||||
shortcut_key_values! { "composing",
|
||||
pub struct ComposingShortcuts {
|
||||
edit |> "Edit." |> Key::Char('e'),
|
||||
send_mail |> "Deliver draft to mailer" |> Key::Char('s'),
|
||||
send_mail |> "Deliver draft to mailer." |> Key::Char('s'),
|
||||
scroll_up |> "Change field focus." |> Key::Char('k'),
|
||||
scroll_down |> "Change field focus." |> Key::Char('j')
|
||||
}
|
||||
|
@ -240,9 +256,10 @@ shortcut_key_values! { "envelope-view",
|
|||
pub struct EnvelopeViewShortcuts {
|
||||
add_addresses_to_contacts |> "Select addresses from envelope to add to contacts." |> Key::Char('c'),
|
||||
edit |> "Open envelope in composer." |> Key::Char('e'),
|
||||
go_to_url |> "Go to url of given index" |> Key::Char('g'),
|
||||
go_to_url |> "Go to url of given index." |> Key::Char('g'),
|
||||
open_attachment |> "Opens selected attachment with xdg-open." |> Key::Char('a'),
|
||||
open_mailcap |> "Opens selected attachment according to its mailcap entry." |> Key::Char('m'),
|
||||
open_html |> "Opens html attachment in the default browser." |> Key::Char('v'),
|
||||
reply |> "Reply to envelope." |> Key::Char('R'),
|
||||
reply_to_author |> "Reply to author." |> Key::Ctrl('r'),
|
||||
reply_to_all |> "Reply to all/Reply to list/Follow up." |> Key::Ctrl('g'),
|
||||
|
@ -259,12 +276,12 @@ shortcut_key_values! { "thread-view",
|
|||
pub struct ThreadViewShortcuts {
|
||||
scroll_up |> "Scroll up list." |> Key::Char('k'),
|
||||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
collapse_subtree |> "collapse thread branches" |> Key::Char('h'),
|
||||
collapse_subtree |> "collapse thread branches." |> Key::Char('h'),
|
||||
next_page |> "Go to next page." |> Key::PageDown,
|
||||
prev_page |> "Go to previous page." |> Key::PageUp,
|
||||
reverse_thread_order |> "reverse thread order" |> Key::Ctrl('r'),
|
||||
toggle_mailview |> "toggle mail view visibility" |> Key::Char('p'),
|
||||
toggle_threadview |> "toggle thread view visibility" |> Key::Char('t'),
|
||||
reverse_thread_order |> "reverse thread order." |> Key::Ctrl('r'),
|
||||
toggle_mailview |> "toggle mail view visibility." |> Key::Char('p'),
|
||||
toggle_threadview |> "toggle thread view visibility." |> Key::Char('t'),
|
||||
toggle_layout |> "Toggle between horizontal and vertical layout." |> Key::Char(' ')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,24 +21,22 @@
|
|||
|
||||
//! E-mail tag configuration and {de,}serializing.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use melib::{Error, Result, TagHash};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::DotAddressable;
|
||||
use crate::terminal::Color;
|
||||
use crate::{conf::DotAddressable, terminal::Color};
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Clone, Serialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TagsSettings {
|
||||
#[serde(default, deserialize_with = "tag_color_de")]
|
||||
pub colors: HashMap<TagHash, Color>,
|
||||
pub colors: IndexMap<TagHash, Color>,
|
||||
#[serde(default, deserialize_with = "tag_set_de", alias = "ignore-tags")]
|
||||
pub ignore_tags: HashSet<TagHash>,
|
||||
pub ignore_tags: IndexSet<TagHash>,
|
||||
}
|
||||
|
||||
pub fn tag_set_de<'de, D, T: std::convert::From<HashSet<TagHash>>>(
|
||||
pub fn tag_set_de<'de, D, T: std::convert::From<IndexSet<TagHash>>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
|
@ -47,11 +45,11 @@ where
|
|||
Ok(<Vec<String>>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|tag| TagHash::from_bytes(tag.as_bytes()))
|
||||
.collect::<HashSet<TagHash>>()
|
||||
.collect::<IndexSet<TagHash>>()
|
||||
.into())
|
||||
}
|
||||
|
||||
pub fn tag_color_de<'de, D, T: std::convert::From<HashMap<TagHash, Color>>>(
|
||||
pub fn tag_color_de<'de, D, T: std::convert::From<IndexMap<TagHash, Color>>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
|
@ -64,7 +62,7 @@ where
|
|||
C(Color),
|
||||
}
|
||||
|
||||
Ok(<HashMap<String, _Color>>::deserialize(deserializer)?
|
||||
Ok(<IndexMap<String, _Color>>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|(tag, color)| {
|
||||
(
|
||||
|
@ -75,7 +73,7 @@ where
|
|||
},
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<TagHash, Color>>()
|
||||
.collect::<IndexMap<TagHash, Color>>()
|
||||
.into())
|
||||
}
|
||||
|
||||
|
@ -93,7 +91,9 @@ impl DotAddressable for TagsSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ use melib::{Error, Result, ToggleFlag};
|
|||
use super::{deserializers::non_empty_opt_string, DotAddressable, Themes};
|
||||
|
||||
/// Settings for terminal display
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct TerminalSettings {
|
||||
/// light, dark
|
||||
|
@ -34,6 +34,9 @@ pub struct TerminalSettings {
|
|||
pub themes: Themes,
|
||||
pub ascii_drawing: bool,
|
||||
pub use_color: ToggleFlag,
|
||||
/// Try forcing text presentations of symbols and emoji as much as possible.
|
||||
/// Might not work on all non-text symbols and is experimental.
|
||||
pub force_text_presentation: ToggleFlag,
|
||||
/// Use mouse events. This will disable text selection, but you will be able
|
||||
/// to resize some widgets.
|
||||
/// Default: False
|
||||
|
@ -55,10 +58,11 @@ pub struct TerminalSettings {
|
|||
|
||||
impl Default for TerminalSettings {
|
||||
fn default() -> Self {
|
||||
TerminalSettings {
|
||||
Self {
|
||||
theme: "dark".to_string(),
|
||||
themes: Themes::default(),
|
||||
ascii_drawing: false,
|
||||
force_text_presentation: ToggleFlag::InternalVal(false),
|
||||
use_color: ToggleFlag::InternalVal(true),
|
||||
use_mouse: ToggleFlag::InternalVal(false),
|
||||
mouse_flag: Some("🖱️ ".to_string()),
|
||||
|
@ -70,15 +74,19 @@ impl Default for TerminalSettings {
|
|||
}
|
||||
|
||||
impl TerminalSettings {
|
||||
#[inline]
|
||||
pub fn use_color(&self) -> bool {
|
||||
/* Don't use color if
|
||||
* - Either NO_COLOR is set and user hasn't explicitly set use_colors or
|
||||
* - User has explicitly set use_colors to false
|
||||
*/
|
||||
!((std::env::var("NO_COLOR").is_ok()
|
||||
// Don't use color if
|
||||
// - Either NO_COLOR is set and user hasn't explicitly set use_colors or
|
||||
// - User has explicitly set use_colors to false
|
||||
!((std::env::var_os("NO_COLOR").is_some()
|
||||
&& (self.use_color.is_false() || self.use_color.is_internal()))
|
||||
|| (self.use_color.is_false() && !self.use_color.is_internal()))
|
||||
}
|
||||
|
||||
pub fn use_text_presentation(&self) -> bool {
|
||||
self.force_text_presentation.is_true() || !self.use_color()
|
||||
}
|
||||
}
|
||||
|
||||
impl DotAddressable for TerminalSettings {
|
||||
|
@ -90,6 +98,7 @@ impl DotAddressable for TerminalSettings {
|
|||
"theme" => self.theme.lookup(field, tail),
|
||||
"themes" => Err(Error::new("unimplemented")),
|
||||
"ascii_drawing" => self.ascii_drawing.lookup(field, tail),
|
||||
"force_text_presentation" => self.force_text_presentation.lookup(field, tail),
|
||||
"use_color" => self.use_color.lookup(field, tail),
|
||||
"use_mouse" => self.use_mouse.lookup(field, tail),
|
||||
"mouse_flag" => self.mouse_flag.lookup(field, tail),
|
||||
|
@ -104,24 +113,103 @@ impl DotAddressable for TerminalSettings {
|
|||
))),
|
||||
}
|
||||
}
|
||||
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
|
||||
None => Ok(toml::Value::try_from(self)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ProgressSpinnerSequence {
|
||||
Integer(usize),
|
||||
Custom {
|
||||
frames: Vec<String>,
|
||||
#[serde(default = "interval_ms_val")]
|
||||
interval_ms: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProgressSpinnerSequence {
|
||||
pub const fn interval_ms(&self) -> u64 {
|
||||
match self {
|
||||
Self::Integer(_) => interval_ms_val(),
|
||||
Self::Custom {
|
||||
frames: _,
|
||||
interval_ms,
|
||||
} => *interval_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
const fn interval_ms_val() -> u64 {
|
||||
crate::utilities::ProgressSpinner::INTERVAL_MS
|
||||
}
|
||||
|
||||
impl DotAddressable for ProgressSpinnerSequence {}
|
||||
|
||||
impl<'de> Deserialize<'de> for ProgressSpinnerSequence {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum Inner {
|
||||
Integer(usize),
|
||||
Frames(Vec<String>),
|
||||
Custom {
|
||||
frames: Vec<String>,
|
||||
#[serde(default = "interval_ms_val")]
|
||||
interval_ms: u64,
|
||||
},
|
||||
}
|
||||
let s = <Inner>::deserialize(deserializer)?;
|
||||
match s {
|
||||
Inner::Integer(i) => Ok(Self::Integer(i)),
|
||||
Inner::Frames(frames) => Ok(Self::Custom {
|
||||
frames,
|
||||
interval_ms: interval_ms_val(),
|
||||
}),
|
||||
Inner::Custom {
|
||||
frames,
|
||||
interval_ms,
|
||||
} => Ok(Self::Custom {
|
||||
frames,
|
||||
interval_ms,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ProgressSpinnerSequence {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Integer(i) => serializer.serialize_i64(*i as i64),
|
||||
Self::Custom {
|
||||
frames,
|
||||
interval_ms,
|
||||
} => {
|
||||
if *interval_ms == interval_ms_val() {
|
||||
use serde::ser::SerializeSeq;
|
||||
let mut seq = serializer.serialize_seq(Some(frames.len()))?;
|
||||
for element in frames {
|
||||
seq.serialize_element(element)?;
|
||||
}
|
||||
seq.end()
|
||||
} else {
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
map.serialize_entry("frames", frames)?;
|
||||
map.serialize_entry("interval_ms", interval_ms)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
384
meli/src/conf/tests.rs
Normal file
384
meli/src/conf/tests.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Write as FmtWrite,
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
conf::{themes::*, FileSettings},
|
||||
terminal::Color,
|
||||
};
|
||||
|
||||
pub struct ConfigFile {
|
||||
pub path: PathBuf,
|
||||
pub file: fs::File,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
pub fn new(
|
||||
content: &str,
|
||||
dir: &tempfile::TempDir,
|
||||
) -> std::result::Result<Self, std::io::Error> {
|
||||
let mut filename = String::with_capacity(2 * 16);
|
||||
for byte in melib::utils::random::random_u64().to_be_bytes() {
|
||||
write!(&mut filename, "{:02X}", byte).unwrap();
|
||||
}
|
||||
let mut path = dir.path().to_path_buf();
|
||||
path.push(&*filename);
|
||||
let mut file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.append(true)
|
||||
.open(&path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
Ok(Self { path, file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
pub const TEST_CONFIG: &str = r#"
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/path/to/root/mailbox"
|
||||
format = "Maildir"
|
||||
send_mail = 'false'
|
||||
listing.index_style = "Conversations" # or [plain, threaded, compact]
|
||||
identity="email@example.com"
|
||||
display_name = "Name"
|
||||
subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
|
||||
# Set mailbox-specific settings
|
||||
[accounts.account-name.mailboxes]
|
||||
"INBOX" = { rename="Inbox" }
|
||||
"drafts" = { rename="Drafts" }
|
||||
"foobar-devel" = { ignore = true } # don't show notifications for this mailbox
|
||||
|
||||
# Setting up an mbox account
|
||||
[accounts.mbox]
|
||||
root_mailbox = "/var/mail/username"
|
||||
format = "mbox"
|
||||
send_mail = 'false'
|
||||
listing.index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
"#;
|
||||
|
||||
pub const EXTRA_CONFIG: &str = r#"
|
||||
[accounts.mbox]
|
||||
root_mailbox = "/"
|
||||
format = "mbox"
|
||||
send_mail = 'false'
|
||||
index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
"#;
|
||||
pub const IMAP_CONFIG: &str = r#"
|
||||
[accounts.imap]
|
||||
root_mailbox = "INBOX"
|
||||
format = "imap"
|
||||
send_mail = 'false'
|
||||
identity="username@example.com"
|
||||
server_username = "null"
|
||||
server_hostname = "example.com"
|
||||
server_password_command = "false"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn test_conf_config_parse() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let new_file = ConfigFile::new(TEST_CONFIG, &tempdir).unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err();
|
||||
assert_eq!(
|
||||
err.summary.as_ref(),
|
||||
"Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a valid \
|
||||
directory."
|
||||
);
|
||||
|
||||
/* Test unrecognised configuration entries error */
|
||||
|
||||
let new_file = ConfigFile::new(EXTRA_CONFIG, &tempdir).unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err();
|
||||
assert_eq!(
|
||||
err.summary.as_ref(),
|
||||
"Unrecognised configuration values: {\"index_style\": \"Compact\"}"
|
||||
);
|
||||
|
||||
/* Test IMAP config */
|
||||
|
||||
let new_file = ConfigFile::new(IMAP_CONFIG, &tempdir).unwrap();
|
||||
FileSettings::validate(new_file.path.clone(), true).expect("could not parse IMAP config");
|
||||
|
||||
/* Test sample config */
|
||||
|
||||
// Sample config contains `crate::conf::composing::SendMail::Smtp` variant which
|
||||
// only exists if meli is build with `smtp` feature.
|
||||
if cfg!(feature = "smtp") {
|
||||
let example_config = FileSettings::EXAMPLE_CONFIG.replace("\n#", "\n");
|
||||
let re = regex::Regex::new(r#"root_mailbox\s*=\s*"[^"]*""#).unwrap();
|
||||
let example_config = re.replace_all(
|
||||
&example_config,
|
||||
&format!(r#"root_mailbox = "{}""#, tempdir.path().to_str().unwrap()),
|
||||
);
|
||||
|
||||
let new_file = ConfigFile::new(&example_config, &tempdir).unwrap();
|
||||
let config = FileSettings::validate(new_file.path.clone(), true)
|
||||
.expect("Could not parse example config!");
|
||||
for (accname, acc) in config.accounts.iter() {
|
||||
if !acc.extra.is_empty() {
|
||||
panic!(
|
||||
"In example config, account `{}` has unrecognised configuration entries: {:?}",
|
||||
accname, acc.extra
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = tempdir.close() {
|
||||
eprintln!("Could not cleanup tempdir: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conf_theme_parsing() {
|
||||
/* MUST SUCCEED: default themes should be valid */
|
||||
let def = Themes::default();
|
||||
def.validate().unwrap();
|
||||
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user
|
||||
* redefinitions */
|
||||
const TEST_STR: &str = r#"[dark]
|
||||
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
|
||||
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
|
||||
"mail.view.headers" = { bg = "mail.listing.tag_default.fg" }
|
||||
|
||||
["hunter2"]
|
||||
"mail.view.body" = { fg = "Black", bg = "White"}"#;
|
||||
let parsed: Themes = toml::from_str(TEST_STR).unwrap();
|
||||
assert!(parsed.other_themes.contains_key("hunter2"));
|
||||
assert_eq!(
|
||||
unlink_bg(
|
||||
&parsed.dark,
|
||||
&ColorField::Bg,
|
||||
&Cow::from("mail.listing.tag_default")
|
||||
),
|
||||
Color::Byte(132)
|
||||
);
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
&ColorField::Fg,
|
||||
&Cow::from("mail.listing.attachment_flag")
|
||||
),
|
||||
Color::Byte(132)
|
||||
);
|
||||
assert_eq!(
|
||||
unlink_bg(
|
||||
&parsed.dark,
|
||||
&ColorField::Bg,
|
||||
&Cow::from("mail.view.headers")
|
||||
),
|
||||
Color::Byte(15), // White
|
||||
);
|
||||
parsed.validate().unwrap();
|
||||
/* MUST FAIL: theme `dark` contains a cycle */
|
||||
const HAS_CYCLE: &str = r#"[dark]
|
||||
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
|
||||
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: theme `dark` contains an invalid key */
|
||||
const HAS_INVALID_KEYS: &str = r#"[dark]
|
||||
"asdfsafsa" = { fg = "Black" }
|
||||
"#;
|
||||
let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
|
||||
parsed.unwrap_err();
|
||||
/* MUST SUCCEED: alias $Jebediah resolves to a valid color */
|
||||
const TEST_ALIAS_STR: &str = r##"[dark]
|
||||
color_aliases= { "Jebediah" = "#b4da55" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah" }
|
||||
"##;
|
||||
let parsed: Themes = toml::from_str(TEST_ALIAS_STR).unwrap();
|
||||
parsed.validate().unwrap();
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
&ColorField::Fg,
|
||||
&Cow::from("mail.listing.tag_default")
|
||||
),
|
||||
Color::Rgb(180, 218, 85)
|
||||
);
|
||||
/* MUST FAIL: Misspell color alias $Jebediah as $Jebedia */
|
||||
const TEST_INVALID_ALIAS_STR: &str = r##"[dark]
|
||||
color_aliases= { "Jebediah" = "#b4da55" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebedia" }
|
||||
"##;
|
||||
let parsed: Themes = toml::from_str(TEST_INVALID_ALIAS_STR).unwrap();
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: Color alias $Jebediah is defined as itself */
|
||||
const TEST_CYCLIC_ALIAS_STR: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$Jebediah" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR).unwrap();
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: Attr alias $Jebediah is defined as itself */
|
||||
const TEST_CYCLIC_ALIAS_ATTR_STR: &str = r#"[dark]
|
||||
attr_aliases= { "Jebediah" = "$Jebediah" }
|
||||
"mail.listing.tag_default" = { attrs = "$Jebediah" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_ATTR_STR).unwrap();
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: alias $Jebediah resolves to a cycle */
|
||||
const TEST_CYCLIC_ALIAS_STR_2: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_2).unwrap();
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST SUCCEED: alias $Jebediah resolves to a key's field */
|
||||
const TEST_CYCLIC_ALIAS_STR_3: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.bg" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_3).unwrap();
|
||||
parsed.validate().unwrap();
|
||||
/* MUST FAIL: alias $Jebediah resolves to an invalid key */
|
||||
const TEST_INVALID_LINK_KEY_FIELD_STR: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.attrs" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_INVALID_LINK_KEY_FIELD_STR).unwrap();
|
||||
parsed.validate().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conf_theme_key_values() {
|
||||
use std::{collections::VecDeque, fs::File, io::Read, path::PathBuf};
|
||||
let mut rust_files: VecDeque<PathBuf> = VecDeque::new();
|
||||
let mut dirs_queue: VecDeque<PathBuf> = VecDeque::new();
|
||||
dirs_queue.push_back("src/".into());
|
||||
let re_whitespace = regex::Regex::new(r"\s*").unwrap();
|
||||
let re_conf = regex::Regex::new(r#"value\([&]?context,"([^"]*)""#).unwrap();
|
||||
|
||||
while let Some(dir) = dirs_queue.pop_front() {
|
||||
for entry in std::fs::read_dir(&dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
dirs_queue.push_back(path);
|
||||
} else if path.extension().map(|os_s| os_s == "rs").unwrap_or(false) {
|
||||
rust_files.push_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for file_path in rust_files {
|
||||
let mut file = File::open(&file_path).unwrap();
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
let content = re_whitespace.replace_all(&content, "");
|
||||
for mat in re_conf.captures_iter(&content) {
|
||||
let theme_key = &mat[1];
|
||||
if !DEFAULT_KEYS.contains(&theme_key) {
|
||||
panic!(
|
||||
"Source file {} contains a hardcoded theme key str, {:?}, that is not \
|
||||
included in the DEFAULT_KEYS table.",
|
||||
file_path.display(),
|
||||
theme_key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conf_progress_spinner_sequence() {
|
||||
use crate::{conf::terminal::ProgressSpinnerSequence, utilities::ProgressSpinner};
|
||||
|
||||
let int_0 = ProgressSpinnerSequence::Integer(5);
|
||||
assert_eq!(
|
||||
toml::Value::try_from(&int_0).unwrap(),
|
||||
toml::Value::try_from(5).unwrap()
|
||||
);
|
||||
|
||||
let frames = ProgressSpinnerSequence::Custom {
|
||||
frames: vec![
|
||||
"⠁".to_string(),
|
||||
"⠂".to_string(),
|
||||
"⠄".to_string(),
|
||||
"⡀".to_string(),
|
||||
"⢀".to_string(),
|
||||
"⠠".to_string(),
|
||||
"⠐".to_string(),
|
||||
"⠈".to_string(),
|
||||
],
|
||||
interval_ms: ProgressSpinner::INTERVAL_MS,
|
||||
};
|
||||
assert_eq!(frames.interval_ms(), ProgressSpinner::INTERVAL_MS);
|
||||
assert_eq!(
|
||||
toml::Value::try_from(&frames).unwrap(),
|
||||
toml::Value::try_from(["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]).unwrap()
|
||||
);
|
||||
let frames = ProgressSpinnerSequence::Custom {
|
||||
frames: vec![
|
||||
"⠁".to_string(),
|
||||
"⠂".to_string(),
|
||||
"⠄".to_string(),
|
||||
"⡀".to_string(),
|
||||
"⢀".to_string(),
|
||||
"⠠".to_string(),
|
||||
"⠐".to_string(),
|
||||
"⠈".to_string(),
|
||||
],
|
||||
interval_ms: ProgressSpinner::INTERVAL_MS + 1,
|
||||
};
|
||||
assert_eq!(
|
||||
toml::Value::try_from(&frames).unwrap(),
|
||||
toml::Value::try_from(indexmap::indexmap! {
|
||||
"frames" => toml::Value::try_from(["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]).unwrap(),
|
||||
"interval_ms" => toml::Value::try_from(ProgressSpinner::INTERVAL_MS + 1).unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
toml::from_str::<ProgressSpinnerSequence>(
|
||||
r#"frames = ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]
|
||||
interval_ms = 51"#
|
||||
)
|
||||
.unwrap(),
|
||||
frames
|
||||
);
|
||||
assert_eq!(
|
||||
toml::from_str::<indexmap::IndexMap<String, ProgressSpinnerSequence>>(
|
||||
r#"sequence = { frames = ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"], interval_ms = 51 }"#
|
||||
)
|
||||
.unwrap(),
|
||||
indexmap::indexmap! {
|
||||
"sequence".to_string() => frames,
|
||||
},
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -19,13 +19,16 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::Card;
|
||||
|
||||
use crate::{
|
||||
terminal::*, Action::*, CellBuffer, Component, ComponentId, Context, Field, FormWidget, Key,
|
||||
StatusEvent, TabAction, ThemeAttribute, UIDialog, UIEvent,
|
||||
terminal::*,
|
||||
utilities::{FormButtonAction, FormWidget},
|
||||
CellBuffer, Component, ComponentId, Context, Field, Key, StatusEvent, ThemeAttribute, UIDialog,
|
||||
UIEvent,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -33,7 +36,6 @@ enum ViewMode {
|
|||
ReadOnly,
|
||||
Discard(Box<UIDialog<char>>),
|
||||
Edit,
|
||||
//New,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -42,13 +44,12 @@ pub struct ContactManager {
|
|||
parent_id: Option<ComponentId>,
|
||||
pub card: Card,
|
||||
mode: ViewMode,
|
||||
form: FormWidget<bool>,
|
||||
form: FormWidget<FormButtonAction>,
|
||||
pub account_pos: usize,
|
||||
content: CellBuffer,
|
||||
content: Screen<Virtual>,
|
||||
theme_default: ThemeAttribute,
|
||||
dirty: bool,
|
||||
has_changes: bool,
|
||||
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
|
@ -61,14 +62,14 @@ impl std::fmt::Display for ContactManager {
|
|||
impl ContactManager {
|
||||
pub fn new(context: &Context) -> Self {
|
||||
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
|
||||
ContactManager {
|
||||
Self {
|
||||
id: ComponentId::default(),
|
||||
parent_id: None,
|
||||
card: Card::new(),
|
||||
mode: ViewMode::Edit,
|
||||
form: FormWidget::default(),
|
||||
account_pos: 0,
|
||||
content: CellBuffer::new_with_context(100, 1, None, context),
|
||||
content: Screen::<Virtual>::new(theme_default),
|
||||
theme_default,
|
||||
dirty: true,
|
||||
has_changes: false,
|
||||
|
@ -77,48 +78,59 @@ impl ContactManager {
|
|||
}
|
||||
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
let (width, _) = self.content.size();
|
||||
|
||||
let (x, _) = write_string_to_grid(
|
||||
"Last edited: ",
|
||||
&mut self.content,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, 0), (width - 1, 0)),
|
||||
None,
|
||||
);
|
||||
let (x, y) = write_string_to_grid(
|
||||
&self.card.last_edited(),
|
||||
&mut self.content,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((x, 0), (width - 1, 0)),
|
||||
None,
|
||||
);
|
||||
if !self.content.resize_with_context(100, 1, context) {
|
||||
return;
|
||||
}
|
||||
let area = self.content.area();
|
||||
|
||||
if self.card.external_resource() {
|
||||
self.mode = ViewMode::ReadOnly;
|
||||
let _ = self.content.resize(self.content.size().0, 2, None);
|
||||
write_string_to_grid(
|
||||
self.content.grid_mut().write_string(
|
||||
"This contact's origin is external and cannot be edited within meli.",
|
||||
&mut self.content,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((x, y), (width - 1, y)),
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
let (x, _) = self.content.grid_mut().write_string(
|
||||
"Last edited: ",
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.content.grid_mut().write_string(
|
||||
&self.card.last_edited(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area.skip_cols(x),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
self.form = FormWidget::new(
|
||||
("Save".into(), true),
|
||||
if self.card.external_resource() {
|
||||
("Cancel(Esc)".into(), FormButtonAction::Cancel)
|
||||
} else {
|
||||
("Save".into(), FormButtonAction::Accept)
|
||||
},
|
||||
/* cursor_up_shortcut */ context.settings.shortcuts.general.scroll_up.clone(),
|
||||
/* cursor_down_shortcut */
|
||||
context.settings.shortcuts.general.scroll_down.clone(),
|
||||
);
|
||||
self.form.add_button(("Cancel(Esc)".into(), false));
|
||||
if !self.card.external_resource() {
|
||||
self.form
|
||||
.add_button(("Export".into(), FormButtonAction::Other("Export")));
|
||||
self.form
|
||||
.add_button(("Cancel(Esc)".into(), FormButtonAction::Cancel));
|
||||
}
|
||||
self.form
|
||||
.push(("NAME".into(), self.card.name().to_string()));
|
||||
self.form.push((
|
||||
|
@ -150,25 +162,14 @@ impl Component for ContactManager {
|
|||
self.initialized = true;
|
||||
}
|
||||
|
||||
let upper_left = upper_left!(area);
|
||||
let bottom_right = bottom_right!(area);
|
||||
|
||||
if self.dirty {
|
||||
let (width, _height) = self.content.size();
|
||||
clear_area(
|
||||
grid,
|
||||
(upper_left, set_y(bottom_right, get_y(upper_left) + 1)),
|
||||
self.theme_default,
|
||||
);
|
||||
copy_area_with_break(grid, &self.content, area, ((0, 0), (width - 1, 0)));
|
||||
if self.is_dirty() {
|
||||
grid.clear_area(area, self.theme_default);
|
||||
grid.copy_area(self.content.grid(), area, self.content.area());
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
self.form.draw(
|
||||
grid,
|
||||
(set_y(upper_left, get_y(upper_left) + 2), bottom_right),
|
||||
context,
|
||||
);
|
||||
self.form
|
||||
.draw(grid, area.skip_rows(self.content.area().height()), context);
|
||||
if let ViewMode::Discard(ref mut selector) = self.mode {
|
||||
/* Let user choose whether to quit with/without saving or cancel */
|
||||
selector.draw(grid, area, context);
|
||||
|
@ -180,32 +181,36 @@ impl Component for ContactManager {
|
|||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let UIEvent::ConfigReload { old_settings: _ } = event {
|
||||
self.theme_default = crate::conf::value(context, "theme_default");
|
||||
self.content = CellBuffer::new_with_context(100, 1, None, context);
|
||||
self.content.grid_mut().empty();
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
match self.mode {
|
||||
ViewMode::Discard(ref mut selector) => {
|
||||
if matches!(event, UIEvent::ComponentUnrealize(ref id) if *id == selector.id()) {
|
||||
selector.unrealize(context);
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ViewMode::Edit => {
|
||||
if let (Some(parent_id), &UIEvent::Input(Key::Esc)) = (self.parent_id, &event) {
|
||||
if matches!(event, UIEvent::Input(Key::Esc)) {
|
||||
if self.can_quit_cleanly(context) {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(Tab(TabAction::Kill(parent_id))));
|
||||
self.unrealize(context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if self.form.process_event(event, context) {
|
||||
match self.form.buttons_result() {
|
||||
None => {}
|
||||
Some(true) => {
|
||||
let fields = std::mem::take(&mut self.form).collect().unwrap();
|
||||
let fields: HashMap<String, String> = fields
|
||||
Some(FormButtonAction::Accept) => {
|
||||
let fields = std::mem::take(&mut self.form).collect();
|
||||
let fields: IndexMap<String, String> = fields
|
||||
.into_iter()
|
||||
.map(|(s, v)| {
|
||||
(
|
||||
|
@ -220,26 +225,53 @@ impl Component for ContactManager {
|
|||
let mut new_card = Card::from(fields);
|
||||
new_card.set_id(*self.card.id());
|
||||
context.accounts[self.account_pos]
|
||||
.address_book
|
||||
.contacts
|
||||
.add_card(new_card);
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage("Saved.".into()),
|
||||
));
|
||||
self.unrealize(context);
|
||||
}
|
||||
Some(false) => {
|
||||
Some(FormButtonAction::Cancel) => {
|
||||
self.unrealize(context);
|
||||
}
|
||||
Some(FormButtonAction::Other("Export")) => {
|
||||
let card = if self.has_changes {
|
||||
let fields = self.form.clone().collect();
|
||||
let fields: IndexMap<String, String> = fields
|
||||
.into_iter()
|
||||
.map(|(s, v)| {
|
||||
(
|
||||
s.to_string(),
|
||||
match v {
|
||||
Field::Text(v) => v.as_str().to_string(),
|
||||
Field::Choice(mut v, c, _) => {
|
||||
v.remove(c).to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut card = Card::from(fields);
|
||||
card.set_id(*self.card.id());
|
||||
Cow::Owned(card)
|
||||
} else {
|
||||
Cow::Borrowed(&self.card)
|
||||
};
|
||||
super::export_to_vcard(&card, self.account_pos, context);
|
||||
return true;
|
||||
}
|
||||
Some(FormButtonAction::Reset | FormButtonAction::Other(_)) => {}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
if let UIEvent::InsertInput(_) = event {
|
||||
if matches!(event, UIEvent::InsertInput(_)) {
|
||||
self.has_changes = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ViewMode::ReadOnly => {
|
||||
if let &mut UIEvent::Input(Key::Esc) = event {
|
||||
if matches!(event, UIEvent::Input(Key::Esc)) {
|
||||
if self.can_quit_cleanly(context) {
|
||||
self.unrealize(context);
|
||||
}
|
||||
|
@ -253,11 +285,7 @@ impl Component for ContactManager {
|
|||
fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
|| self.form.is_dirty()
|
||||
|| if let ViewMode::Discard(ref selector) = self.mode {
|
||||
selector.is_dirty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|| matches!(self.mode, ViewMode::Discard(ref selector) if selector.is_dirty())
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
|
@ -277,28 +305,31 @@ impl Component for ContactManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent_id) = self.parent_id {
|
||||
if matches!(self.mode, ViewMode::Discard(_)) {
|
||||
true
|
||||
} else {
|
||||
let Some(parent_id) = self.parent_id else {
|
||||
return true;
|
||||
};
|
||||
/* Play it safe and ask user for confirmation */
|
||||
self.mode = ViewMode::Discard(Box::new(UIDialog::new(
|
||||
"this contact has unsaved changes",
|
||||
vec![
|
||||
('x', "quit without saving".to_string()),
|
||||
('y', "save draft and quit".to_string()),
|
||||
('y', "quit without saving".to_string()),
|
||||
('n', "cancel".to_string()),
|
||||
],
|
||||
true,
|
||||
Some(Box::new(move |_, results: &[char]| match results[0] {
|
||||
'x' => Some(UIEvent::Action(Tab(TabAction::Kill(parent_id)))),
|
||||
'n' => None,
|
||||
'y' => None,
|
||||
_ => None,
|
||||
Some(Box::new(move |id, results: &[char]| {
|
||||
if matches!(results.first(), Some(&'y')) {
|
||||
Some(UIEvent::ComponentUnrealize(parent_id))
|
||||
} else {
|
||||
Some(UIEvent::ComponentUnrealize(id))
|
||||
}
|
||||
})),
|
||||
context,
|
||||
)));
|
||||
self.set_dirty(true);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
use std::cmp;
|
||||
|
||||
use melib::{backends::AccountHash, log, text_processing::TextProcessing, Card, CardId, Draft};
|
||||
use melib::{backends::AccountHash, text::TextProcessing, Card, CardId, Draft};
|
||||
|
||||
use crate::{
|
||||
conf, contacts::editor::ContactManager, shortcut, terminal::*, Action::Tab, Component,
|
||||
|
@ -29,10 +29,10 @@ use crate::{
|
|||
ShortcutMaps, Shortcuts, StatusEvent, TabAction, ThemeAttribute, UIEvent, UIMode,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
enum ViewMode {
|
||||
List,
|
||||
View(ComponentId),
|
||||
View(Box<ContactManager>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -66,7 +66,6 @@ pub struct ContactList {
|
|||
menu_visibility: bool,
|
||||
movement: Option<PageMovement>,
|
||||
cmd_buf: String,
|
||||
view: Option<ContactManager>,
|
||||
ratio: usize, // right/(container width) * 100
|
||||
id: ComponentId,
|
||||
}
|
||||
|
@ -89,7 +88,8 @@ impl ContactList {
|
|||
index: i,
|
||||
})
|
||||
.collect();
|
||||
ContactList {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
Self {
|
||||
accounts,
|
||||
cursor_pos: 0,
|
||||
new_cursor_pos: 0,
|
||||
|
@ -97,14 +97,13 @@ impl ContactList {
|
|||
account_pos: 0,
|
||||
id_positions: Vec::new(),
|
||||
mode: ViewMode::List,
|
||||
data_columns: DataColumns::default(),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
data_columns: DataColumns::new(theme_default),
|
||||
theme_default,
|
||||
highlight_theme: crate::conf::value(context, "highlight"),
|
||||
initialized: false,
|
||||
dirty: true,
|
||||
movement: None,
|
||||
cmd_buf: String::with_capacity(8),
|
||||
view: None,
|
||||
ratio: 90,
|
||||
sidebar_divider: context.settings.listing.sidebar_divider,
|
||||
sidebar_divider_theme: conf::value(context, "mail.sidebar_divider"),
|
||||
|
@ -114,144 +113,164 @@ impl ContactList {
|
|||
}
|
||||
|
||||
pub fn for_account(pos: usize, context: &Context) -> Self {
|
||||
ContactList {
|
||||
Self {
|
||||
account_pos: pos,
|
||||
..Self::new(context)
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &mut Context) {
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
self.data_columns.clear();
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let book = &account.address_book;
|
||||
self.length = book.len();
|
||||
let contacts = &account.contacts;
|
||||
self.length = contacts.len();
|
||||
|
||||
self.id_positions.clear();
|
||||
if self.id_positions.capacity() < book.len() {
|
||||
self.id_positions.reserve(book.len());
|
||||
if self.id_positions.capacity() < contacts.len() {
|
||||
self.id_positions.reserve(contacts.len());
|
||||
}
|
||||
self.dirty = true;
|
||||
let mut min_width = ("Name".len(), "E-mail".len(), 0, "external".len(), 0, 0);
|
||||
|
||||
for c in book.values() {
|
||||
for c in contacts.values() {
|
||||
/* name */
|
||||
min_width.0 = cmp::max(min_width.0, c.name().split_graphemes().len());
|
||||
let name = c.name().split_graphemes().len();
|
||||
if name > 0 {
|
||||
min_width.0 = cmp::max(min_width.0, name + 1);
|
||||
}
|
||||
/* email */
|
||||
min_width.1 = cmp::max(min_width.1, c.email().split_graphemes().len());
|
||||
let email = c.email().split_graphemes().len();
|
||||
if email > 0 {
|
||||
min_width.1 = cmp::max(min_width.1, email + 1);
|
||||
}
|
||||
/* url */
|
||||
min_width.2 = cmp::max(min_width.2, c.url().split_graphemes().len());
|
||||
let url = c.url().split_graphemes().len();
|
||||
if url > 0 {
|
||||
min_width.2 = cmp::max(min_width.2, url + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* name column */
|
||||
self.data_columns.columns[0] =
|
||||
CellBuffer::new_with_context(min_width.0, self.length, None, context);
|
||||
_ = self.data_columns.columns[0].resize_with_context(min_width.0, self.length, context);
|
||||
/* email column */
|
||||
self.data_columns.columns[1] =
|
||||
CellBuffer::new_with_context(min_width.1, self.length, None, context);
|
||||
_ = self.data_columns.columns[1].resize_with_context(min_width.1, self.length, context);
|
||||
/* url column */
|
||||
self.data_columns.columns[2] =
|
||||
CellBuffer::new_with_context(min_width.2, self.length, None, context);
|
||||
_ = self.data_columns.columns[2].resize_with_context(min_width.2, self.length, context);
|
||||
/* source column */
|
||||
self.data_columns.columns[3] =
|
||||
CellBuffer::new_with_context("external".len(), self.length, None, context);
|
||||
_ = self.data_columns.columns[3].resize_with_context(
|
||||
"external".len(),
|
||||
self.length,
|
||||
context,
|
||||
);
|
||||
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let book = &account.address_book;
|
||||
let mut book_values = book.values().collect::<Vec<&Card>>();
|
||||
let contacts = &account.contacts;
|
||||
let mut book_values = contacts.values().collect::<Vec<&Card>>();
|
||||
book_values.sort_unstable_by_key(|c| c.name());
|
||||
for (idx, c) in book_values.iter().enumerate() {
|
||||
self.id_positions.push(*c.id());
|
||||
|
||||
write_string_to_grid(
|
||||
c.name(),
|
||||
&mut self.data_columns.columns[0],
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.0, idx)),
|
||||
None,
|
||||
);
|
||||
{
|
||||
let area = self.data_columns.columns[0].area().nth_row(idx);
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
c.name(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
write_string_to_grid(
|
||||
c.email(),
|
||||
&mut self.data_columns.columns[1],
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.1, idx)),
|
||||
None,
|
||||
);
|
||||
{
|
||||
let area = self.data_columns.columns[1].area().nth_row(idx);
|
||||
self.data_columns.columns[1].grid_mut().write_string(
|
||||
c.email(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
write_string_to_grid(
|
||||
c.url(),
|
||||
&mut self.data_columns.columns[2],
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.2, idx)),
|
||||
None,
|
||||
);
|
||||
{
|
||||
let area = self.data_columns.columns[2].area().nth_row(idx);
|
||||
self.data_columns.columns[2].grid_mut().write_string(
|
||||
c.url(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
write_string_to_grid(
|
||||
if c.external_resource() {
|
||||
"external"
|
||||
} else {
|
||||
"local"
|
||||
},
|
||||
&mut self.data_columns.columns[3],
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.3, idx)),
|
||||
None,
|
||||
);
|
||||
{
|
||||
let area = self.data_columns.columns[3].area().nth_row(idx);
|
||||
self.data_columns.columns[3].grid_mut().write_string(
|
||||
if c.external_resource() {
|
||||
"external"
|
||||
} else {
|
||||
"local"
|
||||
},
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if self.length == 0 {
|
||||
let message = "Address book is empty.".to_string();
|
||||
self.data_columns.columns[0] =
|
||||
CellBuffer::new_with_context(message.len(), self.length, None, context);
|
||||
write_string_to_grid(
|
||||
&message,
|
||||
&mut self.data_columns.columns[0],
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, 0), (message.len() - 1, 0)),
|
||||
None,
|
||||
);
|
||||
if self.data_columns.columns[0].resize_with_context(message.len(), self.length, context)
|
||||
{
|
||||
let area = self.data_columns.columns[0].area();
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
&message,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) {
|
||||
fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize) {
|
||||
/* Reset previously highlighted line */
|
||||
let mut theme = if idx == self.new_cursor_pos {
|
||||
self.highlight_theme
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
theme.fg = self.theme_default.fg;
|
||||
if !grid.use_color {
|
||||
theme.attrs |= Attr::REVERSE;
|
||||
}
|
||||
change_theme(grid, area, theme);
|
||||
grid.change_theme(area, theme);
|
||||
}
|
||||
|
||||
fn draw_menu(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if !self.is_dirty() {
|
||||
return;
|
||||
}
|
||||
clear_area(grid, area, self.theme_default);
|
||||
let upper_left = upper_left!(area);
|
||||
let bottom_right = bottom_right!(area);
|
||||
grid.clear_area(area, self.theme_default);
|
||||
self.dirty = false;
|
||||
let mut y = get_y(upper_left);
|
||||
for a in &self.accounts {
|
||||
self.print_account(grid, (set_y(upper_left, y), bottom_right), a, context);
|
||||
y += 1;
|
||||
for (y, a) in self.accounts.iter().enumerate() {
|
||||
self.print_account(grid, area.nth_row(y), a, context);
|
||||
}
|
||||
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
/*
|
||||
* Print a single account in the menu area.
|
||||
*/
|
||||
|
@ -260,13 +279,9 @@ impl ContactList {
|
|||
grid: &mut CellBuffer,
|
||||
area: Area,
|
||||
a: &AccountMenuEntry,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
) {
|
||||
if !is_valid_area!(area) {
|
||||
log::debug!("BUG: invalid area in print_account");
|
||||
}
|
||||
|
||||
let width = width!(area);
|
||||
let width = area.width();
|
||||
let must_highlight_account: bool = self.account_pos == a.index;
|
||||
let account_attrs = if must_highlight_account {
|
||||
let mut v = crate::conf::value(context, "mail.sidebar_highlighted");
|
||||
|
@ -278,109 +293,58 @@ impl ContactList {
|
|||
crate::conf::value(context, "mail.sidebar_account_name")
|
||||
};
|
||||
|
||||
let s = format!(" [{}]", context.accounts[a.index].address_book.len());
|
||||
grid.change_theme(area, account_attrs);
|
||||
let s = format!(" [{}]", context.accounts[a.index].contacts.len());
|
||||
/* Print account name */
|
||||
grid.write_string(
|
||||
&a.name,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
grid.write_string(
|
||||
&s,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
area.skip_cols(area.width().saturating_sub(s.len())),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
if a.name.grapheme_len() + s.len() > width + 1 {
|
||||
/* Print account name */
|
||||
let (x, y) = write_string_to_grid(
|
||||
&a.name,
|
||||
grid,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
write_string_to_grid(
|
||||
&s,
|
||||
grid,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
(
|
||||
pos_dec(
|
||||
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
|
||||
(s.len() - 1, 0),
|
||||
),
|
||||
bottom_right!(area),
|
||||
),
|
||||
None,
|
||||
);
|
||||
write_string_to_grid(
|
||||
grid.write_string(
|
||||
"…",
|
||||
grid,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
(
|
||||
pos_dec(
|
||||
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
|
||||
(s.len() - 1, 0),
|
||||
),
|
||||
bottom_right!(area),
|
||||
),
|
||||
area.skip_cols(area.width().saturating_sub(s.len() + 1)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
for x in x..=get_x(bottom_right!(area)) {
|
||||
grid[(x, y)]
|
||||
.set_fg(account_attrs.fg)
|
||||
.set_bg(account_attrs.bg)
|
||||
.set_attrs(account_attrs.attrs);
|
||||
}
|
||||
} else {
|
||||
/* Print account name */
|
||||
|
||||
let (x, y) = write_string_to_grid(
|
||||
&a.name,
|
||||
grid,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
write_string_to_grid(
|
||||
&s,
|
||||
grid,
|
||||
account_attrs.fg,
|
||||
account_attrs.bg,
|
||||
account_attrs.attrs,
|
||||
(
|
||||
pos_dec(
|
||||
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
|
||||
(s.len() - 1, 0),
|
||||
),
|
||||
bottom_right!(area),
|
||||
),
|
||||
None,
|
||||
);
|
||||
for x in x..=get_x(bottom_right!(area)) {
|
||||
grid[(x, y)]
|
||||
.set_fg(account_attrs.fg)
|
||||
.set_bg(account_attrs.bg)
|
||||
.set_attrs(account_attrs.attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
let total_area = area;
|
||||
/* reserve top row for column headers */
|
||||
let upper_left = pos_inc(upper_left!(area), (0, 1));
|
||||
let bottom_right = bottom_right!(area);
|
||||
|
||||
let header_area = area.nth_row(0);
|
||||
let area = area.skip_rows(1);
|
||||
if self.length == 0 {
|
||||
clear_area(grid, area, self.theme_default);
|
||||
copy_area(
|
||||
grid,
|
||||
&self.data_columns.columns[0],
|
||||
grid.clear_area(area, self.theme_default);
|
||||
|
||||
grid.copy_area(
|
||||
self.data_columns.columns[0].grid(),
|
||||
area,
|
||||
((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))),
|
||||
self.data_columns.columns[0].area(),
|
||||
);
|
||||
context.dirty_areas.push_back(area);
|
||||
context.dirty_areas.push_back(total_area);
|
||||
return;
|
||||
}
|
||||
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
|
||||
let rows = area.height();
|
||||
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
match mvm {
|
||||
|
@ -429,7 +393,7 @@ impl ContactList {
|
|||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines: top_idx + rows,
|
||||
shown_lines: (top_idx + rows).min(self.length - top_idx),
|
||||
total_lines: self.length,
|
||||
has_more_lines: false,
|
||||
},
|
||||
|
@ -450,12 +414,9 @@ impl ContactList {
|
|||
self.cursor_pos = self.new_cursor_pos;
|
||||
for idx in &[old_cursor_pos, self.new_cursor_pos] {
|
||||
if *idx >= self.length {
|
||||
continue; //bounds check
|
||||
continue;
|
||||
}
|
||||
let new_area = (
|
||||
set_y(upper_left, get_y(upper_left) + (*idx % rows)),
|
||||
set_y(bottom_right, get_y(upper_left) + (*idx % rows)),
|
||||
);
|
||||
let new_area = area.nth_row(*idx % rows);
|
||||
self.highlight_line(grid, new_area, *idx);
|
||||
context.dirty_areas.push_back(new_area);
|
||||
}
|
||||
|
@ -468,34 +429,20 @@ impl ContactList {
|
|||
self.cursor_pos = self.new_cursor_pos;
|
||||
}
|
||||
|
||||
let width = width!(area);
|
||||
self.data_columns.widths = Default::default();
|
||||
self.data_columns.widths[0] = self.data_columns.columns[0].size().0; /* name */
|
||||
self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* email */
|
||||
self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* url */
|
||||
self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* source */
|
||||
|
||||
let min_col_width = std::cmp::min(
|
||||
15,
|
||||
std::cmp::min(self.data_columns.widths[0], self.data_columns.widths[1]),
|
||||
);
|
||||
if self.data_columns.widths[0] + self.data_columns.widths[1] + 3 * min_col_width + 8 > width
|
||||
{
|
||||
let remainder =
|
||||
width.saturating_sub(self.data_columns.widths[0] + self.data_columns.widths[1] + 4);
|
||||
self.data_columns.widths[2] = remainder / 6;
|
||||
}
|
||||
clear_area(grid, area, self.theme_default);
|
||||
/* Page_no has changed, so draw new page */
|
||||
grid.clear_area(total_area, self.theme_default);
|
||||
_ = self.data_columns.recalc_widths(area.size(), top_idx);
|
||||
/* copy table columns */
|
||||
self.data_columns
|
||||
.draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area));
|
||||
|
||||
let header_attrs = crate::conf::value(context, "widgets.list.header");
|
||||
let mut x = get_x(upper_left);
|
||||
let mut x = 0;
|
||||
for i in 0..self.data_columns.columns.len() {
|
||||
if self.data_columns.widths[i] == 0 {
|
||||
continue;
|
||||
}
|
||||
let (column_width, column_height) = self.data_columns.columns[i].size();
|
||||
write_string_to_grid(
|
||||
grid.write_string(
|
||||
match i {
|
||||
0 => "NAME",
|
||||
1 => "E-MAIL",
|
||||
|
@ -503,77 +450,38 @@ impl ContactList {
|
|||
3 => "SOURCE",
|
||||
_ => "",
|
||||
},
|
||||
grid,
|
||||
header_attrs.fg,
|
||||
header_attrs.bg,
|
||||
header_attrs.attrs,
|
||||
(
|
||||
set_x(upper_left!(area), x),
|
||||
(
|
||||
std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])),
|
||||
get_y(upper_left!(area)),
|
||||
),
|
||||
),
|
||||
header_area
|
||||
.skip_cols(x)
|
||||
.take_cols(x + (self.data_columns.widths[i])),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
copy_area(
|
||||
grid,
|
||||
&self.data_columns.columns[i],
|
||||
(
|
||||
set_x(upper_left, x),
|
||||
set_x(
|
||||
bottom_right,
|
||||
std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])),
|
||||
),
|
||||
),
|
||||
(
|
||||
(0, top_idx),
|
||||
(
|
||||
column_width.saturating_sub(1),
|
||||
column_height.saturating_sub(1),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
x += self.data_columns.widths[i] + 2; // + SEPARATOR
|
||||
if x > get_x(bottom_right) {
|
||||
if x > header_area.width() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
change_theme(
|
||||
grid,
|
||||
(
|
||||
upper_left!(area),
|
||||
set_y(bottom_right, get_y(upper_left!(area))),
|
||||
),
|
||||
header_attrs,
|
||||
);
|
||||
grid.change_theme(header_area, header_attrs);
|
||||
|
||||
if top_idx + rows > self.length {
|
||||
clear_area(
|
||||
grid,
|
||||
(
|
||||
pos_inc(upper_left, (0, self.length - top_idx + 2)),
|
||||
bottom_right,
|
||||
),
|
||||
grid.clear_area(
|
||||
area.skip_rows(top_idx + rows - self.length.saturating_sub(1)),
|
||||
self.theme_default,
|
||||
);
|
||||
}
|
||||
self.highlight_line(
|
||||
grid,
|
||||
(
|
||||
set_y(upper_left, get_y(upper_left) + (self.cursor_pos % rows)),
|
||||
set_y(bottom_right, get_y(upper_left) + (self.cursor_pos % rows)),
|
||||
),
|
||||
self.cursor_pos,
|
||||
);
|
||||
context.dirty_areas.push_back(area);
|
||||
self.highlight_line(grid, area.nth_row(self.cursor_pos % rows), self.cursor_pos);
|
||||
context.dirty_areas.push_back(total_area);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ContactList {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if let Some(mgr) = self.view.as_mut() {
|
||||
if let ViewMode::View(ref mut mgr) = self.mode {
|
||||
mgr.draw(grid, area, context);
|
||||
return;
|
||||
}
|
||||
|
@ -585,27 +493,26 @@ impl Component for ContactList {
|
|||
self.initialize(context);
|
||||
}
|
||||
|
||||
let upper_left = upper_left!(area);
|
||||
let bottom_right = bottom_right!(area);
|
||||
let total_cols = get_x(bottom_right) - get_x(upper_left);
|
||||
let total_cols = area.width();
|
||||
|
||||
let right_component_width = if self.menu_visibility {
|
||||
(self.ratio * total_cols) / 100
|
||||
} else {
|
||||
total_cols
|
||||
};
|
||||
let mid = get_x(bottom_right) - right_component_width;
|
||||
if self.dirty && mid != get_x(upper_left) {
|
||||
for i in get_y(upper_left)..=get_y(bottom_right) {
|
||||
grid[(mid, i)]
|
||||
.set_ch(self.sidebar_divider)
|
||||
.set_fg(self.sidebar_divider_theme.fg)
|
||||
.set_bg(self.sidebar_divider_theme.bg)
|
||||
.set_attrs(self.sidebar_divider_theme.attrs);
|
||||
let mid = area.width().saturating_sub(right_component_width);
|
||||
if self.dirty && mid != 0 {
|
||||
let divider_area = area.nth_col(mid);
|
||||
for row in grid.bounds_iter(divider_area) {
|
||||
for c in row {
|
||||
grid[c]
|
||||
.set_ch(self.sidebar_divider)
|
||||
.set_fg(self.sidebar_divider_theme.fg)
|
||||
.set_bg(self.sidebar_divider_theme.bg)
|
||||
.set_attrs(self.sidebar_divider_theme.attrs);
|
||||
}
|
||||
}
|
||||
context
|
||||
.dirty_areas
|
||||
.push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right))));
|
||||
context.dirty_areas.push_back(divider_area);
|
||||
}
|
||||
|
||||
if right_component_width == total_cols {
|
||||
|
@ -613,18 +520,18 @@ impl Component for ContactList {
|
|||
} else if right_component_width == 0 {
|
||||
self.draw_menu(grid, area, context);
|
||||
} else {
|
||||
self.draw_menu(
|
||||
grid,
|
||||
(upper_left, (mid.saturating_sub(1), get_y(bottom_right))),
|
||||
context,
|
||||
);
|
||||
self.draw_list(grid, (set_x(upper_left, mid + 1), bottom_right), context);
|
||||
self.draw_menu(grid, area.take_cols(mid), context);
|
||||
self.draw_list(grid, area.skip_cols(mid + 1), context);
|
||||
}
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
match event {
|
||||
UIEvent::VisibilityChange(true) => {
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::ConfigReload { old_settings: _ } => {
|
||||
self.theme_default = crate::conf::value(context, "theme_default");
|
||||
self.initialized = false;
|
||||
|
@ -636,12 +543,6 @@ impl Component for ContactList {
|
|||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::ComponentUnrealize(ref kill_id) if self.mode == ViewMode::View(*kill_id) => {
|
||||
self.mode = ViewMode::List;
|
||||
self.view.take();
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::ChangeMode(UIMode::Normal) => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
|
@ -651,33 +552,36 @@ impl Component for ContactList {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(ref mut v) = self.view {
|
||||
if v.process_event(event, context) {
|
||||
if let ViewMode::View(ref mut mgr) = self.mode {
|
||||
if matches!(event, UIEvent::ComponentUnrealize(id) if *id == mgr.id()) {
|
||||
mgr.unrealize(context);
|
||||
self.mode = ViewMode::List;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
if mgr.process_event(event, context) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let shortcuts = self.shortcuts(context);
|
||||
if self.view.is_none() {
|
||||
if matches!(self.mode, ViewMode::List) {
|
||||
match *event {
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["create_contact"]) =>
|
||||
{
|
||||
let mut manager = ContactManager::new(context);
|
||||
let mut manager = Box::new(ContactManager::new(context));
|
||||
manager.set_parent_id(self.id);
|
||||
manager.account_pos = self.account_pos;
|
||||
|
||||
self.mode = ViewMode::View(manager.id());
|
||||
self.view = Some(manager);
|
||||
self.mode = ViewMode::View(manager);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["edit_contact"]) =>
|
||||
{
|
||||
|
@ -685,21 +589,33 @@ impl Component for ContactList {
|
|||
return true;
|
||||
}
|
||||
let account = &mut context.accounts[self.account_pos];
|
||||
let book = &mut account.address_book;
|
||||
let card = book[&self.id_positions[self.cursor_pos]].clone();
|
||||
let mut manager = ContactManager::new(context);
|
||||
let contacts = &mut account.contacts;
|
||||
let card = contacts[&self.id_positions[self.cursor_pos]].clone();
|
||||
let mut manager = Box::new(ContactManager::new(context));
|
||||
manager.set_parent_id(self.id);
|
||||
manager.card = card;
|
||||
manager.account_pos = self.account_pos;
|
||||
|
||||
self.mode = ViewMode::View(manager.id());
|
||||
self.view = Some(manager);
|
||||
self.mode = ViewMode::View(manager);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["export_contact"]) =>
|
||||
{
|
||||
if self.length == 0 {
|
||||
return true;
|
||||
}
|
||||
let card = {
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let contacts = &account.contacts;
|
||||
contacts[&self.id_positions[self.cursor_pos]].clone()
|
||||
};
|
||||
super::export_to_vcard(&card, self.account_pos, context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
|
@ -710,8 +626,8 @@ impl Component for ContactList {
|
|||
}
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let account_hash = account.hash();
|
||||
let book = &account.address_book;
|
||||
let card = &book[&self.id_positions[self.cursor_pos]];
|
||||
let contacts = &account.contacts;
|
||||
let card = &contacts[&self.id_positions[self.cursor_pos]];
|
||||
let mut draft: Draft = Draft::default();
|
||||
*draft.headers_mut().get_mut("To").unwrap() =
|
||||
format!("{} <{}>", &card.name(), &card.email());
|
||||
|
@ -733,7 +649,7 @@ impl Component for ContactList {
|
|||
}
|
||||
// [ref:TODO]: add a confirmation dialog?
|
||||
context.accounts[self.account_pos]
|
||||
.address_book
|
||||
.contacts
|
||||
.remove_card(self.id_positions[self.cursor_pos]);
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
|
@ -821,7 +737,7 @@ impl Component for ContactList {
|
|||
self.menu_visibility = !self.menu_visibility;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
|
||||
if !self.cmd_buf.is_empty() =>
|
||||
{
|
||||
self.cmd_buf.clear();
|
||||
|
@ -944,6 +860,25 @@ impl Component for ContactList {
|
|||
self.movement = Some(PageMovement::End);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.contact_list
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -951,13 +886,13 @@ impl Component for ContactList {
|
|||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.dirty || self.view.as_ref().map(|v| v.is_dirty()).unwrap_or(false)
|
||||
self.dirty || matches!(self.mode, ViewMode::View(ref mgr) if mgr.is_dirty())
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
if let Some(p) = self.view.as_mut() {
|
||||
p.set_dirty(value);
|
||||
};
|
||||
if let ViewMode::View(ref mut mgr) = self.mode {
|
||||
mgr.set_dirty(value);
|
||||
}
|
||||
self.dirty = value;
|
||||
}
|
||||
|
||||
|
@ -967,12 +902,13 @@ impl Component for ContactList {
|
|||
.replies
|
||||
.push_back(UIEvent::Action(Tab(TabAction::Kill(uuid))));
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = self
|
||||
.view
|
||||
.as_ref()
|
||||
.map(|p| p.shortcuts(context))
|
||||
.unwrap_or_default();
|
||||
let mut map = if let ViewMode::View(ref mgr) = self.mode {
|
||||
mgr.shortcuts(context)
|
||||
} else {
|
||||
ShortcutMaps::default()
|
||||
};
|
||||
|
||||
map.insert(
|
||||
Shortcuts::CONTACT_LIST,
|
||||
|
@ -991,16 +927,16 @@ impl Component for ContactList {
|
|||
}
|
||||
|
||||
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
||||
self.view
|
||||
.as_mut()
|
||||
.map(|p| p.can_quit_cleanly(context))
|
||||
.unwrap_or(true)
|
||||
if let ViewMode::View(ref mut mgr) = self.mode {
|
||||
return mgr.can_quit_cleanly(context);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn status(&self, context: &Context) -> String {
|
||||
format!(
|
||||
"{} entries",
|
||||
context.accounts[self.account_pos].address_book.len()
|
||||
context.accounts[self.account_pos].contacts.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,57 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod list;
|
||||
use melib::Card;
|
||||
|
||||
use crate::{
|
||||
types::{sanitize_filename, File, NotificationType, UIEvent},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub mod editor;
|
||||
pub mod list;
|
||||
|
||||
pub fn export_to_vcard(card: &Card, account_pos: usize, context: &mut Context) {
|
||||
let mut output_dir = context.accounts[account_pos]
|
||||
.settings
|
||||
.account
|
||||
.vcard_folder()
|
||||
.map(|s| std::path::Path::new(s).to_path_buf());
|
||||
let filename = sanitize_filename(format!(
|
||||
"{prefix}{name}{suffix}{space}{additionalname}",
|
||||
prefix = card.name_prefix(),
|
||||
name = card.name(),
|
||||
suffix = card.name_suffix(),
|
||||
space = if card.additionalname().trim().is_empty() {
|
||||
""
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
additionalname = card.additionalname()
|
||||
));
|
||||
let res = File::create_temp_file(
|
||||
card.to_vcard_string().as_bytes(),
|
||||
filename.as_deref(),
|
||||
output_dir.as_mut(),
|
||||
Some("vcf"),
|
||||
false,
|
||||
);
|
||||
match res {
|
||||
Ok(f) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Exported .vcf".into()),
|
||||
body: format!("Exported contact to vcard file to\n{}", f.path().display()).into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
source: None,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Could not export contact.".into()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
source: Some(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
289
meli/src/jobs.rs
289
meli/src/jobs.rs
|
@ -26,7 +26,10 @@ use std::{
|
|||
future::Future,
|
||||
iter,
|
||||
panic::catch_unwind,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
@ -42,8 +45,44 @@ use melib::{log, smol, utils::datetime, uuid::Uuid, UnixTimestamp};
|
|||
|
||||
use crate::types::{StatusEvent, ThreadEvent, UIEvent};
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum IsAsync {
|
||||
Async,
|
||||
Blocking,
|
||||
}
|
||||
|
||||
type AsyncTask = async_task::Runnable;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FinishedTimestamp(Arc<Mutex<UnixTimestamp>>);
|
||||
|
||||
impl FinishedTimestamp {
|
||||
fn finished(&self) -> Option<UnixTimestamp> {
|
||||
match self.0.lock() {
|
||||
Ok(v) if *v == 0 => None,
|
||||
Ok(v) => Some(*v),
|
||||
Err(poison) => {
|
||||
let mut guard = poison.into_inner();
|
||||
if *guard == 0 {
|
||||
*guard = UnixTimestamp::default();
|
||||
}
|
||||
Some(*guard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_finished(&self, new_value: Option<UnixTimestamp>) {
|
||||
let new_value = new_value.unwrap_or_default();
|
||||
match self.0.lock() {
|
||||
Ok(mut f) => *f = new_value,
|
||||
Err(poison) => {
|
||||
let mut guard = poison.into_inner();
|
||||
*guard = new_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_task(
|
||||
local: &Worker<MeliTask>,
|
||||
global: &Injector<MeliTask>,
|
||||
|
@ -110,15 +149,41 @@ pub struct MeliTask {
|
|||
timer: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
/// A spawned future's metadata for book-keeping.
|
||||
pub struct JobMetadata {
|
||||
pub id: JobId,
|
||||
pub desc: Cow<'static, str>,
|
||||
pub timer: bool,
|
||||
pub started: UnixTimestamp,
|
||||
pub finished: Option<UnixTimestamp>,
|
||||
pub succeeded: bool,
|
||||
id: JobId,
|
||||
desc: Cow<'static, str>,
|
||||
timer: bool,
|
||||
started: UnixTimestamp,
|
||||
finished: FinishedTimestamp,
|
||||
succeeded: bool,
|
||||
}
|
||||
|
||||
impl JobMetadata {
|
||||
pub fn id(&self) -> &JobId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.desc
|
||||
}
|
||||
|
||||
pub fn is_timer(&self) -> bool {
|
||||
self.timer
|
||||
}
|
||||
|
||||
pub fn started(&self) -> UnixTimestamp {
|
||||
self.started
|
||||
}
|
||||
|
||||
pub fn finished(&self) -> Option<UnixTimestamp> {
|
||||
self.finished.finished()
|
||||
}
|
||||
|
||||
pub fn succeeded(&self) -> bool {
|
||||
self.succeeded
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -139,7 +204,7 @@ struct TimerPrivate {
|
|||
value: Duration,
|
||||
active: bool,
|
||||
handle: Option<async_task::Task<()>>,
|
||||
cancel: Arc<Mutex<bool>>,
|
||||
cancel: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -176,7 +241,7 @@ impl JobExecutor {
|
|||
/// A queue that holds scheduled tasks.
|
||||
pub fn new(sender: Sender<ThreadEvent>) -> Self {
|
||||
// Create a queue.
|
||||
let mut ret = JobExecutor {
|
||||
let mut ret = Self {
|
||||
global_queue: Arc::new(Injector::new()),
|
||||
workers: vec![],
|
||||
parkers: vec![],
|
||||
|
@ -185,7 +250,10 @@ impl JobExecutor {
|
|||
jobs: Arc::new(Mutex::new(IndexMap::default())),
|
||||
};
|
||||
let mut workers = vec![];
|
||||
for _ in 0..num_cpus::get().max(1) {
|
||||
for _ in 0..std::thread::available_parallelism()
|
||||
.map(Into::into)
|
||||
.unwrap_or(1)
|
||||
{
|
||||
let new_worker = Worker::new_fifo();
|
||||
ret.workers.push(new_worker.stealer());
|
||||
let p = Parker::new();
|
||||
|
@ -234,7 +302,27 @@ impl JobExecutor {
|
|||
}
|
||||
|
||||
/// Spawns a future with a generic return value `R`
|
||||
pub fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
#[inline(always)]
|
||||
pub fn spawn<F, R>(
|
||||
&self,
|
||||
desc: Cow<'static, str>,
|
||||
future: F,
|
||||
is_async: IsAsync,
|
||||
) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
if matches!(is_async, IsAsync::Async) {
|
||||
self.spawn_specialized(desc, future)
|
||||
} else {
|
||||
self.spawn_blocking(desc, future)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a future with a generic return value `R`
|
||||
#[inline(always)]
|
||||
fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
R: Send + 'static,
|
||||
|
@ -243,8 +331,10 @@ impl JobExecutor {
|
|||
let finished_sender = self.sender.clone();
|
||||
let job_id = JobId::new();
|
||||
let injector = self.global_queue.clone();
|
||||
let cancel = Arc::new(Mutex::new(false));
|
||||
let cancel2 = cancel.clone();
|
||||
// We do not use `AtomicU64` because it's not portable, so ignore the lint.
|
||||
#[allow(clippy::mutex_integer)]
|
||||
let finished = FinishedTimestamp(Arc::new(Mutex::new(0)));
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
|
||||
self.jobs.lock().unwrap().insert(
|
||||
job_id,
|
||||
|
@ -252,34 +342,41 @@ impl JobExecutor {
|
|||
id: job_id,
|
||||
desc: desc.clone(),
|
||||
started: datetime::now(),
|
||||
finished: None,
|
||||
finished: finished.clone(),
|
||||
succeeded: true,
|
||||
timer: false,
|
||||
},
|
||||
);
|
||||
|
||||
// Create a task and schedule it for execution.
|
||||
let (handle, task) = async_task::spawn(
|
||||
async move {
|
||||
let res = future.await;
|
||||
let _ = sender.send(res);
|
||||
finished_sender
|
||||
.send(ThreadEvent::JobFinished(job_id))
|
||||
.unwrap();
|
||||
},
|
||||
move |task| {
|
||||
if *cancel.lock().unwrap() {
|
||||
return;
|
||||
}
|
||||
let desc = desc.clone();
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc,
|
||||
timer: false,
|
||||
})
|
||||
},
|
||||
);
|
||||
let (handle, task) = {
|
||||
let cancel = cancel.clone();
|
||||
let finished = finished.clone();
|
||||
async_task::spawn(
|
||||
async move {
|
||||
let res = future.await;
|
||||
let _ = sender.send(res);
|
||||
if let Ok(mut guard) = finished.0.lock() {
|
||||
*guard = datetime::now();
|
||||
}
|
||||
finished_sender
|
||||
.send(ThreadEvent::JobFinished(job_id))
|
||||
.unwrap();
|
||||
},
|
||||
move |task| {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let desc = desc.clone();
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc,
|
||||
timer: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
handle.schedule();
|
||||
for unparker in self.parkers.iter() {
|
||||
unparker.unpark();
|
||||
|
@ -287,7 +384,8 @@ impl JobExecutor {
|
|||
|
||||
JoinHandle {
|
||||
task: Arc::new(Mutex::new(Some(task))),
|
||||
cancel: cancel2,
|
||||
cancel,
|
||||
finished,
|
||||
chan: receiver,
|
||||
job_id,
|
||||
}
|
||||
|
@ -295,7 +393,8 @@ impl JobExecutor {
|
|||
|
||||
/// Spawns a future with a generic return value `R` that might block on a
|
||||
/// new thread
|
||||
pub fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
#[inline(always)]
|
||||
fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
R: Send + 'static,
|
||||
|
@ -306,10 +405,10 @@ impl JobExecutor {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn create_timer(self: Arc<JobExecutor>, interval: Duration, value: Duration) -> Timer {
|
||||
pub fn create_timer(self: Arc<Self>, interval: Duration, value: Duration) -> Timer {
|
||||
let timer = TimerPrivate {
|
||||
interval,
|
||||
cancel: Arc::new(Mutex::new(false)),
|
||||
cancel: Arc::new(AtomicBool::new(false)),
|
||||
value,
|
||||
active: true,
|
||||
handle: None,
|
||||
|
@ -337,46 +436,48 @@ impl JobExecutor {
|
|||
let sender = self.sender.clone();
|
||||
let injector = self.global_queue.clone();
|
||||
let timers = self.timers.clone();
|
||||
let cancel = Arc::new(Mutex::new(false));
|
||||
let cancel2 = cancel.clone();
|
||||
let (task, handle) = async_task::spawn(
|
||||
async move {
|
||||
let mut value = value;
|
||||
loop {
|
||||
smol::Timer::after(value).await;
|
||||
sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Timer(id)))
|
||||
.unwrap();
|
||||
if let Some(interval) = timers.lock().unwrap().get(&id).and_then(|timer| {
|
||||
if timer.interval.as_millis() == 0 && timer.interval.as_secs() == 0 {
|
||||
None
|
||||
} else if timer.active {
|
||||
Some(timer.interval)
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let (task, handle) = {
|
||||
let cancel = cancel.clone();
|
||||
async_task::spawn(
|
||||
async move {
|
||||
let mut value = value;
|
||||
loop {
|
||||
smol::Timer::after(value).await;
|
||||
sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Timer(id)))
|
||||
.unwrap();
|
||||
if let Some(interval) = timers.lock().unwrap().get(&id).and_then(|timer| {
|
||||
if timer.interval.as_millis() == 0 && timer.interval.as_secs() == 0 {
|
||||
None
|
||||
} else if timer.active {
|
||||
Some(timer.interval)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
value = interval;
|
||||
} else {
|
||||
None
|
||||
break;
|
||||
}
|
||||
}) {
|
||||
value = interval;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
move |task| {
|
||||
if *cancel.lock().unwrap() {
|
||||
return;
|
||||
}
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc: "timer".into(),
|
||||
timer: true,
|
||||
})
|
||||
},
|
||||
);
|
||||
},
|
||||
move |task| {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
injector.push(MeliTask {
|
||||
task,
|
||||
id: job_id,
|
||||
desc: Cow::Borrowed("timer"),
|
||||
timer: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
self.timers.lock().unwrap().entry(id).and_modify(|timer| {
|
||||
timer.handle = Some(handle);
|
||||
timer.cancel = cancel2;
|
||||
timer.cancel = cancel;
|
||||
timer.active = true;
|
||||
});
|
||||
task.schedule();
|
||||
|
@ -389,7 +490,7 @@ impl JobExecutor {
|
|||
let mut timers_lck = self.timers.lock().unwrap();
|
||||
if let Some(timer) = timers_lck.get_mut(&id) {
|
||||
timer.active = false;
|
||||
*timer.cancel.lock().unwrap() = true;
|
||||
timer.cancel.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -402,7 +503,7 @@ impl JobExecutor {
|
|||
|
||||
pub fn set_job_finished(&self, id: JobId) {
|
||||
self.jobs.lock().unwrap().entry(id).and_modify(|entry| {
|
||||
entry.finished = Some(datetime::now());
|
||||
entry.finished.set_finished(Some(datetime::now()));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -415,25 +516,34 @@ impl JobExecutor {
|
|||
|
||||
pub type JobChannel<T> = oneshot::Receiver<T>;
|
||||
|
||||
/// JoinHandle for the future that allows us to cancel the task.
|
||||
/// `JoinHandle` for the future that allows us to cancel the task.
|
||||
#[derive(Debug)]
|
||||
pub struct JoinHandle<T> {
|
||||
pub task: Arc<Mutex<Option<async_task::Task<()>>>>,
|
||||
pub chan: JobChannel<T>,
|
||||
pub cancel: Arc<Mutex<bool>>,
|
||||
pub cancel: Arc<AtomicBool>,
|
||||
finished: FinishedTimestamp,
|
||||
pub job_id: JobId,
|
||||
}
|
||||
|
||||
impl<T> JoinHandle<T> {
|
||||
pub fn cancel(&self) -> Option<StatusEvent> {
|
||||
let mut lck = self.cancel.lock().unwrap();
|
||||
if !*lck {
|
||||
*lck = true;
|
||||
let was_active = self.cancel.swap(true, Ordering::SeqCst);
|
||||
if was_active {
|
||||
self.finished.set_finished(Some(datetime::now()));
|
||||
Some(StatusEvent::JobCanceled(self.job_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finished(&self) -> Option<UnixTimestamp> {
|
||||
self.finished.finished()
|
||||
}
|
||||
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.cancel.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::cmp::PartialEq<JobId> for JoinHandle<T> {
|
||||
|
@ -442,17 +552,8 @@ impl<T> std::cmp::PartialEq<JobId> for JoinHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
impl Future for JoinHandle {
|
||||
type Output = Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match Pin::new(&mut self.inner).poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(output) => Poll::Ready(output.expect("task failed")),
|
||||
}
|
||||
impl<T> Drop for JoinHandle<T> {
|
||||
fn drop(&mut self) {
|
||||
_ = self.cancel();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -19,6 +19,60 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![deny(
|
||||
rustdoc::redundant_explicit_links,
|
||||
unsafe_op_in_unsafe_fn,
|
||||
/* groups */
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::complexity,
|
||||
clippy::perf,
|
||||
clippy::cargo,
|
||||
clippy::nursery,
|
||||
clippy::style,
|
||||
/* restriction */
|
||||
clippy::dbg_macro,
|
||||
clippy::rc_buffer,
|
||||
clippy::as_underscore,
|
||||
clippy::assertions_on_result_states,
|
||||
/* rustdoc */
|
||||
rustdoc::broken_intra_doc_links,
|
||||
/* pedantic */
|
||||
//clippy::cast_lossless,
|
||||
//clippy::cast_possible_wrap,
|
||||
//clippy::ptr_as_ptr,
|
||||
clippy::doc_markdown,
|
||||
clippy::expect_fun_call,
|
||||
clippy::or_fun_call,
|
||||
clippy::bool_to_int_with_if,
|
||||
clippy::borrow_as_ptr,
|
||||
clippy::cast_ptr_alignment,
|
||||
clippy::large_futures,
|
||||
clippy::waker_clone_wake,
|
||||
clippy::unused_enumerate_index,
|
||||
clippy::unnecessary_fallible_conversions,
|
||||
clippy::struct_field_names,
|
||||
clippy::manual_hash_one,
|
||||
clippy::into_iter_without_iter,
|
||||
)]
|
||||
#![allow(
|
||||
clippy::option_if_let_else,
|
||||
clippy::missing_const_for_fn,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::multiple_crate_versions,
|
||||
clippy::significant_drop_in_scrutinee,
|
||||
clippy::cognitive_complexity,
|
||||
clippy::manual_clamp
|
||||
)]
|
||||
/* Source Code Annotation Tags:
|
||||
*
|
||||
* Global tags (in tagref format <https://github.com/stepchowfun/tagref>) for source code
|
||||
* annotation:
|
||||
*
|
||||
* - tags from melib/src/lib.rs.
|
||||
* - [tag:hardcoded_color_value] Replace hardcoded color values with user configurable ones.
|
||||
*/
|
||||
|
||||
//!
|
||||
//! This crate contains the frontend stuff of the application. The application
|
||||
//! entry way on `src/bin.rs` creates an event loop and passes input to a
|
||||
|
@ -38,7 +92,6 @@ pub use melib::uuid;
|
|||
|
||||
pub extern crate bitflags;
|
||||
pub extern crate serde_json;
|
||||
#[macro_use]
|
||||
pub extern crate smallvec;
|
||||
pub extern crate termion;
|
||||
|
||||
|
@ -49,11 +102,14 @@ static GLOBAL: System = System;
|
|||
|
||||
pub extern crate melib;
|
||||
pub use melib::{
|
||||
error::*, log, AccountHash, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel, Mail, Mailbox,
|
||||
MailboxHash, ThreadHash, ToggleFlag,
|
||||
error::*, log, AccountHash, ActionFlag, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel,
|
||||
Mail, Mailbox, MailboxHash, ThreadHash, ToggleFlag,
|
||||
};
|
||||
|
||||
pub mod args;
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub mod manpages;
|
||||
pub mod signal_handlers;
|
||||
pub mod subcommands;
|
||||
|
||||
#[macro_use]
|
||||
|
@ -85,19 +141,17 @@ pub use crate::mail::*;
|
|||
|
||||
pub mod notifications;
|
||||
|
||||
pub mod mailbox_management;
|
||||
pub use mailbox_management::*;
|
||||
pub mod manage;
|
||||
pub use manage::*;
|
||||
|
||||
pub mod jobs_view;
|
||||
pub use jobs_view::*;
|
||||
|
||||
#[cfg(feature = "svgscreenshot")]
|
||||
pub mod svg;
|
||||
// #[cfg(feature = "svgscreenshot")]
|
||||
// pub mod svg;
|
||||
|
||||
#[macro_use]
|
||||
pub mod conf;
|
||||
pub use crate::conf::{
|
||||
DotAddressable, IndexStyle, SearchBackend, Settings, Shortcuts, ThemeAttribute,
|
||||
data_types::{IndexStyle, SearchBackend},
|
||||
DotAddressable, Settings, Shortcuts, ThemeAttribute,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
|
@ -108,3 +162,7 @@ pub mod mailcap;
|
|||
|
||||
pub mod accounts;
|
||||
pub use self::accounts::Account;
|
||||
|
||||
pub mod patch_retrieve;
|
||||
|
||||
pub mod version_migrations;
|
||||
|
|
|
@ -21,19 +21,21 @@
|
|||
|
||||
//! Entities that handle Mail specific functions.
|
||||
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::{AccountHash, Mailbox, MailboxHash},
|
||||
email::{attachment_types::*, attachments::*},
|
||||
text::{TextProcessing, Truncate},
|
||||
thread::ThreadNodeHash,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
boundaries::*,
|
||||
melib::text_processing::{TextProcessing, Truncate},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
pub type AttachmentBoxFuture = Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + Send>>;
|
||||
pub type AttachmentFilterBox = Box<dyn FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send>;
|
||||
|
||||
pub mod listing;
|
||||
pub use crate::listing::*;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -21,7 +21,7 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum EditAttachmentCursor {
|
||||
AttachmentNo(usize),
|
||||
#[default]
|
||||
|
@ -32,7 +32,7 @@ pub enum EditAttachmentCursor {
|
|||
pub enum EditAttachmentMode {
|
||||
Overview,
|
||||
Edit {
|
||||
inner: Box<FormWidget<FormButtonActions>>,
|
||||
inner: Box<FormWidget<FormButtonAction>>,
|
||||
no: usize,
|
||||
},
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ pub struct EditAttachments {
|
|||
/// For shortcut setting retrieval.
|
||||
pub account_hash: Option<AccountHash>,
|
||||
pub mode: EditAttachmentMode,
|
||||
pub buttons: ButtonWidget<FormButtonActions>,
|
||||
pub buttons: ButtonWidget<FormButtonAction>,
|
||||
pub cursor: EditAttachmentCursor,
|
||||
pub dirty: bool,
|
||||
pub id: ComponentId,
|
||||
|
@ -50,11 +50,11 @@ pub struct EditAttachments {
|
|||
|
||||
impl EditAttachments {
|
||||
pub fn new(account_hash: Option<AccountHash>) -> Self {
|
||||
//ButtonWidget::new(("Add".into(), FormButtonActions::Other("add")));
|
||||
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel));
|
||||
//ButtonWidget::new(("Add".into(), FormButtonAction::Other("add")));
|
||||
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonAction::Cancel));
|
||||
buttons.set_focus(true);
|
||||
buttons.set_cursor(1);
|
||||
EditAttachments {
|
||||
Self {
|
||||
account_hash,
|
||||
mode: EditAttachmentMode::Overview,
|
||||
buttons,
|
||||
|
@ -70,7 +70,7 @@ impl EditAttachmentsRefMut<'_, '_> {
|
|||
&self,
|
||||
no: usize,
|
||||
context: &Context,
|
||||
) -> Option<Box<FormWidget<FormButtonActions>>> {
|
||||
) -> Option<Box<FormWidget<FormButtonAction>>> {
|
||||
if no >= self.draft.attachments().len() {
|
||||
return None;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ impl EditAttachmentsRefMut<'_, '_> {
|
|||
let shortcuts = self.shortcuts(context);
|
||||
|
||||
let mut ret = FormWidget::new(
|
||||
("Save".into(), FormButtonActions::Accept),
|
||||
("Save".into(), FormButtonAction::Accept),
|
||||
/* cursor_up_shortcut */
|
||||
shortcuts
|
||||
.get(Shortcuts::COMPOSING)
|
||||
|
@ -92,8 +92,8 @@ impl EditAttachmentsRefMut<'_, '_> {
|
|||
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()),
|
||||
);
|
||||
|
||||
ret.add_button(("Reset".into(), FormButtonActions::Reset));
|
||||
ret.add_button(("Cancel".into(), FormButtonActions::Cancel));
|
||||
ret.add_button(("Reset".into(), FormButtonAction::Reset));
|
||||
ret.add_button(("Cancel".into(), FormButtonAction::Cancel));
|
||||
ret.push(("Filename".into(), filename.unwrap_or_default().to_string()));
|
||||
ret.push(("Mime type".into(), mime_type.to_string()));
|
||||
Some(Box::new(ret))
|
||||
|
@ -123,26 +123,26 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
} else if self.is_dirty() {
|
||||
let attachments_no = self.draft.attachments().len();
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
clear_area(grid, area, theme_default);
|
||||
grid.clear_area(area, theme_default);
|
||||
if attachments_no == 0 {
|
||||
write_string_to_grid(
|
||||
grid.write_string(
|
||||
"no attachments",
|
||||
grid,
|
||||
theme_default.fg,
|
||||
theme_default.bg,
|
||||
theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
write_string_to_grid(
|
||||
grid.write_string(
|
||||
&format!("{} attachments ", attachments_no),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
theme_default.bg,
|
||||
theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
for (i, a) in self.draft.attachments().iter().enumerate() {
|
||||
let bg = if let EditAttachmentCursor::AttachmentNo(u) = self.inner.cursor {
|
||||
|
@ -154,46 +154,35 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
} else {
|
||||
theme_default.bg
|
||||
};
|
||||
if let Some(name) = a.content_type().name() {
|
||||
write_string_to_grid(
|
||||
&format!(
|
||||
grid.write_string(
|
||||
&if let Some(name) = a.content_type().name() {
|
||||
format!(
|
||||
"[{}] \"{}\", {} {}",
|
||||
i,
|
||||
name,
|
||||
a.content_type(),
|
||||
melib::BytesDisplay(a.raw.len())
|
||||
),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
bg,
|
||||
theme_default.attrs,
|
||||
(pos_inc(upper_left!(area), (0, 1 + i)), bottom_right!(area)),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
write_string_to_grid(
|
||||
&format!(
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"[{}] {} {}",
|
||||
i,
|
||||
a.content_type(),
|
||||
melib::BytesDisplay(a.raw.len())
|
||||
),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
bg,
|
||||
theme_default.attrs,
|
||||
(pos_inc(upper_left!(area), (0, 1 + i)), bottom_right!(area)),
|
||||
None,
|
||||
);
|
||||
}
|
||||
)
|
||||
},
|
||||
theme_default.fg,
|
||||
bg,
|
||||
theme_default.attrs,
|
||||
area.skip(2, 2 + i),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.inner.buttons.draw(
|
||||
grid,
|
||||
(
|
||||
pos_inc(upper_left!(area), (0, 1 + self.draft.attachments().len())),
|
||||
bottom_right!(area),
|
||||
),
|
||||
area.skip_rows(3 + self.draft.attachments().len()),
|
||||
context,
|
||||
);
|
||||
self.set_dirty(false);
|
||||
|
@ -209,10 +198,10 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
{
|
||||
if inner.process_event(event, context) {
|
||||
match inner.buttons_result() {
|
||||
Some(FormButtonActions::Accept) | Some(FormButtonActions::Cancel) => {
|
||||
Some(FormButtonAction::Accept) | Some(FormButtonAction::Cancel) => {
|
||||
self.inner.mode = EditAttachmentMode::Overview;
|
||||
}
|
||||
Some(FormButtonActions::Reset) => {
|
||||
Some(FormButtonAction::Reset) => {
|
||||
let no = *no;
|
||||
if let Some(inner) = self.new_edit_widget(no, context) {
|
||||
self.inner.mode = EditAttachmentMode::Edit { inner, no };
|
||||
|
|
|
@ -21,15 +21,94 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
type KeylistJoinHandle = JoinHandle<Result<Vec<melib::gpgme::Key>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum KeySelection {
|
||||
LoadingKeys {
|
||||
handle: JoinHandle<Result<Vec<melib::gpgme::Key>>>,
|
||||
progress_spinner: ProgressSpinner,
|
||||
pub struct KeySelectionLoading {
|
||||
handles: (KeylistJoinHandle, Vec<KeylistJoinHandle>),
|
||||
progress_spinner: ProgressSpinner,
|
||||
secret: bool,
|
||||
local: bool,
|
||||
patterns: (String, Vec<String>),
|
||||
allow_remote_lookup: ActionFlag,
|
||||
}
|
||||
|
||||
impl KeySelectionLoading {
|
||||
pub fn new(
|
||||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ToggleFlag,
|
||||
patterns: (String, Vec<String>),
|
||||
allow_remote_lookup: ActionFlag,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
use melib::gpgme::{self, *};
|
||||
let mut ctx = gpgme::Context::new()?;
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
} else {
|
||||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let (pattern, other_patterns) = patterns;
|
||||
let main_job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let main_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::keylist".into(),
|
||||
main_job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let other_handles = other_patterns
|
||||
.iter()
|
||||
.map(|pattern| {
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
Ok(context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::keylist".into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<KeylistJoinHandle>>>()?;
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(Self {
|
||||
handles: (main_handle, other_handles),
|
||||
secret,
|
||||
local,
|
||||
patterns: (pattern, other_patterns),
|
||||
allow_remote_lookup,
|
||||
progress_spinner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, rhs: Self) {
|
||||
let Self {
|
||||
handles: (_, ref mut other_handles),
|
||||
secret: _,
|
||||
local: _,
|
||||
patterns: (_, ref mut other_patterns),
|
||||
allow_remote_lookup: _,
|
||||
progress_spinner: _,
|
||||
} = self;
|
||||
let Self {
|
||||
handles: (rhs_handle, rhs_other_handles),
|
||||
patterns: (rhs_pattern, rhs_other_patterns),
|
||||
secret: _,
|
||||
local: _,
|
||||
allow_remote_lookup: _,
|
||||
progress_spinner: _,
|
||||
} = rhs;
|
||||
other_handles.push(rhs_handle);
|
||||
other_handles.extend(rhs_other_handles);
|
||||
other_patterns.push(rhs_pattern);
|
||||
other_patterns.extend(rhs_other_patterns);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum KeySelection {
|
||||
Loading {
|
||||
inner: KeySelectionLoading,
|
||||
/// Accumulate results from intermediate results (i.e. not the main
|
||||
/// pattern)
|
||||
keys_accumulator: Vec<melib::gpgme::Key>,
|
||||
},
|
||||
Error {
|
||||
id: ComponentId,
|
||||
|
@ -41,80 +120,75 @@ pub enum KeySelection {
|
|||
},
|
||||
}
|
||||
|
||||
impl From<KeySelectionLoading> for KeySelection {
|
||||
fn from(inner: KeySelectionLoading) -> Self {
|
||||
Self::Loading {
|
||||
inner,
|
||||
keys_accumulator: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeySelection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "select pgp keys")
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySelection {
|
||||
pub fn new(
|
||||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ToggleFlag,
|
||||
context: &mut Context,
|
||||
) -> Result<Self> {
|
||||
use melib::gpgme::*;
|
||||
let mut ctx = Context::new()?;
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
} else {
|
||||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let handle = context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("gpg::keylist".into(), job);
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(KeySelection::LoadingKeys {
|
||||
handle,
|
||||
secret,
|
||||
local,
|
||||
pattern,
|
||||
allow_remote_lookup,
|
||||
progress_spinner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for KeySelection {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
} => progress_spinner.draw(grid, center_area(area, (2, 2)), context),
|
||||
KeySelection::Error { ref err, .. } => {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
|
||||
Self::Error { ref err, .. } => {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
write_string_to_grid(
|
||||
grid.write_string(
|
||||
&err.to_string(),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
theme_default.bg,
|
||||
theme_default.attrs,
|
||||
center_area(area, (15, 2)),
|
||||
area.center_inside((15, 2)),
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
}
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
|
||||
Self::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
ref mut handle,
|
||||
secret,
|
||||
local,
|
||||
ref mut pattern,
|
||||
allow_remote_lookup,
|
||||
..
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
handles: (ref mut main_handle, ref mut other_handles),
|
||||
secret,
|
||||
local,
|
||||
patterns: (ref mut pattern, ref mut other_patterns),
|
||||
allow_remote_lookup,
|
||||
..
|
||||
},
|
||||
ref mut keys_accumulator,
|
||||
} => match event {
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id)) if *id == handle.job_id => {
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id))
|
||||
if *id == main_handle.job_id
|
||||
|| other_handles.iter().any(|h| h.job_id == *id) =>
|
||||
{
|
||||
let mut main_handle_ref = &mut (*main_handle);
|
||||
let is_main = *id == main_handle_ref.job_id;
|
||||
let other_handle_ref_opt = other_handles.iter_mut().find(|h| h.job_id == *id);
|
||||
let handle = if is_main {
|
||||
&mut main_handle_ref
|
||||
} else {
|
||||
&mut (*other_handle_ref_opt.unwrap())
|
||||
};
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* Job was canceled */ }
|
||||
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
|
||||
|
@ -122,20 +196,24 @@ impl Component for KeySelection {
|
|||
if keys.is_empty() {
|
||||
let id = progress_spinner.id();
|
||||
if allow_remote_lookup.is_true() {
|
||||
match Self::new(
|
||||
match KeySelectionLoading::new(
|
||||
*secret,
|
||||
*local,
|
||||
std::mem::take(pattern),
|
||||
(std::mem::take(pattern), std::mem::take(other_patterns)),
|
||||
*allow_remote_lookup,
|
||||
context,
|
||||
) {
|
||||
Ok(w) => {
|
||||
*self = w;
|
||||
Ok(inner) => {
|
||||
let keys_accumulator = std::mem::take(keys_accumulator);
|
||||
*self = Self::Loading {
|
||||
inner,
|
||||
keys_accumulator,
|
||||
};
|
||||
}
|
||||
Err(err) => *self = KeySelection::Error { err, id },
|
||||
Err(err) => *self = Self::Error { err, id },
|
||||
}
|
||||
} else if !*local && allow_remote_lookup.is_ask() {
|
||||
*self = KeySelection::Error {
|
||||
*self = Self::Error {
|
||||
err: Error::new(format!(
|
||||
"No keys found for {}, perform remote lookup?",
|
||||
pattern
|
||||
|
@ -143,52 +221,88 @@ impl Component for KeySelection {
|
|||
id,
|
||||
}
|
||||
} else {
|
||||
*self = KeySelection::Error {
|
||||
err: Error::new(format!("No keys found for {}.", pattern)),
|
||||
*self = Self::Error {
|
||||
err: if pattern.is_empty() {
|
||||
Error::new("No keys found.")
|
||||
} else {
|
||||
Error::new(format!("No keys found for {}.", pattern))
|
||||
},
|
||||
id,
|
||||
}
|
||||
}
|
||||
if let KeySelection::Error { ref err, .. } = self {
|
||||
if let Self::Error { ref err, .. } = self {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
let res: Option<melib::gpgme::Key> = None;
|
||||
// Even in case of error, we should send a FinishedUIDialog
|
||||
// event so that the component parent knows we're done.
|
||||
let res: Option<Vec<melib::gpgme::Key>> = None;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::FinishedUIDialog(id, Box::new(res)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let mut widget = Box::new(UIDialog::new(
|
||||
"select key",
|
||||
keys.iter()
|
||||
.map(|k| {
|
||||
(
|
||||
k.clone(),
|
||||
if let Some(primary_uid) = k.primary_uid() {
|
||||
format!("{} {}", k.fingerprint(), primary_uid)
|
||||
} else {
|
||||
k.fingerprint().to_string()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(melib::gpgme::Key, String)>>(),
|
||||
true,
|
||||
Some(Box::new(
|
||||
move |id: ComponentId, results: &[melib::gpgme::Key]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(results.get(0).cloned()),
|
||||
))
|
||||
},
|
||||
)),
|
||||
context,
|
||||
));
|
||||
widget.set_dirty(true);
|
||||
*self = KeySelection::Loaded { widget, keys };
|
||||
keys_accumulator.extend(keys);
|
||||
if !is_main {
|
||||
other_handles.retain(|h| h.job_id != *id);
|
||||
return false;
|
||||
}
|
||||
if other_handles.is_empty() {
|
||||
// We are done with all Futures, so finally transition into the
|
||||
// "show the user the list of keys to select" state.
|
||||
let mut widget = Box::new(UIDialog::new(
|
||||
"select key",
|
||||
keys_accumulator
|
||||
.iter()
|
||||
.map(|k| {
|
||||
(
|
||||
k.clone(),
|
||||
if let Some(primary_uid) = k.primary_uid() {
|
||||
format!("{} {}", k.fingerprint(), primary_uid)
|
||||
} else {
|
||||
k.fingerprint().to_string()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(melib::gpgme::Key, String)>>(),
|
||||
false,
|
||||
Some(Box::new(
|
||||
move |id: ComponentId, results: &[melib::gpgme::Key]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(if results.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(results.to_vec())
|
||||
}),
|
||||
))
|
||||
},
|
||||
)),
|
||||
context,
|
||||
));
|
||||
widget.set_dirty(true);
|
||||
*self = Self::Loaded {
|
||||
widget,
|
||||
keys: std::mem::take(keys_accumulator),
|
||||
};
|
||||
} else {
|
||||
// Main handle has finished, replace it with some other one from
|
||||
// other_handles.
|
||||
*main_handle = other_handles.remove(0);
|
||||
}
|
||||
}
|
||||
Ok(Some(Err(err))) => {
|
||||
*self = KeySelection::Error {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
// Even in case of error, we should send a FinishedUIDialog
|
||||
// event so that the component parent knows we're done.
|
||||
let res: Option<Vec<melib::gpgme::Key>> = None;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::FinishedUIDialog(self.id(), Box::new(res)));
|
||||
*self = Self::Error {
|
||||
err,
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
|
@ -198,30 +312,38 @@ impl Component for KeySelection {
|
|||
}
|
||||
_ => progress_spinner.process_event(event, context),
|
||||
},
|
||||
KeySelection::Error { .. } => false,
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.process_event(event, context),
|
||||
Self::Error { .. } => false,
|
||||
Self::Loaded { ref mut widget, .. } => widget.process_event(event, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref progress_spinner,
|
||||
..
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.is_dirty(),
|
||||
KeySelection::Error { .. } => true,
|
||||
KeySelection::Loaded { ref widget, .. } => widget.is_dirty(),
|
||||
Self::Error { .. } => true,
|
||||
Self::Loaded { ref widget, .. } => widget.is_dirty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.set_dirty(value),
|
||||
KeySelection::Error { .. } => {}
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.set_dirty(value),
|
||||
Self::Error { .. } => {}
|
||||
Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,29 +351,31 @@ impl Component for KeySelection {
|
|||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
match self {
|
||||
KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => {
|
||||
ShortcutMaps::default()
|
||||
}
|
||||
KeySelection::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
Self::Loading { .. } | Self::Error { .. } => ShortcutMaps::default(),
|
||||
Self::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref progress_spinner,
|
||||
..
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.id(),
|
||||
KeySelection::Error { ref id, .. } => *id,
|
||||
KeySelection::Loaded { ref widget, .. } => widget.id(),
|
||||
Self::Error { ref id, .. } => *id,
|
||||
Self::Loaded { ref widget, .. } => widget.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GpgComposeState {
|
||||
pub sign_mail: ToggleFlag,
|
||||
pub encrypt_mail: ToggleFlag,
|
||||
pub sign_mail: Option<ActionFlag>,
|
||||
pub encrypt_mail: Option<ActionFlag>,
|
||||
pub encrypt_keys: Vec<melib::gpgme::Key>,
|
||||
pub encrypt_for_self: bool,
|
||||
pub sign_keys: Vec<melib::gpgme::Key>,
|
||||
|
@ -259,12 +383,192 @@ pub struct GpgComposeState {
|
|||
|
||||
impl Default for GpgComposeState {
|
||||
fn default() -> Self {
|
||||
GpgComposeState {
|
||||
sign_mail: ToggleFlag::Unset,
|
||||
encrypt_mail: ToggleFlag::Unset,
|
||||
Self {
|
||||
sign_mail: None,
|
||||
encrypt_mail: None,
|
||||
encrypt_keys: vec![],
|
||||
encrypt_for_self: true,
|
||||
sign_keys: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Cow, ffi::CString, thread::sleep, time::Duration};
|
||||
|
||||
use melib::gpgme::{EngineInfo, LocateKey, Protocol};
|
||||
use rusty_fork::rusty_fork_test;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl KeySelection {
|
||||
fn new_mock(
|
||||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ActionFlag,
|
||||
context: &Context,
|
||||
ctx: &mut melib::gpgme::Context,
|
||||
) -> Result<Self> {
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
} else {
|
||||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::keylist".into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(Self::Loading {
|
||||
inner: KeySelectionLoading {
|
||||
handles: (handle, vec![]),
|
||||
secret,
|
||||
local,
|
||||
patterns: (pattern, vec![]),
|
||||
allow_remote_lookup,
|
||||
progress_spinner,
|
||||
},
|
||||
keys_accumulator: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const PUBKEY: &[u8]=b"-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: GnuPG v2.1.0-gitb3c71eb (GNU/Linux)\r\n\r\nmQGiBDo41NoRBADSfQazKGYf8nokq6zUKH/6INtV6MypSzSGmX2XErnARkIIPPYj\r\ncQRQ8zCbGV7ZU2ezVbzhFLUSJveE8PZUzzCrLp1O2NSyBTRcR5HVSXW95nJfY8eV\r\npOvZRAKul0BVLh81kYTsrfzaaCjh9VWNP26LoeN2r+PjZyktXe7gM3C4SwCgoTxK\r\nWUVi9HoT2HCLY7p7oig5hEcEALdCJal0UYomX3nJapIVLVZg3vkidr1RICYMb2vz\r\n58i17h8sxEtobD1vdIKNejulntaRAXs4n0tDYD9z7pRlwG1CLz1R9WxYzeOOqUDr\r\nfnVXdmU8L/oVWABat8v1V7QQhjMMf+41fuzVwDMMGqjVPLhu4X6wp3A8uyM3YDnQ\r\nVMN1A/4n2G5gHoOvjqxn8Ch5tBAdMGfO8gH4RjQOwzm2R1wPQss/yzUN1+tlMZGX\r\nK2dQ2FCWC/hDUSNaEQRlI15wxxBNZ2RQwlzE2A8v113DpvyzOtv0QO95gJ1teCXC\r\n7j/BN9asgHaBBc39JLO/TcpuI7Hf8PQ5VcP2F0UE3lczGhXbLLRESm9lIFJhbmRv\r\nbSBIYWNrZXIgKHRlc3Qga2V5IHdpdGggcGFzc3BocmFzZSAiYWJjIikgPGpvZUBl\r\neGFtcGxlLmNvbT6IYgQTEQIAIgUCTbdXqQIbIwYLCQgHAwIGFQgCCQoLBBYCAwEC\r\nHgECF4AACgkQr4IkT5zZ/VUcCACfQvSPi//9/gBv8SVrK6O4DiyD+jAAn3LEnfF1\r\n4j6MjwlqXTqol2VgQn1yuQENBDo41N0QBACedJb7Qhm50JSPe1V+rSZKLHT5nc3l\r\n2k1n7//wNsJkgDW2J7snIRjGtSzeNxMPh+hVzFidzAf3sbOlARQoBrMPPKpnJWtm\r\n6LEDf2lSwO36l0/bo6qDRmiFRJoHWytTJEjxVwRclVt4bXqHfNw9FKhZZbcKeAN2\r\noHgmBVSU6edHdwADBQP+OGAkEG4PcfSb8x191R+wkV/q2hA5Ay9z289Dx2rO28CO\r\n4M2fhhcjSmgr6x0DsrkfESCiG47UGJ169eu+QqJwk3HiF4crGN9rE5+VelBVFtrd\r\nMWkX2rPLGQWyw8iCZKbeH8g/ujmkaLovSmalzDcLe4v1xSLaP7Fnfzit0iIGZAGI\r\nRgQYEQIABgUCOjjU3QAKCRCvgiRPnNn9VVSaAJ9+rj1lIQnRl20i8Rom2Hwbe3re\r\n9QCfSYFnkZUw0yKF2DfCfqrDzdGAsbaIRgQYEQIABgUCOjjU3gAKCRCvgiRPnNn9\r\nVe4iAJ9FrGMlFR7s+GWf1scTeeyrthKrPQCfSpc/Yps72aFI7hPfyIa9MuerVZ4=\r\n=QRit\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n";
|
||||
|
||||
rusty_fork_test! {
|
||||
#[test]
|
||||
fn test_gpg_verify_sig() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
{
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
std::env::set_var("GNUPGHOME", tempdir.path());
|
||||
}
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
std::env::set_var("GPG_AGENT_INFO", "");
|
||||
}
|
||||
}
|
||||
|
||||
let mut ctx = Context::new_mock(&tempdir);
|
||||
let mut gpgme_ctx = match melib::gpgme::Context::new() {
|
||||
Ok(v) => v,
|
||||
Err(err) if err.kind.is_not_found() => {
|
||||
eprintln!("INFO: libgpgme could not be loaded, skipping this test.");
|
||||
return;
|
||||
}
|
||||
err => err.unwrap(),
|
||||
};
|
||||
let current_engine_info = gpgme_ctx.engine_info().unwrap();
|
||||
let prev_len = current_engine_info.len();
|
||||
let Some(EngineInfo {
|
||||
file_name: Some(engine_file_name),
|
||||
..
|
||||
}) = current_engine_info
|
||||
.into_iter()
|
||||
.find(|eng| eng.protocol == Protocol::OpenPGP)
|
||||
else {
|
||||
eprintln!("WARN: No openpg protocol engine returned from gpgme.");
|
||||
return;
|
||||
};
|
||||
gpgme_ctx
|
||||
.set_engine_info(
|
||||
Protocol::OpenPGP,
|
||||
Some(Cow::Owned(CString::new(engine_file_name).unwrap())),
|
||||
Some(Cow::Owned(
|
||||
CString::new(tempdir.path().display().to_string()).unwrap(),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
let new_engine_info = gpgme_ctx.engine_info().unwrap();
|
||||
assert_eq!(
|
||||
new_engine_info.len(),
|
||||
prev_len,
|
||||
"new_engine_info was expected to have {} entry/ies but has {}: {:#?}",
|
||||
prev_len,
|
||||
new_engine_info.len(),
|
||||
new_engine_info
|
||||
);
|
||||
assert_eq!(
|
||||
new_engine_info[0].home_dir,
|
||||
Some(tempdir.path().display().to_string()),
|
||||
"new_engine_info was expected to have temp dir as home_dir but has: {:#?}",
|
||||
new_engine_info[0].home_dir
|
||||
);
|
||||
let mut pubkey_data = Some(gpgme_ctx.new_data_mem(PUBKEY).unwrap());
|
||||
for _ in 0..2 {
|
||||
let mut key_sel = KeySelection::new_mock(
|
||||
false,
|
||||
true,
|
||||
"".to_string(),
|
||||
false.into(),
|
||||
&ctx,
|
||||
&mut gpgme_ctx,
|
||||
)
|
||||
.unwrap();
|
||||
let component_id = key_sel.id();
|
||||
|
||||
for _ in 0..2 {
|
||||
sleep(Duration::from_secs(2));
|
||||
}
|
||||
while let Ok(ev) = ctx.receiver.try_recv() {
|
||||
// if !matches!(ev, ThreadEvent::UIEvent(UIEvent::Timer(_))) {
|
||||
// dbg!(&ev);
|
||||
// }
|
||||
if let ThreadEvent::UIEvent(mut ev) = ev {
|
||||
key_sel.process_event(&mut ev, &mut ctx);
|
||||
} else if let ThreadEvent::JobFinished(job_id) = ev {
|
||||
let mut ev = UIEvent::StatusEvent(StatusEvent::JobFinished(job_id));
|
||||
key_sel.process_event(&mut ev, &mut ctx);
|
||||
}
|
||||
}
|
||||
if let Some(pubkey_data) = pubkey_data.take() {
|
||||
assert!(
|
||||
matches!(
|
||||
key_sel,
|
||||
KeySelection::Error {
|
||||
ref id,
|
||||
ref err
|
||||
} if *id == component_id && err.to_string() == melib::Error::new("No keys found.").to_string(),
|
||||
),
|
||||
"key_sel should have been an error but is: {:?}",
|
||||
key_sel
|
||||
);
|
||||
gpgme_ctx.import_key(pubkey_data).unwrap();
|
||||
} else {
|
||||
let assert_key = |key: &melib::gpgme::Key| {
|
||||
key.fingerprint() == "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55"
|
||||
&& key.primary_uid()
|
||||
== Some(melib::Address::new(
|
||||
Some("Joe Random Hacker".into()),
|
||||
"joe@example.com".into(),
|
||||
))
|
||||
&& key.can_encrypt()
|
||||
&& key.can_sign()
|
||||
&& !key.secret()
|
||||
&& !key.revoked()
|
||||
&& !key.expired()
|
||||
&& !key.invalid()
|
||||
};
|
||||
assert!(
|
||||
matches!(
|
||||
key_sel,
|
||||
KeySelection::Loaded {
|
||||
ref keys,
|
||||
widget: _,
|
||||
} if keys.len() == 1 && assert_key(&keys[0]),
|
||||
),
|
||||
"key_sel should have been an error but is: {:?}",
|
||||
key_sel
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue