mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
802 Commits
1.52.0
...
modseq-ski
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48eb400a69 | ||
|
|
8aa6decbf9 | ||
|
|
7cf4bcaca2 | ||
|
|
9ccd9c3e0e | ||
|
|
c6773a6303 | ||
|
|
e858a32aa1 | ||
|
|
99f2680e2c | ||
|
|
7a9a323bac | ||
|
|
62aa234352 | ||
|
|
0cb9e7922a | ||
|
|
e73107006e | ||
|
|
ca389cc6fc | ||
|
|
60ec7f0cbf | ||
|
|
d342d59e65 | ||
|
|
2690fa2da5 | ||
|
|
e411c394ca | ||
|
|
d69f3ba225 | ||
|
|
739807b1a9 | ||
|
|
d029ea7f3f | ||
|
|
11098cb869 | ||
|
|
f6807d6b22 | ||
|
|
7fc9bacf54 | ||
|
|
57ea4c1d92 | ||
|
|
bcdd15ef3a | ||
|
|
5f939c3123 | ||
|
|
2446fc44ad | ||
|
|
9ba8dd91df | ||
|
|
10e1cdbc52 | ||
|
|
46eceb38d5 | ||
|
|
81de882e2f | ||
|
|
593e07cdff | ||
|
|
8ca54f616e | ||
|
|
f7f899f0a4 | ||
|
|
05a3c0c89b | ||
|
|
f21691c122 | ||
|
|
836e26d8d0 | ||
|
|
8a7c1fe4cb | ||
|
|
7f43d3bb37 | ||
|
|
11b975ab19 | ||
|
|
315e4215d9 | ||
|
|
e35e6c44cf | ||
|
|
260cb78e3a | ||
|
|
a1f04d2129 | ||
|
|
9b562eebcd | ||
|
|
1d175c4557 | ||
|
|
f755070080 | ||
|
|
1c6c72a0fe | ||
|
|
1755f2ea3d | ||
|
|
498cc6c80b | ||
|
|
8d3227a92b | ||
|
|
c6d855084e | ||
|
|
827b3f8aeb | ||
|
|
fb95573000 | ||
|
|
f026bd455f | ||
|
|
f0b92a5757 | ||
|
|
d7c6f1e63b | ||
|
|
1e9e308df3 | ||
|
|
c9a70f149d | ||
|
|
d4ff47b6ac | ||
|
|
8b4b241403 | ||
|
|
6dcd6947d7 | ||
|
|
327328412a | ||
|
|
42f9ef00b9 | ||
|
|
8c2ea0fa26 | ||
|
|
14e9afaf42 | ||
|
|
a3a101641a | ||
|
|
8bd93fe495 | ||
|
|
56df22bca7 | ||
|
|
5f32a6738a | ||
|
|
1c081935fb | ||
|
|
8fd4d00776 | ||
|
|
7d04ea58c3 | ||
|
|
2cc84a0f0d | ||
|
|
8f715532cb | ||
|
|
5a77df7cc5 | ||
|
|
59658f2b0b | ||
|
|
0b983906da | ||
|
|
cd1f164d18 | ||
|
|
e2a6ac6625 | ||
|
|
8e8c10c438 | ||
|
|
7ff25f282e | ||
|
|
b8dc608032 | ||
|
|
d7e699320b | ||
|
|
ef333da770 | ||
|
|
575a389b08 | ||
|
|
9bc0824be6 | ||
|
|
b656a60234 | ||
|
|
bd988d805c | ||
|
|
7ad7ccb8fe | ||
|
|
e30c535f18 | ||
|
|
de7706f622 | ||
|
|
de20e4c9dd | ||
|
|
41f9314e2a | ||
|
|
2280ce349a | ||
|
|
7aa05e1c9f | ||
|
|
69d174c9e8 | ||
|
|
3c38fa6b70 | ||
|
|
728c8b4663 | ||
|
|
fab9563cd5 | ||
|
|
20fe2473e1 | ||
|
|
3815062c11 | ||
|
|
581ea9fda0 | ||
|
|
bfa641cea8 | ||
|
|
29c58efeb3 | ||
|
|
27eb82c556 | ||
|
|
652d67a20f | ||
|
|
ce0984f02f | ||
|
|
b3e3b1e245 | ||
|
|
c69ee180af | ||
|
|
bba3a25371 | ||
|
|
095b358aca | ||
|
|
833e5f46cc | ||
|
|
3e0ce0e07a | ||
|
|
1f31dd12fc | ||
|
|
f63efc29bf | ||
|
|
3e394f14e8 | ||
|
|
ff6ffa1656 | ||
|
|
304c259a57 | ||
|
|
630754b52e | ||
|
|
afd8c0d879 | ||
|
|
6316ee7c9b | ||
|
|
b6b8d11881 | ||
|
|
8753fd5887 | ||
|
|
5aaafb5ac1 | ||
|
|
a043557c44 | ||
|
|
4af4914e32 | ||
|
|
e35b3f1e80 | ||
|
|
ff8859b9db | ||
|
|
937ff5a378 | ||
|
|
f3a716fac6 | ||
|
|
72659580de | ||
|
|
30cb0cbcfd | ||
|
|
4136217249 | ||
|
|
246cae5d9e | ||
|
|
12313543ca | ||
|
|
87e3dead14 | ||
|
|
9af36460c2 | ||
|
|
2e2d881e01 | ||
|
|
db58946312 | ||
|
|
9a02a58273 | ||
|
|
147f5c1e0d | ||
|
|
f0ca50ba27 | ||
|
|
83137b5968 | ||
|
|
12823c2213 | ||
|
|
0b810d7d65 | ||
|
|
7aebdc9b7b | ||
|
|
6859b651a8 | ||
|
|
d47680733b | ||
|
|
273a38d781 | ||
|
|
93d1162caf | ||
|
|
8d550a66a3 | ||
|
|
4c58e05be3 | ||
|
|
500563054e | ||
|
|
2a11f8f59d | ||
|
|
9e7bdc579e | ||
|
|
61af0c9ac4 | ||
|
|
91f02ad553 | ||
|
|
b20d3dfc10 | ||
|
|
3e7666021c | ||
|
|
ae58bceeb9 | ||
|
|
6665b9e13c | ||
|
|
367a9705e9 | ||
|
|
8d3a1e84c3 | ||
|
|
d009835210 | ||
|
|
83a664ca68 | ||
|
|
d98d1857a4 | ||
|
|
33a514aa54 | ||
|
|
0aefdc85e6 | ||
|
|
3ff0964f02 | ||
|
|
cf33db3dcb | ||
|
|
dae80cbe35 | ||
|
|
f8b4ef26b3 | ||
|
|
db991453b0 | ||
|
|
c11ce4c8d4 | ||
|
|
92e300cb9f | ||
|
|
d210e0bffe | ||
|
|
4d4968f358 | ||
|
|
8dfede148a | ||
|
|
6d125028f5 | ||
|
|
7ff3cf4af0 | ||
|
|
bb3353397d | ||
|
|
572260ec29 | ||
|
|
0b1faa0523 | ||
|
|
3b6c3e10d7 | ||
|
|
d3909a5483 | ||
|
|
645fd10446 | ||
|
|
ee7e29fb3a | ||
|
|
bdc3a4d24c | ||
|
|
88ccda139e | ||
|
|
63e78bae37 | ||
|
|
b50f211c28 | ||
|
|
5efbd9c7f5 | ||
|
|
7f1d2ea11f | ||
|
|
46eb391a1b | ||
|
|
b166cc5bf4 | ||
|
|
1d0f6aad95 | ||
|
|
764aa71770 | ||
|
|
21e9206a77 | ||
|
|
5f29977c50 | ||
|
|
ee4c9bc01e | ||
|
|
4a8259cb12 | ||
|
|
a6441d70f0 | ||
|
|
01db8d0130 | ||
|
|
8ad9db5572 | ||
|
|
607cd23014 | ||
|
|
220758d244 | ||
|
|
7ab71bb468 | ||
|
|
a74377b620 | ||
|
|
c9effa3c06 | ||
|
|
8b8102334b | ||
|
|
5dedb86472 | ||
|
|
d93f77f991 | ||
|
|
5409bc575e | ||
|
|
576d31206d | ||
|
|
86e3297414 | ||
|
|
ab4a947456 | ||
|
|
5ce2581b4c | ||
|
|
c0d6c6b882 | ||
|
|
2bc8c967b1 | ||
|
|
fee08f0eeb | ||
|
|
078c3d05d7 | ||
|
|
7f97768c56 | ||
|
|
ecd548a7aa | ||
|
|
62efb0795b | ||
|
|
53f042ee08 | ||
|
|
6ce97bd0cd | ||
|
|
c29149e74c | ||
|
|
487f7593ce | ||
|
|
6b3b33d2a0 | ||
|
|
2d70ccc2bf | ||
|
|
e90fc9504a | ||
|
|
5108314c03 | ||
|
|
c1e144d4db | ||
|
|
f2b86a1c0f | ||
|
|
9583d41446 | ||
|
|
e594064f93 | ||
|
|
7c52fd95ec | ||
|
|
d0d5ac6e87 | ||
|
|
416bf3a829 | ||
|
|
cc78347293 | ||
|
|
27e4ae992a | ||
|
|
a1767dc153 | ||
|
|
eb610c27bf | ||
|
|
016fb2ceb2 | ||
|
|
5c571520a0 | ||
|
|
ddefd2cf09 | ||
|
|
30a3eeece8 | ||
|
|
5919388588 | ||
|
|
2cc738f481 | ||
|
|
babd405928 | ||
|
|
e885857875 | ||
|
|
a1d57a2645 | ||
|
|
a28aecd4d1 | ||
|
|
a3f1ff2827 | ||
|
|
c4d1a639b0 | ||
|
|
8732b7a55c | ||
|
|
1b9148f28e | ||
|
|
e0129c3b43 | ||
|
|
60d41022ea | ||
|
|
dd4f2ac671 | ||
|
|
eebb2a3b68 | ||
|
|
0d62069b67 | ||
|
|
56cf2e6596 | ||
|
|
59bd5481b9 | ||
|
|
6c8da526a0 | ||
|
|
13bc8b78d7 | ||
|
|
c7c68094d9 | ||
|
|
84f54b10dc | ||
|
|
cebc9e3e91 | ||
|
|
1379f8a055 | ||
|
|
53d049e5f5 | ||
|
|
4968f72dfb | ||
|
|
b24a0ed8fd | ||
|
|
c810347c7c | ||
|
|
cf8c4142c7 | ||
|
|
f71901b5f9 | ||
|
|
4419f9c4e7 | ||
|
|
4e5982b682 | ||
|
|
39e1510e64 | ||
|
|
64206160cc | ||
|
|
6376659348 | ||
|
|
43b2a4ad27 | ||
|
|
eaf06bb239 | ||
|
|
5e26b5bfdc | ||
|
|
60fbb6df5a | ||
|
|
3e60ee9d3e | ||
|
|
54e79409e6 | ||
|
|
31d113207b | ||
|
|
3a014477e7 | ||
|
|
90d8c8baf5 | ||
|
|
1dee17f980 | ||
|
|
6aeb21d3af | ||
|
|
4747ae2f1c | ||
|
|
7968f55191 | ||
|
|
33aa3556d2 | ||
|
|
b8b7563fca | ||
|
|
0d3f90770e | ||
|
|
51a4f0aa76 | ||
|
|
ebb89e20b4 | ||
|
|
8fc60e321b | ||
|
|
8ef6b6089f | ||
|
|
5b5b26122e | ||
|
|
300f5be4f3 | ||
|
|
c5d47ffcb0 | ||
|
|
3b7b8ea0f1 | ||
|
|
59739ee5c9 | ||
|
|
63207eb681 | ||
|
|
65f09c238b | ||
|
|
bb97d842df | ||
|
|
4dba5ab5f9 | ||
|
|
4c0e46fd44 | ||
|
|
ee3b40a59a | ||
|
|
e511b87955 | ||
|
|
53f51ad312 | ||
|
|
3878c4f041 | ||
|
|
499e4d3242 | ||
|
|
b5d0907090 | ||
|
|
6613fa67ee | ||
|
|
41ec380b55 | ||
|
|
7fb305e898 | ||
|
|
d4255a4979 | ||
|
|
b21dcd17b7 | ||
|
|
0caea85d16 | ||
|
|
42e0fb5eb9 | ||
|
|
6061d71492 | ||
|
|
dbd8814d2c | ||
|
|
a3562c5940 | ||
|
|
49b07c1c6a | ||
|
|
51d220f1e0 | ||
|
|
9f81a94d86 | ||
|
|
f6098fc931 | ||
|
|
6e3c2fc839 | ||
|
|
30e616f74f | ||
|
|
5e29cae81a | ||
|
|
1ee19bf3ca | ||
|
|
b18bdd1b00 | ||
|
|
6c59b0de85 | ||
|
|
c1d82ad417 | ||
|
|
ba931773d1 | ||
|
|
b6f88a9fca | ||
|
|
b0902102a2 | ||
|
|
4f19036408 | ||
|
|
fe1f9c0ed9 | ||
|
|
bcadd0cd5c | ||
|
|
30a3da97da | ||
|
|
a8b2a20146 | ||
|
|
82819a642f | ||
|
|
3960d4129e | ||
|
|
e405ddf080 | ||
|
|
1eadbbb7cd | ||
|
|
941b8caa8b | ||
|
|
95bce993ad | ||
|
|
acbf363fc8 | ||
|
|
2309c7ca13 | ||
|
|
89d8b26192 | ||
|
|
ee32a7b00a | ||
|
|
1dbbf6b3be | ||
|
|
f8a4a88fb2 | ||
|
|
3096193d58 | ||
|
|
d8b47dc4aa | ||
|
|
a5826d6a06 | ||
|
|
5df0be8311 | ||
|
|
66a5e0743d | ||
|
|
43d1d9b1b3 | ||
|
|
3e0f601212 | ||
|
|
085a899de2 | ||
|
|
b07e20b955 | ||
|
|
4e8724694a | ||
|
|
47bf67e658 | ||
|
|
7bb7748b6b | ||
|
|
b33ad05c3b | ||
|
|
398cea6466 | ||
|
|
1afd2f2d66 | ||
|
|
48f1ef3641 | ||
|
|
e95911a484 | ||
|
|
b1af486e10 | ||
|
|
bffb41326c | ||
|
|
c532055153 | ||
|
|
be595f8601 | ||
|
|
1d1d98e02b | ||
|
|
771e84af6e | ||
|
|
bbfed20d34 | ||
|
|
0f2095947c | ||
|
|
46956caf75 | ||
|
|
6f3dd7f0c2 | ||
|
|
15dcd62652 | ||
|
|
da2f30786b | ||
|
|
50a5e715d2 | ||
|
|
1bef623c89 | ||
|
|
7745db8310 | ||
|
|
1d1491c95d | ||
|
|
2a0f6f5cf7 | ||
|
|
b27793e852 | ||
|
|
8fb5e038a9 | ||
|
|
e518dc3331 | ||
|
|
ea1368a36b | ||
|
|
0aeb2bd6fb | ||
|
|
0263d0816a | ||
|
|
bb71f6ec98 | ||
|
|
02a1abc0d5 | ||
|
|
40fe65716f | ||
|
|
d05b399eac | ||
|
|
c31216f043 | ||
|
|
f66bde7275 | ||
|
|
7f819de49f | ||
|
|
5f065b245f | ||
|
|
3c43d790a3 | ||
|
|
d33177a721 | ||
|
|
aa2e03382b | ||
|
|
2a59e6121b | ||
|
|
1a438d61df | ||
|
|
444486f5df | ||
|
|
1eae2477c3 | ||
|
|
7b3eefc6c6 | ||
|
|
4dd0830baf | ||
|
|
8e3f062881 | ||
|
|
cf445f265a | ||
|
|
963c66b76c | ||
|
|
79df667e1e | ||
|
|
785c796bd6 | ||
|
|
6a2112ba66 | ||
|
|
3f170279da | ||
|
|
3408501a75 | ||
|
|
3b765cb3c9 | ||
|
|
8a9ea388ed | ||
|
|
77acf910bf | ||
|
|
c04c87658c | ||
|
|
fd784ec223 | ||
|
|
25f1b0c4af | ||
|
|
580ec6e6ce | ||
|
|
8e5195c4f6 | ||
|
|
729a1e1cd2 | ||
|
|
78b93f3621 | ||
|
|
4111489daf | ||
|
|
b7bd4c6ba7 | ||
|
|
83dc0bc2b0 | ||
|
|
51c6467feb | ||
|
|
6a60ae2f09 | ||
|
|
7be0583628 | ||
|
|
2b74a705ef | ||
|
|
9dedcad220 | ||
|
|
71e0493c4a | ||
|
|
1679ddddf0 | ||
|
|
de258645f4 | ||
|
|
b463b602a9 | ||
|
|
3aa2b57ac1 | ||
|
|
ab1de69fbc | ||
|
|
90703b0dd2 | ||
|
|
a163be9248 | ||
|
|
2b7bf11b05 | ||
|
|
f95e1db8e2 | ||
|
|
d0c97bce4c | ||
|
|
3440daca1a | ||
|
|
d0bfb555dd | ||
|
|
6ffaa38b37 | ||
|
|
339d46ecf0 | ||
|
|
5399c9151d | ||
|
|
53cd633e8d | ||
|
|
ade39fe026 | ||
|
|
b8dad1dbaf | ||
|
|
72d503fa32 | ||
|
|
223aeb7b1a | ||
|
|
b315c6f6d5 | ||
|
|
481276cf46 | ||
|
|
faab61b0d4 | ||
|
|
20bf41b4e6 | ||
|
|
5a5b80c960 | ||
|
|
ac245a6cb2 | ||
|
|
126beb62f3 | ||
|
|
1f642046bc | ||
|
|
d79e4a6571 | ||
|
|
cadc0b2c00 | ||
|
|
87071e6d4b | ||
|
|
057b004553 | ||
|
|
4071fe53a0 | ||
|
|
c3062976c0 | ||
|
|
85efc0ea26 | ||
|
|
4ef80aaea5 | ||
|
|
0f86800f5d | ||
|
|
9c2035538c | ||
|
|
0276938975 | ||
|
|
ee44a162b6 | ||
|
|
c09a83df2b | ||
|
|
8729d2c4aa | ||
|
|
fdf3397437 | ||
|
|
3712524765 | ||
|
|
0b5c4df432 | ||
|
|
0f0072f5a2 | ||
|
|
09066571be | ||
|
|
8963dab7a4 | ||
|
|
265d54e431 | ||
|
|
ffb17c4e61 | ||
|
|
44bd9f93b4 | ||
|
|
5287a3de40 | ||
|
|
6f644f5c7c | ||
|
|
31b930b2fa | ||
|
|
0574aeb768 | ||
|
|
bf68bc14a4 | ||
|
|
c380647c12 | ||
|
|
e22a9999d7 | ||
|
|
57870ec54a | ||
|
|
a6e1dc4f16 | ||
|
|
fc441d4a44 | ||
|
|
9a77a7b66f | ||
|
|
5856936f49 | ||
|
|
532060d8b7 | ||
|
|
0691aa3d2c | ||
|
|
ef9fbf9eba | ||
|
|
3647aac4e6 | ||
|
|
f88f4155ae | ||
|
|
065b574d93 | ||
|
|
5c36b6e119 | ||
|
|
cd0da723ce | ||
|
|
49fc72fa42 | ||
|
|
a1aaa1e0b4 | ||
|
|
1eab99df56 | ||
|
|
d9caf5853d | ||
|
|
8869c34539 | ||
|
|
05bb25c645 | ||
|
|
b340459752 | ||
|
|
980d2a9433 | ||
|
|
5f365b259b | ||
|
|
b070198063 | ||
|
|
6e7f63dba7 | ||
|
|
eff64ed9b0 | ||
|
|
49acfd90eb | ||
|
|
aec8332544 | ||
|
|
188353d581 | ||
|
|
3bd5b7e604 | ||
|
|
61e1e18088 | ||
|
|
a5065c21af | ||
|
|
cd958c6a33 | ||
|
|
308403ad99 | ||
|
|
599be61566 | ||
|
|
64088f02a2 | ||
|
|
77aa8b2c3f | ||
|
|
5bffdc6bbf | ||
|
|
350fe06ea9 | ||
|
|
e100dca348 | ||
|
|
f1c4c40aec | ||
|
|
f96d04e80f | ||
|
|
c1d3e9358d | ||
|
|
e77651f2f5 | ||
|
|
056f3ecf03 | ||
|
|
8700cf0aba | ||
|
|
a6ad457065 | ||
|
|
f113b43046 | ||
|
|
0b3eece26d | ||
|
|
8ce9a78d6c | ||
|
|
ad266fe82f | ||
|
|
15c38ba395 | ||
|
|
70e776e407 | ||
|
|
6b5ba35d5b | ||
|
|
7b9e54be56 | ||
|
|
6202f85a6f | ||
|
|
8ac2bd0298 | ||
|
|
3f00a6efbe | ||
|
|
a411fe1e01 | ||
|
|
8ea773628d | ||
|
|
a47c0486ae | ||
|
|
c08df8d3da | ||
|
|
1a830c23b5 | ||
|
|
18ace81842 | ||
|
|
838957badd | ||
|
|
f820671d53 | ||
|
|
bf61c16dc1 | ||
|
|
96f0e47091 | ||
|
|
514c4bc8a7 | ||
|
|
b53613d1e0 | ||
|
|
4c4f24fb35 | ||
|
|
475fa24876 | ||
|
|
cf8736da48 | ||
|
|
a638259c36 | ||
|
|
d821cdf1c8 | ||
|
|
62e9fbf68c | ||
|
|
15664be4f6 | ||
|
|
62388514dd | ||
|
|
ad7c7e90b3 | ||
|
|
b16785bb62 | ||
|
|
d12d9d94d6 | ||
|
|
991d15615e | ||
|
|
5dee1efa59 | ||
|
|
1870684c43 | ||
|
|
1803db2dfe | ||
|
|
7fee3d995c | ||
|
|
4b62500989 | ||
|
|
8f2cb1e8ab | ||
|
|
72ebd83479 | ||
|
|
2842042304 | ||
|
|
25fed9ab52 | ||
|
|
751b9add09 | ||
|
|
b727190da5 | ||
|
|
368fa9fc44 | ||
|
|
c07c5bb358 | ||
|
|
67f8fb4b66 | ||
|
|
056721b916 | ||
|
|
4fe3a80f96 | ||
|
|
6c530b4c77 | ||
|
|
c616b65ce4 | ||
|
|
50f680a00b | ||
|
|
47e0f224ca | ||
|
|
8b872b7e6f | ||
|
|
29356a6ca8 | ||
|
|
c5539de4da | ||
|
|
b017af78ce | ||
|
|
3b897eac53 | ||
|
|
d8a3014896 | ||
|
|
4209960c0f | ||
|
|
04c8622e94 | ||
|
|
002e33d28c | ||
|
|
35aeda3849 | ||
|
|
af287ee9a8 | ||
|
|
cc3e8c5117 | ||
|
|
1127521923 | ||
|
|
bf7f64d50b | ||
|
|
8380ac28c1 | ||
|
|
d8ba466c6a | ||
|
|
0c2a3c8347 | ||
|
|
e3e2adeea5 | ||
|
|
46e901be78 | ||
|
|
d899a38d17 | ||
|
|
b60994b313 | ||
|
|
8b19b6f9fe | ||
|
|
ea88a567f9 | ||
|
|
38c23104da | ||
|
|
2a83147d90 | ||
|
|
7e3bfd38f0 | ||
|
|
8b78e12b36 | ||
|
|
15660f2741 | ||
|
|
03520fbd4b | ||
|
|
b1228cbbe5 | ||
|
|
09f5b015bb | ||
|
|
67ca93b093 | ||
|
|
dabf31204c | ||
|
|
c22580e07f | ||
|
|
0028f579b6 | ||
|
|
9522240992 | ||
|
|
8ab3415c58 | ||
|
|
e858fca356 | ||
|
|
7d4affcc8d | ||
|
|
60d596bb0e | ||
|
|
c57bfde010 | ||
|
|
afd7e7eaac | ||
|
|
cfc324c95b | ||
|
|
aa5d6077a8 | ||
|
|
4a2beac6fc | ||
|
|
b5dc954408 | ||
|
|
a9f5077cf9 | ||
|
|
09280508bc | ||
|
|
4391835a8d | ||
|
|
05f9ac0583 | ||
|
|
fd9d632cd6 | ||
|
|
c9e626322b | ||
|
|
f343ec47b4 | ||
|
|
efb1534d5c | ||
|
|
6ec765cad6 | ||
|
|
372a4ee539 | ||
|
|
418b591602 | ||
|
|
e387b4f4dd | ||
|
|
88a10eaf2c | ||
|
|
4aae12ead7 | ||
|
|
89c985120b | ||
|
|
40bb0616da | ||
|
|
2b81c274f1 | ||
|
|
0266b70b23 | ||
|
|
9c0d84090e | ||
|
|
baf7e98c1e | ||
|
|
2d20a81f22 | ||
|
|
4be4472dfb | ||
|
|
98c1158cde | ||
|
|
3769ad32bd | ||
|
|
1c436777e0 | ||
|
|
adac903818 | ||
|
|
03f0659454 | ||
|
|
296c230bc9 | ||
|
|
ffd00978e9 | ||
|
|
a8f58ec2cf | ||
|
|
d8a2c05c71 | ||
|
|
7e4386c197 | ||
|
|
f595264418 | ||
|
|
f3e8f5babc | ||
|
|
d7b4a5fc9e | ||
|
|
be413b20f1 | ||
|
|
99d9773b75 | ||
|
|
633929b84c | ||
|
|
f42da17a78 | ||
|
|
d421670477 | ||
|
|
c4f36836d4 | ||
|
|
553f4c4b88 | ||
|
|
30c463e0ba | ||
|
|
d5c1e26354 | ||
|
|
b7864f232b | ||
|
|
8e9d8ae1ec | ||
|
|
f52c23d1c7 | ||
|
|
957f942872 | ||
|
|
6971bfc3d4 | ||
|
|
16dcd712f0 | ||
|
|
9f337e8be5 | ||
|
|
c4217ea929 | ||
|
|
3a742f1d09 | ||
|
|
ae0dbf024d | ||
|
|
01d3611f3b | ||
|
|
f1608b503f | ||
|
|
98beb7f40c | ||
|
|
574bb8fd7f | ||
|
|
f5de2e7684 | ||
|
|
42086ceec5 | ||
|
|
cfb22c23df | ||
|
|
d49de4b3e4 | ||
|
|
540ad71473 | ||
|
|
b27ad955f8 | ||
|
|
5546ed772e | ||
|
|
23e891f051 | ||
|
|
7dd5b05a00 | ||
|
|
b7d274e0f9 | ||
|
|
437b7ef1f1 | ||
|
|
6934947d0d | ||
|
|
d920ec96fa | ||
|
|
ebfeec8907 | ||
|
|
6d064dca84 | ||
|
|
2c2fad6f28 | ||
|
|
60b4f3f21a | ||
|
|
c128e54896 | ||
|
|
0ea6f72624 | ||
|
|
855b6b18fd | ||
|
|
abac35c872 | ||
|
|
17ad4e99ee | ||
|
|
c5aef03008 | ||
|
|
c7f2a43654 | ||
|
|
19176d9d47 | ||
|
|
db1a7023eb | ||
|
|
ae31b5895b | ||
|
|
35b6dd797d | ||
|
|
1d708de82f | ||
|
|
f7139331e7 | ||
|
|
131651cc02 | ||
|
|
bba437523a | ||
|
|
f76bc44cdc | ||
|
|
f6eb169c60 | ||
|
|
e15ec2eb7a | ||
|
|
b3b46688fc | ||
|
|
9faf4a5fa7 | ||
|
|
628c30f130 | ||
|
|
f40b557454 | ||
|
|
e1b9e8f2c9 | ||
|
|
65c17cfea2 | ||
|
|
39d3a594af | ||
|
|
949e671d9c | ||
|
|
eef51f064a | ||
|
|
143c5e6249 | ||
|
|
8610b0c945 | ||
|
|
d179dced4e | ||
|
|
1dc055fb66 | ||
|
|
819775ac39 | ||
|
|
a1ef32170d | ||
|
|
a4486d8c30 | ||
|
|
7bdae8b2c5 | ||
|
|
75999c5d5a | ||
|
|
34ffa4e7ea | ||
|
|
3f1623eab1 | ||
|
|
99373774aa | ||
|
|
acd51a7058 | ||
|
|
61bf0b208c | ||
|
|
efd0314872 | ||
|
|
ef89bc64c9 | ||
|
|
6d4ec75a7b | ||
|
|
8af47de5a4 | ||
|
|
c7345c16f8 | ||
|
|
a4b14c6b98 | ||
|
|
321354531d | ||
|
|
5b0f07f9a7 | ||
|
|
87cb5de8b1 | ||
|
|
baae31117f | ||
|
|
553d3936a9 | ||
|
|
004fb76864 | ||
|
|
09bc8fc603 | ||
|
|
8f1bb38a3b | ||
|
|
2688f233b8 | ||
|
|
7be8fb7245 | ||
|
|
a9b8776342 | ||
|
|
9a34fe5c70 | ||
|
|
e35a8d4415 | ||
|
|
59dea29e88 | ||
|
|
cfdc841c7e | ||
|
|
2974affaeb | ||
|
|
69dae4c006 | ||
|
|
a795ae98ee | ||
|
|
ac54301923 | ||
|
|
9ecb6d9b15 | ||
|
|
ac9394cb16 | ||
|
|
edb9ea0e83 | ||
|
|
4c1315446e | ||
|
|
e6d2b1052c | ||
|
|
19c1e6efc3 | ||
|
|
26d9addc5d | ||
|
|
6601015a09 | ||
|
|
36653928f7 | ||
|
|
2e015e685f | ||
|
|
d4a1858d41 | ||
|
|
d6a6ba01e4 | ||
|
|
27714f596e | ||
|
|
dc6fb7d481 |
@@ -1,68 +0,0 @@
|
||||
version: 2.1
|
||||
executors:
|
||||
default:
|
||||
docker:
|
||||
- image: filecoin/rust:latest
|
||||
working_directory: /mnt/crate
|
||||
doxygen:
|
||||
docker:
|
||||
- image: hrektts/doxygen
|
||||
|
||||
jobs:
|
||||
build_doxygen:
|
||||
executor: doxygen
|
||||
steps:
|
||||
- checkout
|
||||
- run: bash ci_scripts/run-doxygen.sh
|
||||
- run: mkdir -p workspace/c-docs
|
||||
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
- c-docs
|
||||
|
||||
remote_python_packaging:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
# the following commands on success produces
|
||||
# workspace/{wheelhouse,py-docs} as artefact directories
|
||||
- run: bash ci_scripts/remote_python_packaging.sh
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
# - c-docs
|
||||
- py-docs
|
||||
- wheelhouse
|
||||
|
||||
upload_docs_wheels:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: workspace
|
||||
- run: ls -laR workspace
|
||||
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||
|
||||
workflows:
|
||||
version: 2.1
|
||||
|
||||
test:
|
||||
jobs:
|
||||
- remote_python_packaging:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
|
||||
- upload_docs_wheels:
|
||||
requires:
|
||||
- remote_python_packaging
|
||||
- build_doxygen
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "cargo"
|
||||
open-pull-requests-limit: 10
|
||||
26
.github/mergeable.yml
vendored
Normal file
26
.github/mergeable.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
version: 2
|
||||
mergeable:
|
||||
- when: pull_request.*
|
||||
name: "Changelog check"
|
||||
validate:
|
||||
- do: or
|
||||
validate:
|
||||
- do: description
|
||||
must_include:
|
||||
regex: '#skip-changelog'
|
||||
- do: and
|
||||
validate:
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'src/**'
|
||||
required: ['CHANGELOG.md']
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'deltachat-ffi/**'
|
||||
required: ['CHANGELOG.md']
|
||||
fail:
|
||||
- do: checks
|
||||
status: 'action_required'
|
||||
payload:
|
||||
title: Changlog might need an update
|
||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
||||
94
.github/workflows/ci.yml
vendored
94
.github/workflows/ci.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
- staging
|
||||
- trying
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
@@ -18,9 +21,11 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.50.0
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
@@ -32,9 +37,11 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.50.0
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -65,26 +72,26 @@ jobs:
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
matrix:
|
||||
# macOS disabled due to random failures related to caching
|
||||
#os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
rust: [1.50.0]
|
||||
experimental: [false]
|
||||
# include:
|
||||
# - os: ubuntu-latest
|
||||
# rust: nightly
|
||||
# experimental: true
|
||||
# - os: windows-latest
|
||||
# rust: nightly
|
||||
# experimental: true
|
||||
# - os: macOS-latest
|
||||
# rust: nightly
|
||||
# experimental: true
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.54.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.54.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.51.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.51.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
@@ -94,32 +101,43 @@ jobs:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl --benches
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
- name: install python
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: install tox
|
||||
if: ${{ matrix.python }}
|
||||
run: pip install tox
|
||||
|
||||
- name: build C library
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: -p deltachat_ffi
|
||||
|
||||
- name: run python tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
21
.github/workflows/remote_tests.yml
vendored
21
.github/workflows/remote_tests.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: Remote tests
|
||||
on: [push]
|
||||
jobs:
|
||||
remote_tests_python:
|
||||
name: Remote Python tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CIRCLE_BRANCH: ${{ github.ref }}
|
||||
CIRCLE_JOB: remote_tests_python
|
||||
CIRCLE_BUILD_NUM: ${{ github.run_number }}
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: mkdir -m 700 -p ~/.ssh
|
||||
- run: touch ~/.ssh/id_ed25519
|
||||
- run: chmod 600 ~/.ssh/id_ed25519
|
||||
- run: 'echo "$SSH_KEY" | base64 -d > ~/.ssh/id_ed25519'
|
||||
shell: bash
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||
- run: ci_scripts/remote_tests_python.sh
|
||||
32
.github/workflows/repl.yml
vendored
Normal file
32
.github/workflows/repl.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Manually triggered action to build a Windows repl.exe which users can
|
||||
# download to debug complex bugs.
|
||||
|
||||
name: Build Windows REPL .exe
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_repl:
|
||||
name: Build REPL example
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.50.0
|
||||
override: true
|
||||
|
||||
- name: build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --example repl --features repl,vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: repl.exe
|
||||
path: 'target/debug/examples/repl.exe'
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,3 +26,6 @@ deltachat-ffi/html
|
||||
deltachat-ffi/xml
|
||||
|
||||
.rsynclist
|
||||
|
||||
coverage/
|
||||
.DS_Store
|
||||
|
||||
434
CHANGELOG.md
434
CHANGELOG.md
@@ -1,4 +1,436 @@
|
||||
# Changelog
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
- don't watch Sent folder by default #3025
|
||||
- use webxdc app name in chatlist/quotes/replies etc. #3027
|
||||
- refactorings #3023
|
||||
- remove direct dependency on `byteorder` crate #3031
|
||||
- make it possible to cancel message sending by removing the message #3034,
|
||||
this was previosuly removed in 1.71.0 #2939
|
||||
- always skip Seen flag synchronization when there are no updates #3039
|
||||
|
||||
### Fixes
|
||||
- fix splitting off text from webxdc messages #3032
|
||||
- call slow `delete_expired_imap_messages()` less often #3037
|
||||
- make synchronization of Seen status more robust in case unsolicited FETCH
|
||||
result without UID is returned #3022
|
||||
|
||||
|
||||
## 1.72.0
|
||||
|
||||
### Fixes
|
||||
- run migrations on backup import #3006
|
||||
|
||||
|
||||
## 1.71.0
|
||||
|
||||
### API Changes
|
||||
- added APIs to handle database passwords: `dc_context_new_closed()`, `dc_context_open()`,
|
||||
`dc_context_is_open()` and `dc_accounts_add_closed_account()` #2956 #2972
|
||||
- use second parameter of `dc_imex` to provide backup passphrase #2980
|
||||
- added `DC_MSG_WEBXDC`, `dc_send_webxdc_status_update()`,
|
||||
`dc_get_webxdc_status_updates()`, `dc_msg_get_webxdc_blob()`, `dc_msg_get_webxdc_info()`
|
||||
and `DC_EVENT_WEBXDC_STATUS_UPDATE` #2826 #2971 #2975 #2977 #2979 #2993 #2994 #2998 #3001 #3003
|
||||
- added `dc_msg_get_parent()` #2984
|
||||
- added `dc_msg_force_plaintext()` API for bots #2847
|
||||
- allow removing quotes on drafts `dc_msg_set_quote(msg, NULL)` #2950
|
||||
- removed `mvbox_watch` option; watching is enabled when `mvbox_move` is enabled #2906
|
||||
- removed `inbox_watch` option #2922
|
||||
- deprecated `os_name` in `dc_context_new()`, pass `NULL` or an empty string #2956
|
||||
|
||||
### Changes
|
||||
- start making it possible to write to mailing lists #2736
|
||||
- add `hop_info` to `dc_get_info()` #2751 #2914 #2923
|
||||
- add information about whether the database is encrypted or not to `dc_get_info()` #3000
|
||||
- selfstatus now defaults to empty #2951 #2960
|
||||
- validate detached cryptographic signatures as used eg. by Thunderbird #2865
|
||||
- do not change the draft's `msg_id` on updates and sending #2887
|
||||
- add `imap` table to keep track of message UIDs #2909 #2938
|
||||
- replace `SendMsgToSmtp` jobs which stored outgoing messages in blobdir with `smtp` SQL table #2939 #2996
|
||||
- sql: enable `auto_vacuum=INCREMENTAL` #2931
|
||||
- sql: build rusqlite with sqlcipher #2934
|
||||
- synchronize Seen status across devices #2942
|
||||
- `dc_preconfigure_keypair` now takes ascii armored keys instead of base64 #2862
|
||||
- put removed member in Bcc instead of To in the message about removal #2864
|
||||
- improve group updates #2889
|
||||
- re-write the blob filename creation loop #2981
|
||||
- update provider database (11 Jan 2022) #2959
|
||||
- python: allow timeout for internal configure tracker API #2967
|
||||
- python: remove API deprecated in Python 3.10 #2907
|
||||
- refactorings #2932 #2957 #2947
|
||||
- improve tests #2863 #2866 #2881 #2908 #2918 #2901 #2973
|
||||
- improve documentation #2880 #2886 #2895
|
||||
- improve ci #2919 #2926 #2969 #2999
|
||||
|
||||
### Fixes
|
||||
- fix leaving groups #2929
|
||||
- fix unread count #2861
|
||||
- make `add_parts()` not early-exit #2879
|
||||
- recognize MS Exchange read receipts as read receipts #2890
|
||||
- create parent directory if creating a new file fails #2978
|
||||
- save "configured" flag later #2974
|
||||
- improve log #2928
|
||||
- `dc_receive_imf`: don't fail on invalid address in the To field #2940
|
||||
|
||||
|
||||
## 1.70.0
|
||||
|
||||
### Fixes
|
||||
- fix: do not abort Param parsing on unknown keys #2856
|
||||
- fix: execute `Chat-Group-Member-Removed:` even when arriving disordered #2857
|
||||
|
||||
|
||||
## 1.69.0
|
||||
|
||||
### Fixes
|
||||
- fix group-related system messages in multi-device setups #2848
|
||||
- fix "Google Workspace" (former "G Suite") issues related to bad resolvers #2852
|
||||
|
||||
|
||||
## 1.68.0
|
||||
|
||||
### Fixes
|
||||
- fix chat assignment when forwarding #2843
|
||||
- fix layout issues with the generated QR code svg #2842
|
||||
|
||||
|
||||
## 1.67.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_securejoin_qr_svg(chat_id)` added #2815
|
||||
- added stock-strings `DC_STR_SETUP_CONTACT_QR_DESC` and `DC_STR_SECURE_JOIN_GROUP_QR_DESC`
|
||||
|
||||
|
||||
## 1.66.0
|
||||
|
||||
### API changes
|
||||
- `dc_contact_get_last_seen()` added #2823
|
||||
- python: `Contact.last_seen` added #2823
|
||||
- removed `DC_STR_NEWGROUPDRAFT`, we don't set draft after creating group anymore #2805
|
||||
|
||||
### Changes
|
||||
- python: add cutil.from_optional_dc_charpointer() #2824
|
||||
- refactorings #2807 #2822 #2825
|
||||
|
||||
|
||||
## 1.65.0
|
||||
|
||||
### Changes
|
||||
- python: add mypy support and some type hints #2809
|
||||
|
||||
### Fixes
|
||||
- do not disable ephemeral timer when downloading a message partially #2811
|
||||
- apply existing ephemeral timer also to partially downloaded messages;
|
||||
after full download, the ephemeral timer starts over #2811
|
||||
- replace user-visible error on verification failure with warning;
|
||||
the error is logged to the corresponding chat anyway #2808
|
||||
|
||||
|
||||
## 1.64.0
|
||||
|
||||
### Fixes
|
||||
- add 'waiting for being added to the group' only for group-joins,
|
||||
not for setup-contact #2797
|
||||
- prioritize In-Reply-To: and References: headers over group IDs when assigning
|
||||
messages to chats to fix incorrect assignment of Delta Chat replies to
|
||||
classic email threads #2795
|
||||
|
||||
|
||||
## 1.63.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_last_error()` added #2788
|
||||
|
||||
### Changes
|
||||
- Optimize Autocrypt gossip #2743
|
||||
|
||||
### Fixes
|
||||
- fix permanently hiding of one-to-one chats after secure-join #2791
|
||||
|
||||
|
||||
## 1.62.0
|
||||
|
||||
### API Changes
|
||||
- `dc_join_securejoin()` now always returns immediately;
|
||||
the returned chat may not allow sending (`dc_chat_can_send()` returns false)
|
||||
which may change as usual on `DC_EVENT_CHAT_MODIFIED` #2508 #2767
|
||||
- introduce multi-device-sync-messages;
|
||||
as older cores display them as files in self-chat,
|
||||
they are currently only sent if config option `send_sync_msgs` is set #2669
|
||||
- add `DC_EVENT_SELFAVATAR_CHANGED` #2742
|
||||
|
||||
### Changes
|
||||
- use system DNS instead of google for MX queries #2780
|
||||
- improve error logging #2758
|
||||
- improve tests #2764 #2781
|
||||
- improve ci #2770
|
||||
- refactorings #2677 #2728 #2740 #2729 #2766 #2778
|
||||
|
||||
### Fixes
|
||||
- add Let's Encrypt certificate to core as it may be missing older devices #2752
|
||||
- prioritize certificate setting from user over the one from provider-db #2749
|
||||
- fix "QR process failed" error #2725
|
||||
- do not update quota in endless loop #2726
|
||||
|
||||
|
||||
## 1.61.0
|
||||
|
||||
### API Changes
|
||||
- download-on-demand added: `dc_msg_get_download_state()`, `dc_download_full_msg()`
|
||||
and `download_limit` config option #2631 #2696
|
||||
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
|
||||
- allow ui-specific configs using `ui.`-prefix in key (`dc_set_config(context, "ui.*", value)`) #2672
|
||||
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
|
||||
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
|
||||
- emit warnings and errors from account manager with account-id 0 #2712
|
||||
|
||||
### Changes
|
||||
- notify about incoming contact requests #2690
|
||||
- messages are marked as read on first read receipt #2699
|
||||
- quota warning reappears after import, rewarning at 95% #2702
|
||||
- lock strict TLS if certificate checks are automatic #2711
|
||||
- always check certificates strictly when connecting over SOCKS5 in Automatic mode #2657
|
||||
- `Accounts` is not cloneable anymore #2654 #2658
|
||||
- update chat/contact data only when there was no newer update #2642
|
||||
- better detection of mailing list names #2665 #2685
|
||||
- log all decisions when applying ephemeral timer to chats #2679
|
||||
- connectivity view now translatable #2694 #2723
|
||||
- improve Doxygen documentation #2647 #2668 #2684 #2688 #2705
|
||||
- refactorings #2656 #2659 #2677 #2673 #2678 #2675 #2663 #2692 #2706
|
||||
- update provider database #2618
|
||||
|
||||
### Fixes
|
||||
- ephemeral timer rollback protection #2693 #2709
|
||||
- recreate configured folders if they are deleted #2691
|
||||
- ignore MDNs sent to self #2674
|
||||
- recognize NDNs that put headers into "message/global-headers" part #2598
|
||||
- avoid `dc_get_contacts()` returning duplicate contact ids #2591
|
||||
- do not leak group names on forwarding messages #2719
|
||||
- in case of smtp-errors, iterate over all addresses to fix ipv6/v4 problems #2720
|
||||
- fix pkg-config file #2660
|
||||
- fix "QR process failed" error #2725
|
||||
|
||||
|
||||
## 1.60.0
|
||||
|
||||
### Added
|
||||
- add device message to warn about QUOTA #2621
|
||||
- add SOCKS5 support #2474 #2620
|
||||
|
||||
### Changes
|
||||
- don't emit multiple events with the same import/export progress number #2639
|
||||
- reduce message length limit to 5000 chars #2615
|
||||
|
||||
### Fixes
|
||||
- keep event emitter from closing when there are no accounts #2636
|
||||
|
||||
|
||||
## 1.59.0
|
||||
|
||||
### Added
|
||||
- add quota information to `dc_get_connectivity_html()`
|
||||
|
||||
### Changes
|
||||
- refactorings #2592 #2570 #2581
|
||||
- add 'device chat about' to now existing status #2613
|
||||
- update provider database #2608
|
||||
|
||||
### Fixes
|
||||
- provider database supports socket=PLAIN and dotless domains now #2604 #2608
|
||||
- add migrated accounts to events emitter #2607
|
||||
- fix forwarding quote-only mails #2600
|
||||
- do not set WantsMdn param for outgoing messages #2603
|
||||
- set timestamps for system messages #2593
|
||||
- do not treat gmail labels as folders #2587
|
||||
- avoid timing problems in `dc_maybe_network_lost()` #2551
|
||||
- only set smtp to "connected" if the last message was actually sent #2541
|
||||
|
||||
|
||||
## 1.58.0
|
||||
|
||||
### Fixes
|
||||
- move WAL file together with database
|
||||
and avoid using data if the database was not closed correctly before #2583
|
||||
|
||||
|
||||
## 1.57.0
|
||||
|
||||
### API Changes
|
||||
|
||||
- breaking change: removed deaddrop chat #2514 #2563
|
||||
|
||||
Contact request chats are not merged into a single virtual
|
||||
"deaddrop" chat anymore. Instead, they are shown in the chatlist the
|
||||
same way as other chats, but sending of messages to them is not
|
||||
allowed and MDNs are not sent automatically until the chat is
|
||||
"accepted" by the user.
|
||||
|
||||
New API:
|
||||
- `dc_chat_is_contact_request()`: returns true if chat is a contact
|
||||
request. In this case an option to accept the chat via
|
||||
`dc_accept_chat()` should be shown in the UI.
|
||||
- `dc_accept_chat()`: unblock the chat or accept contact request
|
||||
- `dc_block_chat()`: block the chat, currently works only for mailing
|
||||
lists.
|
||||
|
||||
Removed API:
|
||||
- `dc_create_chat_by_msg_id()`: deprecated 2021-02-07 in favor of
|
||||
`dc_decide_on_contact_request()`
|
||||
- `dc_marknoticed_contact()`: deprecated 2021-02-07 in favor of
|
||||
`dc_decide_on_contact_request()`
|
||||
- `dc_decide_on_contact_request()`: this call requires a message ID
|
||||
from deaddrop chat as input. As deaddrop chat is removed, this
|
||||
call can't be used anymore.
|
||||
- `dc_msg_get_real_chat_id()`: use `dc_msg_get_chat_id()` instead, the
|
||||
only difference between these calls was in handling of deaddrop
|
||||
chat
|
||||
- removed `DC_CHAT_ID_DEADDROP` and `DC_STR_DEADDROP` constants
|
||||
|
||||
- breaking change: removed `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE`
|
||||
Instead, there is a new api `dc_get_connectivity()`
|
||||
and `dc_get_connectivity_html()`;
|
||||
`DC_EVENT_CONNECTIVITY_CHANGED` is emitted on changes
|
||||
|
||||
- breaking change: removed `dc_accounts_import_account()`
|
||||
Instead you need to add an account and call `dc_imex(DC_IMEX_IMPORT_BACKUP)`
|
||||
on its context
|
||||
|
||||
- update account api, 2 new methods:
|
||||
`int dc_all_work_done (dc_context_t* context);`
|
||||
`int dc_accounts_all_work_done (dc_accounts_t* accounts);`
|
||||
|
||||
- add api to check if a message was `Auto-Submitted`
|
||||
cffi: `int dc_msg_is_bot (const dc_msg_t* msg);`
|
||||
python: `Message.is_bot()`
|
||||
|
||||
- `dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);`
|
||||
now returns `NULL` if there is no selected account
|
||||
|
||||
- added `dc_accounts_maybe_network_lost()` for systems core cannot find out
|
||||
connectivity loss on its own (eg. iOS) #2550
|
||||
|
||||
### Added
|
||||
- use Auto-Submitted: auto-generated header to identify bots #2502
|
||||
- allow sending stickers via repl tool
|
||||
- chat: make `get_msg_cnt()` and `get_fresh_msg_cnt()` work for deaddrop chat #2493
|
||||
- withdraw/revive own qr-codes #2512
|
||||
- add Connectivity view (a better api for getting the connection status) #2319 #2549 #2542
|
||||
|
||||
### Changes
|
||||
- updated spec: new `Chat-User-Avatar` usage, `Chat-Content: sticker`, structure, copyright year #2480
|
||||
- update documentation #2548 #2561 #2569
|
||||
- breaking: `Accounts::create` does not also create an default account anymore #2500
|
||||
- remove "forwarded" from stickers, as the primary way of getting stickers
|
||||
is by asking a bot and then forwarding them currently #2526
|
||||
- mimeparser: use mailparse to parse RFC 2231 filenames #2543
|
||||
- allow email addresses without dot in the domain part #2112
|
||||
- allow installing lib and include under different prefixes #2558
|
||||
- remove counter from name provided by `DC_CHAT_ID_ARCHIVED_LINK` #2566
|
||||
- improve tests #2487 #2491 #2497
|
||||
- refactorings #2492 #2503 #2504 #2506 #2515 #2520 #2567 #2575 #2577 #2579
|
||||
- improve ci #2494
|
||||
- update provider-database #2565
|
||||
|
||||
### Removed
|
||||
- remove `dc_accounts_import_account()` api #2521
|
||||
- remove `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE` #2319
|
||||
|
||||
### Fixes
|
||||
- allow stickers with gif-images #2481
|
||||
- fix database migration #2486
|
||||
- do not count hidden messages in get_msg_cnt(). #2493
|
||||
- improve drafts detection #2489
|
||||
- fix panic when removing last, selected account from account manager #2500
|
||||
- set_draft's message-changed-event returns now draft's msg id instead of 0 #2304
|
||||
- avoid hiding outgoing classic emails #2505
|
||||
- fixes for message timestamps #2517
|
||||
- do not process names, avatars, location XMLs, message signature etc.
|
||||
for duplicate messages #2513
|
||||
- fix `can_send` for users not in group #2479
|
||||
- fix receiving events for accounts added by `dc_accounts_add_account()` #2559
|
||||
- fix which chats messages are assigned to #2465
|
||||
- fix: don't create chats when MDNs are received #2578
|
||||
|
||||
|
||||
## 1.56.0
|
||||
|
||||
- fix downscaling images #2469
|
||||
|
||||
- fix outgoing messages popping up in selfchat #2456
|
||||
|
||||
- securejoin: display error reason if there is any #2470
|
||||
|
||||
- do not allow deleting contacts with ongoing chats #2458
|
||||
|
||||
- fix: ignore drafts folder when scanning #2454
|
||||
|
||||
- fix: scan folders also when inbox is not watched #2446
|
||||
|
||||
- more robust In-Reply-To parsing #2182
|
||||
|
||||
- update dependencies #2441 #2438 #2439 #2440 #2447 #2448 #2449 #2452 #2453 #2460 #2464 #2466
|
||||
|
||||
- update provider-database #2471
|
||||
|
||||
- refactorings #2459 #2457
|
||||
|
||||
- improve tests and ci #2445 #2450 #2451
|
||||
|
||||
|
||||
## 1.55.0
|
||||
|
||||
- fix panic when receiving some HTML messages #2434
|
||||
|
||||
- fix downloading some messages multiple times #2430
|
||||
|
||||
- fix formatting of read receipt texts #2431
|
||||
|
||||
- simplify SQL error handling #2415
|
||||
|
||||
- explicit rust API for creating chats with blocked status #2282
|
||||
|
||||
- debloat the binary by using less AsRef arguments #2425
|
||||
|
||||
|
||||
## 1.54.0
|
||||
|
||||
- switch back from `sqlx` to `rusqlite` due to performance regressions #2380 #2381 #2385 #2387
|
||||
|
||||
- global search performance improvement #2364 #2365 #2366
|
||||
|
||||
- improve SQLite performance with `PRAGMA synchronous=normal` #2382
|
||||
|
||||
- python: fix building of bindings against system-wide install of `libdeltachat` #2383 #2385
|
||||
|
||||
- python: list `requests` as a requirement #2390
|
||||
|
||||
- fix creation of many delete jobs when being offline #2372
|
||||
|
||||
- synchronize status between devices #2386
|
||||
|
||||
- deaddrop (contact requests) chat improvements #2373
|
||||
|
||||
- add "Forwarded:" to notification and chatlist summaries #2310
|
||||
|
||||
- place user avatar directly into `Chat-User-Avatar` header #2232 #2384
|
||||
|
||||
- improve tests #2360 #2362 #2370 #2377 #2387
|
||||
|
||||
- cleanup #2359 #2361 #2374 #2376 #2379 #2388
|
||||
|
||||
|
||||
## 1.53.0
|
||||
|
||||
- fix sqlx performance regression #2355 2356
|
||||
|
||||
- add a `ci_scripts/coverage.sh` #2333 #2334
|
||||
|
||||
- refactorings and tests #2348 #2349 #2350
|
||||
|
||||
- improve python bindings #2332 #2326
|
||||
|
||||
|
||||
## 1.52.0
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat)
|
||||
project(deltachat LANGUAGES C)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
@@ -8,8 +9,22 @@ add_custom_command(
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.so"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --package deltachat_ffi --release
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --release --no-default-features
|
||||
|
||||
# Build in `deltachat-ffi` directory instead of using
|
||||
# `--package deltachat_ffi` to avoid feature resolver version
|
||||
# "1" bug which makes `--no-default-features` affect only
|
||||
# `deltachat`, but not `deltachat-ffi` package.
|
||||
#
|
||||
# We can't enable version "2" resolver [1] because it is not
|
||||
# stable yet on rust 1.50.0.
|
||||
#
|
||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
@@ -21,7 +36,6 @@ add_custom_target(
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
1992
Cargo.lock
generated
1992
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
129
Cargo.toml
129
Cargo.toml
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.52.0"
|
||||
version = "1.72.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -11,81 +12,89 @@ debug = 0
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[patch.crates-io]
|
||||
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" }
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1.0.28"
|
||||
async-imap = "0.4.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
async-std-resolver = "0.19.5"
|
||||
async-std = { version = "~1.8.0", features = ["unstable"] }
|
||||
async-tar = "0.3.0"
|
||||
async-trait = "0.1.31"
|
||||
backtrace = "0.3.33"
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
||||
async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.1.0"
|
||||
byteorder = "1.3.1"
|
||||
charset = "0.1"
|
||||
chrono = "0.4.6"
|
||||
dirs = { version = "3.0.1", optional=true }
|
||||
bitflags = "1.3"
|
||||
chrono = "0.4"
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
escaper = "0.1.0"
|
||||
futures = "0.3.4"
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.3.0"
|
||||
itertools = "0.10.0"
|
||||
imap-proto = "0.14.3"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2.51"
|
||||
libc = "0.2"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
mailparse = "0.13.0"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.13.0"
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
once_cell = "1.4.1"
|
||||
mailparse = "0.13"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.9.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
quick-xml = "0.18.1"
|
||||
rand = "0.7.0"
|
||||
regex = "1.1.6"
|
||||
rust-hsluv = "0.1.4"
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
sanitize-filename = "0.3.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.19"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.26", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9.3"
|
||||
sha2 = "0.9.0"
|
||||
smallvec = "1.0.0"
|
||||
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std-native-tls", "sqlite"] }
|
||||
# keep in sync with sqlx
|
||||
libsqlite3-sys = { version = "0.22.0", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] }
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
strum = "0.20.0"
|
||||
strum_macros = "0.20.1"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1.0.14"
|
||||
toml = "0.5.6"
|
||||
url = "2.1.1"
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
stop-token = "0.7"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
url = "2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.2.1"
|
||||
textwrap = "0.14.2"
|
||||
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
|
||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.7.0"
|
||||
log = "0.4.11"
|
||||
pretty_assertions = "0.6.1"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "0.10"
|
||||
tempfile = "3.0"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
]
|
||||
|
||||
[[example]]
|
||||
@@ -103,9 +112,17 @@ required-features = ["repl"]
|
||||
name = "create_account"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "contacts"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "search_msgs"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
27
README.md
27
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
> Deltachat-core written in Rust
|
||||
|
||||
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
|
||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
@@ -79,6 +79,16 @@ For more commands type:
|
||||
> help
|
||||
```
|
||||
|
||||
## Installing libdeltachat system wide
|
||||
|
||||
```
|
||||
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
$ cd deltachat-core-rust
|
||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
||||
$ cmake --build build
|
||||
$ sudo cmake --install build
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
@@ -111,20 +121,15 @@ $ cargo test -- --ignored
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
- `nightly`: Enable nightly only performance and security related features.
|
||||
|
||||
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
|
||||
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
|
||||
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/lqpegel3ld4ipxj8/branch/master?style=flat-square
|
||||
[appveyor]: https://ci.appveyor.com/project/dignifiedquire/deltachat-core-rust/branch/master
|
||||
|
||||
## Language bindings and frontend projects
|
||||
|
||||
Language bindings are available for:
|
||||
|
||||
- [C](https://c.delta.chat)
|
||||
- [Node.js](https://www.npmjs.com/package/deltachat-node)
|
||||
- [Python](https://py.delta.chat)
|
||||
- [Go](https://github.com/deltachat/go-deltachat/)
|
||||
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js** \[[📂 source](https://github.com/deltachat/deltachat-node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
|
||||
BIN
assets/icon-broadcast.png
Normal file
BIN
assets/icon-broadcast.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
149
assets/icon-broadcast.svg
Normal file
149
assets/icon-broadcast.svg
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new 0 0 128 128"
|
||||
viewBox="0 0 60 60"
|
||||
version="1.1"
|
||||
id="svg878"
|
||||
sodipodi:docname="icon-broadcast.svg"
|
||||
width="60"
|
||||
height="60"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-broadcast.png"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001">
|
||||
<metadata
|
||||
id="metadata884">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs882" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1329"
|
||||
inkscape:window-height="847"
|
||||
id="namedview880"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.21875"
|
||||
inkscape:cx="36.598802"
|
||||
inkscape:cy="32.191617"
|
||||
inkscape:window-x="111"
|
||||
inkscape:window-y="205"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg878"
|
||||
inkscape:document-rotation="0" />
|
||||
<radialGradient
|
||||
id="c"
|
||||
cx="65.25"
|
||||
cy="89"
|
||||
r="26.440001"
|
||||
gradientTransform="matrix(0.77611266,0.11996647,-0.18999676,1.2286617,-11.305867,-60.065999)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#FFC107"
|
||||
offset="0"
|
||||
id="stop833" />
|
||||
<stop
|
||||
stop-color="#FFBD06"
|
||||
offset=".3502"
|
||||
id="stop835" />
|
||||
<stop
|
||||
stop-color="#FFB104"
|
||||
offset=".6938"
|
||||
id="stop837" />
|
||||
<stop
|
||||
stop-color="#FFA000"
|
||||
offset="1"
|
||||
id="stop839" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="b"
|
||||
cx="52.5"
|
||||
cy="19.75"
|
||||
r="92.975998"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45.323856,68.997115,75.979538)">
|
||||
<stop
|
||||
stop-color="#EF5350"
|
||||
offset="0"
|
||||
id="stop848" />
|
||||
<stop
|
||||
stop-color="#EB4F4C"
|
||||
offset=".246"
|
||||
id="stop850" />
|
||||
<stop
|
||||
stop-color="#E04341"
|
||||
offset=".4878"
|
||||
id="stop852" />
|
||||
<stop
|
||||
stop-color="#CD302F"
|
||||
offset=".7272"
|
||||
id="stop854" />
|
||||
<stop
|
||||
stop-color="#C62828"
|
||||
offset=".8004"
|
||||
id="stop856" />
|
||||
<stop
|
||||
stop-color="#C62828"
|
||||
offset="1"
|
||||
id="stop858" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="a"
|
||||
cx="16.979"
|
||||
cy="92"
|
||||
r="24.165001"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45.323856,68.997115,75.979538)"
|
||||
xlink:href="#b">
|
||||
<stop
|
||||
stop-color="#E0E0E0"
|
||||
offset="0"
|
||||
id="stop863" />
|
||||
<stop
|
||||
stop-color="#CFCFCF"
|
||||
offset=".3112"
|
||||
id="stop865" />
|
||||
<stop
|
||||
stop-color="#A4A4A4"
|
||||
offset=".9228"
|
||||
id="stop867" />
|
||||
<stop
|
||||
stop-color="#9E9E9E"
|
||||
offset="1"
|
||||
id="stop869" />
|
||||
</radialGradient>
|
||||
<rect
|
||||
y="0"
|
||||
x="0"
|
||||
height="60"
|
||||
width="60"
|
||||
id="rect1420"
|
||||
style="fill:#7cc0bc;fill-opacity:1;stroke:none;stroke-width:1.29077" />
|
||||
<path
|
||||
id="path872"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.336872;stroke-opacity:1"
|
||||
d="m 8.6780027,35.573064 0.032831,-11.910176 c 0.00138,-0.476406 0.4881282,-0.794259 0.9235226,-0.604877 l 4.1144877,2.345752 -0.02386,8.656315 -4.1268029,2.122946 C 9.1617452,36.370003 8.6766889,36.049472 8.6780027,35.573064 Z m 5.0469633,-1.508222 0.02386,-8.656314 31.145424,-9.537653 c 0.841472,-0.219211 1.65915,0.41667 1.656755,1.283728 l -0.06929,25.139995 c -0.0024,0.867062 -0.825942,1.500799 -1.663803,1.274581 z m 3.8042,6.892234 C 16.681121,40.104348 16.315444,38.819414 16.69043,37.591308 l 2.252234,-7.347193 c 0.2644,-0.861571 0.845185,-1.567441 1.641953,-1.989251 0.796769,-0.421808 1.706956,-0.509819 2.568531,-0.245419 l 7.263888,2.225804 c 1.775518,0.543235 2.780299,2.432591 2.232297,4.208094 L 30.3971,41.790532 c -0.545627,1.777887 -2.432591,2.780297 -4.208095,2.232298 l -7.263891,-2.225804 c -0.545033,-0.165864 -1.01825,-0.460162 -1.395948,-0.83995 z m 12.377693,-7.976728 c -0.07601,-0.07642 -0.17114,-0.133864 -0.280621,-0.167516 l -7.263891,-2.225803 c -0.233244,-0.07209 -0.421626,0.0013 -0.512275,0.04861 -0.09064,0.0474 -0.25772,0.166033 -0.327435,0.396899 l -2.252234,7.347191 c -0.108166,0.354628 0.09088,0.731541 0.447888,0.842099 l 7.263891,2.225802 c 0.354626,0.108174 0.731539,-0.09088 0.842099,-0.447888 l 2.249845,-7.344814 c 0.07453,-0.245145 0.0014,-0.504991 -0.167267,-0.67458 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/icon-webxdc.png
Normal file
BIN
assets/icon-webxdc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
181
assets/icon-webxdc.svg
Normal file
181
assets/icon-webxdc.svg
Normal file
@@ -0,0 +1,181 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="80mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 80 297"
|
||||
version="1.1"
|
||||
id="svg71"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
sodipodi:docname="icon-webxdc.svg"
|
||||
inkscape:export-filename="C:\Users\user\OneDrive - BFW-Leipzig\Documents\LogoDC\finalohnerand.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<metadata
|
||||
id="metadata856">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
id="namedview73"
|
||||
pagecolor="#767676"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
showborder="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="-90.271136"
|
||||
inkscape:cy="-1233.1209"
|
||||
inkscape:window-width="1864"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="56"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:snap-global="false"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:document-rotation="0"
|
||||
units="px">
|
||||
<sodipodi:guide
|
||||
position="-154.76097,641.11689"
|
||||
orientation="0,-1"
|
||||
id="guide21118" />
|
||||
<sodipodi:guide
|
||||
position="-60.286487,633.36619"
|
||||
orientation="0,-1"
|
||||
id="guide21120" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs68">
|
||||
<linearGradient
|
||||
id="linearGradient4375">
|
||||
<stop
|
||||
style="stop-color:#364e59;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4377" />
|
||||
<stop
|
||||
style="stop-color:#364e59;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop4379" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:0.167903"
|
||||
id="rect880"
|
||||
width="79.8321"
|
||||
height="79.8321"
|
||||
x="-64.03286"
|
||||
y="-375.9097"
|
||||
ry="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3799-2"
|
||||
d="m -24.089585,-372.59579 c -19.986026,0.24336 -36.196903,16.666 -36.196903,36.67011 0,20.00409 16.210877,36.03233 36.196903,35.78912 19.0024236,-0.076 14.5340713,-10.6146 35.538854,-0.85693 -11.50627538,-17.97454 0.390097,-20.36737 0.658079,-35.81316 0,-20.00411 -16.2108788,-36.03235 -36.196911,-35.78914 z"
|
||||
style="fill:#364e59;fill-opacity:1;stroke:none;stroke-width:1.93355"
|
||||
sodipodi:nodetypes="sscccs" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -54.193871,-325.26419 Z"
|
||||
id="path3846" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -49.397951,-326.67773 Z"
|
||||
id="path3848" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m -49.397951,-326.67773 v 0 0"
|
||||
id="path3850" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.01;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m -51.35133,-325.0334 -7.964067,5.98895 z"
|
||||
id="path3965" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path11037"
|
||||
d="m -24.089585,-372.19891 c -19.986026,0.24156 -36.196903,16.54352 -36.196903,36.40062 0,7.86524 2.543315,15.1113 6.857155,20.97971 6.577146,8.94734 11.123515,9.77363 11.123515,9.77363 1.343237,1.78324 10.270932,4.3223 10.270932,4.3223 l 16.791727,-70.86654 -0.468369,-0.33457 c 0.458597,0.26445 0.428277,-0.27515 -8.378035,-0.27515 z"
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke:none;stroke-width:1.92643"
|
||||
sodipodi:nodetypes="sssccccss" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -49.944239,-310.69957 Z"
|
||||
id="path13674" />
|
||||
<g
|
||||
id="g15178"
|
||||
transform="matrix(0.79975737,0,0,0.79975737,53.088959,-63.716396)">
|
||||
<rect
|
||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="-334.4964"
|
||||
y="-154.51025"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-5"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="147.63107"
|
||||
y="-334.4964"
|
||||
transform="rotate(-45)"
|
||||
inkscape:transform-center-x="-0.74835017"
|
||||
inkscape:transform-center-y="0.37417525" />
|
||||
</g>
|
||||
<g
|
||||
id="g22468"
|
||||
transform="translate(3.3033974)">
|
||||
<g
|
||||
id="g15178-0"
|
||||
transform="matrix(-0.79975737,0,0,0.79975737,-103.11028,-63.716404)"
|
||||
style="fill:#7cc5cc;fill-opacity:1">
|
||||
<rect
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-2"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="-334.4964"
|
||||
y="-154.51025"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-5-5"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="147.63107"
|
||||
y="-334.4964"
|
||||
transform="rotate(-45)"
|
||||
inkscape:transform-center-x="-0.74835017"
|
||||
inkscape:transform-center-y="0.37417525" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
10
assets/qrcode_logo_footer.svg
Normal file
10
assets/qrcode_logo_footer.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:bold;font-size:24.4118px;line-height:1.25;font-family:sans-serif;fill:#aaaaaa;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
x="42.325161"
|
||||
y="23.32255"
|
||||
id="text72398">get.delta.chat</text>
|
||||
<path
|
||||
id="path84310"
|
||||
style="opacity:0.25;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
d="M 17.13769,0.00129321 C 7.6753075,0.11650893 0,7.8915283 0,17.362467 c 0,9.47094 7.6753075,17.059745 17.13769,16.944599 8.99669,-0.03598 6.880074,-5.025654 16.824785,-0.405885 -5.447648,-8.510047 0.184241,-9.642482 0.311117,-16.955289 0,-9.4709395 -7.673512,-17.0597453 -17.135895,-16.94459879 z M 17.0769,4.9986797 c 1.84214,0 3.447355,0.253959 4.815003,0.7616693 1.381603,0.5076411 2.072253,1.207862 2.072253,2.0990711 0,0.4286855 -0.167495,0.7836052 -0.50242,1.0656242 -0.334921,0.2819844 -0.724544,0.4237724 -1.171121,0.4237724 -0.641952,0 -1.396532,-0.3909376 -2.261778,-1.169353 C 19.14963,7.3898036 18.402555,6.83791 17.788507,6.5220182 17.188416,6.1950547 16.484552,6.0321266 15.675129,6.0321266 c -1.032717,0 -1.883352,0.1854523 -2.553215,0.5578447 -0.655913,0.372254 -0.98517,0.8460916 -0.98517,1.4214436 0,0.5414792 0.272815,1.0495355 0.817093,1.5233385 0.544275,0.4738026 1.946291,1.3367446 4.207097,2.5889976 2.414319,1.342419 4.117377,2.390985 5.108232,3.146807 1.004795,0.755857 1.821505,1.675853 2.449514,2.758846 0.628002,1.082993 0.942253,2.227607 0.942253,3.434674 0,2.120834 -0.929555,3.993314 -2.785656,5.617786 -1.84214,1.613228 -3.99694,2.41915 -6.467082,2.41915 -2.246845,0 -4.145607,-0.647976 -5.694677,-1.945312 -1.5490699,-1.297336 -2.3225722,-3.028063 -2.3225722,-5.194049 0,-2.087031 0.8506345,-3.83094 2.5532182,-5.229825 1.716541,-1.398884 3.824203,-2.245599 6.322256,-2.538897 -0.697774,-0.631749 -1.668763,-1.387225 -2.910816,-2.267155 -1.367648,-0.970199 -2.287914,-1.73045 -2.762402,-2.283243 -0.474491,-0.5640381 -0.711618,-1.1795944 -0.711618,-1.8451814 0,-0.9927581 0.572093,-1.7710351 1.716451,-2.3351077 1.144362,-0.5753173 2.636724,-0.8635642 4.478865,-0.8635642 z m 1.110327,10.3738083 c -4.005262,0.5302 -6.007576,2.75279 -6.007576,6.667322 0,2.01932 0.49495,3.587291 1.485805,4.704157 1.004806,1.116832 2.169696,1.675299 3.495479,1.675299 1.381602,0 2.520072,-0.535632 3.413229,-1.60738 0.893168,-1.082959 1.339187,-2.545264 1.339187,-4.384079 0,-2.662348 -1.242022,-5.013441 -3.726124,-7.055319 z" />
|
||||
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
Binary file not shown.
37
benches/contacts.rs
Normal file
37
benches/contacts.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use async_std::task::block_on;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
Contact::add_address_book(&context, &book).await.unwrap();
|
||||
|
||||
let query: Option<&str> = None;
|
||||
for _ in 0..read_count {
|
||||
Contact::get_all(&context, 0, query).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("create 500 contacts", |b| {
|
||||
b.iter(|| block_on(async { address_book_benchmark(black_box(500), black_box(0)).await }))
|
||||
});
|
||||
|
||||
c.bench_function("create 100 contacts and read it 1000 times", |b| {
|
||||
b.iter(|| block_on(async { address_book_benchmark(black_box(100), black_box(1000)).await }))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -8,7 +8,7 @@ async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
|
||||
27
benches/search_msgs.rs
Normal file
27
benches/search_msgs.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use async_std::task::block_on;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use std::path::Path;
|
||||
|
||||
async fn search_benchmark(path: impl AsRef<Path>) {
|
||||
let dbfile = path.as_ref();
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
for _ in 0..10u32 {
|
||||
context.search_msgs(None, "hello").await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
c.bench_function("search hello", |b| {
|
||||
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$DEVPI_LOGIN" ] ; then
|
||||
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -xe
|
||||
|
||||
PYDOCDIR=${1:?directory with python docs}
|
||||
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
||||
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
|
||||
SSHTARGET=ci@b1.delta.chat
|
||||
|
||||
|
||||
# if CIRCLE_BRANCH is not set we are called for a tag with empty CIRCLE_BRANCH variable.
|
||||
export BRANCH=${CIRCLE_BRANCH:master}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
|
||||
|
||||
|
||||
# python docs to py.delta.chat
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
|
||||
rsync -avz \
|
||||
--delete \
|
||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||
"$PYDOCDIR/html/" \
|
||||
delta@py.delta.chat:build/${BRANCH}
|
||||
|
||||
# C docs to c.delta.chat
|
||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
|
||||
rsync -avz \
|
||||
--delete \
|
||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||
"$DOXYDOCDIR/html/" \
|
||||
delta@c.delta.chat:build-c/${BRANCH}
|
||||
|
||||
echo -----------------------
|
||||
echo upload wheels
|
||||
echo -----------------------
|
||||
|
||||
# Bundle external shared libraries into the wheels
|
||||
|
||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSHTARGET mkdir -p $BUILDDIR
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ci_scripts/cleanup_devpi_indices.py $SSHTARGET:$BUILDDIR
|
||||
rsync -avz \
|
||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||
$WHEELHOUSEDIR \
|
||||
$SSHTARGET:$BUILDDIR
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
|
||||
# we rely on the "venv" virtualenv on the remote account to exist
|
||||
source venv/bin/activate
|
||||
cd $BUILDDIR
|
||||
|
||||
devpi use https://m.devpi.net
|
||||
devpi login dc --password $DEVPI_LOGIN
|
||||
|
||||
N_BRANCH=${BRANCH//[\/]}
|
||||
|
||||
devpi use dc/\$N_BRANCH || {
|
||||
devpi index -c \$N_BRANCH
|
||||
devpi use dc/\$N_BRANCH
|
||||
}
|
||||
devpi index \$N_BRANCH bases=/root/pypi
|
||||
devpi upload wheelhouse/deltachat*
|
||||
|
||||
# remove devpi non-master dc indices if thy are too old
|
||||
# this script was copied above
|
||||
python cleanup_devpi_indices.py
|
||||
_HERE
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.5 environment as the base environment
|
||||
/opt/python/cp35-cp35m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.5.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.5.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.5.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.50.0-x86_64-unknown-linux-gnu -y
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
rustc --version
|
||||
|
||||
# remove some 300-400 MB that we don't need for automated builds
|
||||
rm -rf /root/.rustup/toolchains/1.50.0-x86_64-unknown-linux-gnu/share
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
|
||||
export CIRCLE_BUILD_NUM=$USER
|
||||
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
|
||||
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
|
||||
|
||||
time bash ci_scripts/$CIRCLE_JOB.sh
|
||||
@@ -1,77 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [nightly]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Install ${{ matrix.rust }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
if: matrix.rust == 'nightly'
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
- name: tests ignored
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all --release -- --ignored
|
||||
|
||||
check_fmt:
|
||||
name: Checking fmt and docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
# clippy_check:
|
||||
# name: Clippy check
|
||||
# runs-on: ubuntu-latest
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# profile: minimal
|
||||
# toolchain: nightly
|
||||
# override: true
|
||||
# components: clippy
|
||||
#
|
||||
# - name: clippy
|
||||
# run: cargo clippy --all
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build the Delta Chat C/Rust library typically run in a docker
|
||||
# container that contains all library deps but should also work
|
||||
# outside if you have the dependencies installed on your system.
|
||||
|
||||
set -e -x
|
||||
|
||||
# Perform clean build of core and install.
|
||||
export TOXWORKDIR=.docker-tox
|
||||
|
||||
# install core lib
|
||||
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
cargo build --release -p deltachat_ffi
|
||||
# cargo test --all --all-features
|
||||
|
||||
# Statically link against libdeltachat.a.
|
||||
export DCC_RS_DEV=$(pwd)
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
ln -s /opt/python/cp27-cp27m/bin/python2.7
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
popd
|
||||
|
||||
if [ -n "$TESTS" ]; then
|
||||
|
||||
pushd python
|
||||
# prepare a clean tox run
|
||||
rm -rf tests/__pycache__
|
||||
rm -rf src/deltachat/__pycache__
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
|
||||
# allows running of "liveconfig" tests but for speed reasons
|
||||
# we run them only for the highest python version we support
|
||||
|
||||
# we split out qr-tests run to minimize likelyness of flaky tests
|
||||
# (some qr tests are pretty heavy in terms of send/received
|
||||
# messages and rust's imap code likely has concurrency problems)
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||
popd
|
||||
fi
|
||||
|
||||
|
||||
# if [ -n "$DOCS" ]; then
|
||||
# echo -----------------------
|
||||
# echo generating python docs
|
||||
# echo -----------------------
|
||||
# (cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||
# fi
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
set -xe
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
# we seem to need .git for setuptools_scm versioning
|
||||
find .git >>.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
set +x
|
||||
|
||||
# we have to create a remote file for the remote-docker run
|
||||
# so we can do a simple ssh command with a TTY
|
||||
# so that when our job dies, all container-runs are aborted.
|
||||
# sidenote: the circle-ci machinery will kill ongoing jobs
|
||||
# if there are new commits and we want to ensure that
|
||||
# everything is terminated/cleaned up and we have no orphaned
|
||||
# useless still-running docker-containers consuming resources.
|
||||
|
||||
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
shopt -s huponexit
|
||||
cd $BUILDDIR
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
|
||||
_HERE
|
||||
|
||||
echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||
mkdir -p workspace
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||
|
||||
set -e
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=x86_64-unknown-linux-gnu
|
||||
export RUSTC_WRAPPER=sccache
|
||||
|
||||
bash ci_scripts/run-rust-test.sh
|
||||
_HERE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.52.0"
|
||||
version = "1.72.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -17,13 +17,13 @@ crate-type = ["cdylib", "staticlib"]
|
||||
[dependencies]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
human-panic = "1"
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
async-std = "1.6.0"
|
||||
anyhow = "1.0.28"
|
||||
thiserror = "1.0.14"
|
||||
rand = "0.7.3"
|
||||
async-std = "1"
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -583,7 +583,7 @@ SORT_MEMBERS_CTORS_1ST = NO
|
||||
# appear in their defined order.
|
||||
# The default value is: NO.
|
||||
|
||||
SORT_GROUP_NAMES = NO
|
||||
SORT_GROUP_NAMES = YES
|
||||
|
||||
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
|
||||
# fully-qualified names, including namespaces. If set to NO, the class list will
|
||||
|
||||
@@ -4,4 +4,16 @@ div.fragment {
|
||||
background-color: #e0e0e0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #e0e0e0;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
# Delta Chat C Interface
|
||||
|
||||
## Installation
|
||||
|
||||
see `Installing libdeltachat system wide` in [../README.md](../README.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
To generate the C Interface documentation,
|
||||
|
||||
@@ -23,11 +23,13 @@ fn main() {
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
|
||||
.unwrap()
|
||||
.write_all(&pkg_config.as_bytes())
|
||||
.write_all(pkg_config.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
prefix={prefix}
|
||||
libdir=${{prefix}}/lib
|
||||
includedir=${{prefix}}/include
|
||||
libdir={libdir}
|
||||
includedir={includedir}
|
||||
|
||||
Name: {name}
|
||||
Description: {description}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
245
deltachat-ffi/src/lot.rs
Normal file
245
deltachat-ffi/src/lot.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
//! # Legacy generic return values for C API.
|
||||
|
||||
use crate::message::MessageState;
|
||||
use crate::qr::Qr;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
use anyhow::Error;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
/// Lot objects are created
|
||||
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
|
||||
///
|
||||
/// *Lot* is used in the meaning *heap* here.
|
||||
#[derive(Debug)]
|
||||
pub enum Lot {
|
||||
Summary(Summary),
|
||||
Qr(Qr),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
Text1Username = 2,
|
||||
Text1Self = 3,
|
||||
}
|
||||
|
||||
impl Default for Meaning {
|
||||
fn default() -> Self {
|
||||
Meaning::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
Some(SummaryPrefix::Draft(text)) => Some(text),
|
||||
Some(SummaryPrefix::Username(username)) => Some(username),
|
||||
Some(SummaryPrefix::Me(text)) => Some(text),
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => None,
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { .. } => None,
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(_) => None,
|
||||
Self::Error(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text1_meaning(&self) -> Meaning {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => Meaning::None,
|
||||
Some(SummaryPrefix::Draft(_text)) => Meaning::Text1Draft,
|
||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
||||
},
|
||||
Self::Qr(_qr) => Meaning::None,
|
||||
Self::Error(_err) => Meaning::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> LotState {
|
||||
match self {
|
||||
Self::Summary(summary) => summary.state.into(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> u32 {
|
||||
match self {
|
||||
Self::Summary(_) => Default::default(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => *contact_id,
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id } => *contact_id,
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_timestamp(&self) -> i64 {
|
||||
match self {
|
||||
Self::Summary(summary) => summary.timestamp,
|
||||
Self::Qr(_) => Default::default(),
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
|
||||
// Qr States
|
||||
/// id=contact
|
||||
QrAskVerifyContact = 200,
|
||||
|
||||
/// text1=groupname
|
||||
QrAskVerifyGroup = 202,
|
||||
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// text1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
/// text1=text
|
||||
QrText = 330,
|
||||
|
||||
/// text1=URL
|
||||
QrUrl = 332,
|
||||
|
||||
/// text1=error string
|
||||
QrError = 400,
|
||||
|
||||
QrWithdrawVerifyContact = 500,
|
||||
|
||||
/// text1=groupname
|
||||
QrWithdrawVerifyGroup = 502,
|
||||
|
||||
QrReviveVerifyContact = 510,
|
||||
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
|
||||
// Message States
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
MsgInSeen = 16,
|
||||
MsgOutPreparing = 18,
|
||||
MsgOutDraft = 19,
|
||||
MsgOutPending = 20,
|
||||
MsgOutFailed = 24,
|
||||
MsgOutDelivered = 26,
|
||||
MsgOutMdnRcvd = 28,
|
||||
}
|
||||
|
||||
impl Default for LotState {
|
||||
fn default() -> Self {
|
||||
LotState::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageState> for LotState {
|
||||
fn from(s: MessageState) -> Self {
|
||||
use MessageState::*;
|
||||
match s {
|
||||
Undefined => LotState::Undefined,
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
OutDelivered => LotState::MsgOutDelivered,
|
||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Summary> for Lot {
|
||||
fn from(summary: Summary) -> Self {
|
||||
Lot::Summary(summary)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qr> for Lot {
|
||||
fn from(qr: Qr) -> Self {
|
||||
Lot::Qr(qr)
|
||||
}
|
||||
}
|
||||
|
||||
// Make it easy to convert errors into the final `Lot`.
|
||||
impl From<Error> for Lot {
|
||||
fn from(error: Error) -> Self {
|
||||
Lot::Error(error.to_string())
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,12 @@ use std::ptr;
|
||||
/// }
|
||||
/// ```
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
let ret: *mut libc::c_char;
|
||||
if !s.is_null() {
|
||||
ret = libc::strdup(s);
|
||||
assert!(!ret.is_null());
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
ret = libc::calloc(1, 1) as *mut libc::c_char;
|
||||
assert!(!ret.is_null());
|
||||
}
|
||||
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -170,15 +167,20 @@ pub(crate) trait Strdup {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> Strdup for T {
|
||||
impl Strdup for str {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self.as_ref());
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
// We can not implement for AsRef<OsStr> because we already implement
|
||||
// AsRev<str> and this conflicts. So implement for Path directly.
|
||||
impl Strdup for String {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let s: &str = self;
|
||||
s.strdup()
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for std::path::Path {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
@@ -186,6 +188,13 @@ impl Strdup for std::path::Path {
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for [u8] {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods to turn optional strings into C strings.
|
||||
///
|
||||
/// This is the same as the [Strdup] trait but a different trait name
|
||||
|
||||
13
deltachat_derive/Cargo.toml
Normal file
13
deltachat_derive/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1"
|
||||
quote = "1"
|
||||
47
deltachat_derive/src/lib.rs
Normal file
47
deltachat_derive/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
#![recursion_limit = "128"]
|
||||
extern crate proc_macro;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
// For now, assume (not check) that these macroses are applied to enum without
|
||||
// data. If this assumption is violated, compiler error will point to
|
||||
// generated code, which is not very user-friendly.
|
||||
|
||||
#[proc_macro_derive(ToSql)]
|
||||
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::ToSql for #name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let num = *self as i64;
|
||||
let value = rusqlite::types::Value::Integer(num);
|
||||
let output = rusqlite::types::ToSqlOutput::Owned(value);
|
||||
std::result::Result::Ok(output)
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromSql)]
|
||||
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::FromSql for #name {
|
||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||
if let Some(value) = num_traits::FromPrimitive::from_i64(inner) {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
33
draft/aeap-mvp.rst
Normal file
33
draft/aeap-mvp.rst
Normal file
@@ -0,0 +1,33 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one
|
||||
|
||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
||||
|
||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
||||
|
||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
194
draft/webxdc-dev-reference.md
Normal file
194
draft/webxdc-dev-reference.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Webxdc Developer Reference
|
||||
|
||||
## Webxdc File Format
|
||||
|
||||
- a **Webxdc app** is a **ZIP-file** with the extension `.xdc`
|
||||
- the ZIP-file must use the default compression methods as of RFC 1950,
|
||||
this is "Deflate" or "Store"
|
||||
- the ZIP-file must contain at least the file `index.html`
|
||||
- if the Webxdc app is started, `index.html` is opened in a restricted webview
|
||||
that allow accessing resources only from the ZIP-file
|
||||
|
||||
|
||||
## Webxdc API
|
||||
|
||||
There are some additional APIs available once `webxdc.js` is included
|
||||
(the file will be provided by the concrete implementations,
|
||||
no need to add `webxdc.js` to your ZIP-file):
|
||||
|
||||
```html
|
||||
<script src="webxdc.js"></script>
|
||||
```
|
||||
|
||||
### sendUpdate()
|
||||
|
||||
```js
|
||||
window.webxdc.sendUpdate(update, descr);
|
||||
```
|
||||
|
||||
Webxdc apps are usually shared in a chat and run independently on each peer.
|
||||
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
|
||||
|
||||
- `update`: an object with the following fields:
|
||||
- `update.payload`: any javascript primitive, array or object.
|
||||
- `update.info`: optional, short, informational message that will be added to the chat,
|
||||
eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
usually only one line of text is shown,
|
||||
use this option sparingly to not spam the chat.
|
||||
- `update.summary`: optional, short text, shown beside app icon;
|
||||
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
|
||||
|
||||
- `descr`: short, human-readable description what this update is about.
|
||||
this is shown eg. as a fallback text in an email program.
|
||||
|
||||
All peers, including the sending one,
|
||||
will receive the update by the callback given to `setUpdateListener()`.
|
||||
|
||||
There are situations where the user cannot send messages to a chat,
|
||||
eg. contact requests or if the user has left a group.
|
||||
In these cases, you can still call `sendUpdate()`,
|
||||
however, the update won't be sent to other peers
|
||||
and you won't get the update by `setUpdateListener()` nor by `getAllUpdates()`.
|
||||
|
||||
|
||||
### setUpdateListener()
|
||||
|
||||
```js
|
||||
window.webxdc.setUpdateListener((update) => {});
|
||||
```
|
||||
|
||||
With `setUpdateListener()` you define a callback that receives the updates
|
||||
sent by `sendUpdate()`.
|
||||
|
||||
- `update`: passed to the callback on updates with the following fields:
|
||||
`update.payload`: equals the payload given to `sendUpdate()`
|
||||
|
||||
The callback is called for updates sent by you or other peers.
|
||||
|
||||
|
||||
### getAllUpdates()
|
||||
|
||||
```js
|
||||
updates = await window.webxdc.getAllUpdates();
|
||||
```
|
||||
|
||||
In case your Webxdc was just started,
|
||||
you may want to reconstruct the state from the last run -
|
||||
and also incorporate updates that may have arrived while the app was not running.
|
||||
|
||||
- `updates`: All previous updates in an array,
|
||||
eg. `[{payload: "foo"},{payload: "bar"}]`
|
||||
if `webxdc.sendUpdate({payload: "foo"}); webxdc.sendUpdate({payload: "bar"};` was called on the last run.
|
||||
|
||||
The updates are wrapped into a Promise that you can `await` for.
|
||||
If you are not in an async function and cannot use `await` therefore,
|
||||
you can get the updates with `then()`:
|
||||
|
||||
```js
|
||||
window.webxdc.getAllUpdates().then(updates => {});
|
||||
```
|
||||
|
||||
|
||||
### selfAddr
|
||||
|
||||
```js
|
||||
window.webxdc.selfAddr
|
||||
```
|
||||
|
||||
Property with the peer's own address.
|
||||
This is esp. useful if you want to differ between different peers -
|
||||
just send the address along with the payload,
|
||||
and, if needed, compare the payload addresses against selfAddr() later on.
|
||||
|
||||
|
||||
### selfName
|
||||
|
||||
```js
|
||||
window.webxdc.selfName
|
||||
```
|
||||
|
||||
Property with the peer's own name.
|
||||
This is name chosen by the user in their settings,
|
||||
if there is nothing set, that defaults to the peer's address.
|
||||
|
||||
|
||||
## manifest.toml
|
||||
|
||||
If the ZIP-file contains a `manifest.toml` in its root directory,
|
||||
some basic information are read and used from there.
|
||||
|
||||
the `manifest.toml` has the following format
|
||||
|
||||
```toml
|
||||
name = "My App Name"
|
||||
```
|
||||
|
||||
- **name** - The name of the app.
|
||||
If no name is set or if there is no manifest, the filename is used as the app name.
|
||||
|
||||
|
||||
## App Icon
|
||||
|
||||
If the ZIP-root contains an `icon.png` or `icon.jpg`,
|
||||
these files are used as the icon for the app.
|
||||
The icon should be a square at reasonable width/height;
|
||||
round corners etc. will be added by the implementations as needed.
|
||||
If no icon is set, a default icon will be used.
|
||||
|
||||
|
||||
## Webxdc Examples
|
||||
|
||||
The following example shows an input field and every input is show on all peers.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<script src="webxdc.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input id="input" type="text"/>
|
||||
<a href="" onclick="sendMsg(); return false;">Send</a>
|
||||
<p id="output"></p>
|
||||
<script>
|
||||
|
||||
function sendMsg() {
|
||||
msg = document.getElementById("input").value;
|
||||
window.webxdc.sendUpdate({payload: msg}, 'Someone typed "'+msg+'".');
|
||||
}
|
||||
|
||||
function receiveUpdate(update) {
|
||||
document.getElementById('output').innerHTML += update.payload + "<br>";
|
||||
}
|
||||
|
||||
window.webxdc.setUpdateListener(receiveUpdate);
|
||||
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
||||
You can also use that repository as a template for your own app -
|
||||
just clone and start adapting things to your need.
|
||||
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
- [2048](https://github.com/adbenitez/2048.xdc)
|
||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc)
|
||||
|
||||
|
||||
## Closing Remarks
|
||||
|
||||
- older devices might not have the newest js features in their webview,
|
||||
you may want to transpile your code down to an older js version eg. with https://babeljs.io
|
||||
- there are tons of ideas for enhancements of the API and the file format,
|
||||
eg. in the future, we will may define icon- and manifest-files,
|
||||
allow to aggregate the state or add metadata.
|
||||
@@ -2,7 +2,7 @@ extern crate dirs;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure, Error};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||
@@ -13,11 +13,11 @@ use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::lot::LotState;
|
||||
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
@@ -34,7 +34,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM jobs;"))
|
||||
.execute("DELETE FROM jobs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
@@ -42,7 +42,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM acpeerstates;"))
|
||||
.execute("DELETE FROM acpeerstates;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
@@ -50,7 +50,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM keypairs;"))
|
||||
.execute("DELETE FROM keypairs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
@@ -58,34 +58,35 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM contacts WHERE id>9;"))
|
||||
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM chats WHERE id>9;"))
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM chats_contacts;"))
|
||||
.execute("DELETE FROM chats_contacts;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM msgs WHERE id>9;"))
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query(
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
))
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(sqlx::query("DELETE FROM leftgrps;"))
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(8) Rest but server config reset.");
|
||||
@@ -97,10 +98,10 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
});
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -188,10 +189,18 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
MessageState::OutFailed => " !!",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let downloadstate = match msg.download_state() {
|
||||
DownloadState::Done => "",
|
||||
DownloadState::Available => " [⬇ Download available]",
|
||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
};
|
||||
|
||||
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
|
||||
prefix.as_ref(),
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
@@ -225,11 +234,12 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
""
|
||||
},
|
||||
statestr,
|
||||
downloadstate,
|
||||
&temp2,
|
||||
);
|
||||
}
|
||||
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
let mut lines_out = 0;
|
||||
for &msg_id in msglist {
|
||||
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||
@@ -257,59 +267,59 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
|
||||
for contact_id in contacts {
|
||||
let line;
|
||||
let mut line2 = "".to_string();
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
let verified_state = contact.is_verified(context).await;
|
||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
||||
if verified_state == VerifiedStatus::BidirectVerified {
|
||||
" √√"
|
||||
} else {
|
||||
" √"
|
||||
}
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
let verified_state = contact.is_verified(context).await?;
|
||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
||||
if verified_state == VerifiedStatus::BidirectVerified {
|
||||
" √√"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
line = format!(
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
&name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() {
|
||||
&addr
|
||||
} else {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
);
|
||||
" √"
|
||||
}
|
||||
|
||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||
} else {
|
||||
""
|
||||
};
|
||||
line = format!(
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
&name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() {
|
||||
&addr
|
||||
} else {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
);
|
||||
}
|
||||
|
||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn chat_prefix(chat: &Chat) -> &'static str {
|
||||
chat.typ.into()
|
||||
}
|
||||
|
||||
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
|
||||
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<()> {
|
||||
let mut sel_chat = if !chat_id.is_unset() {
|
||||
Chat::load_from_db(&context, *chat_id).await.ok()
|
||||
Some(Chat::load_from_db(&context, *chat_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -350,6 +360,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
connectivity\n\
|
||||
maybenetwork\n\
|
||||
housekeeping\n\
|
||||
help imex (Import/Export)\n\
|
||||
@@ -359,6 +370,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat [<chat-id>|0]\n\
|
||||
createchat <contact-id>\n\
|
||||
creategroup <name>\n\
|
||||
createbroadcast\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
@@ -371,8 +383,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendsticker <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
sendupdate <msg-id> <json status update>\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
@@ -386,13 +401,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
protect <chat-id>\n\
|
||||
unprotect <chat-id>\n\
|
||||
delchat <chat-id>\n\
|
||||
===========================Contact requests==\n\
|
||||
decidestartchat <msg-id>\n\
|
||||
decideblock <msg-id>\n\
|
||||
decidenotnow <msg-id>\n\
|
||||
accept <chat-id>\n\
|
||||
decline <chat-id>\n\
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
download <msg-id>\n\
|
||||
html <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
@@ -410,8 +424,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
listblocked\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getqrsvg [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
@@ -449,27 +465,39 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <setup-code> expected"
|
||||
);
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
}
|
||||
"export-backup" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportBackup, &dir).await?;
|
||||
imex(
|
||||
&context,
|
||||
ImexMode::ExportBackup,
|
||||
dir.as_ref(),
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
imex(&context, ImexMode::ImportBackup, arg1).await?;
|
||||
imex(
|
||||
&context,
|
||||
ImexMode::ImportBackup,
|
||||
arg1.as_ref(),
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"export-keys" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
@@ -496,19 +524,33 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"set" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let key = config::Config::from_str(arg1)?;
|
||||
let value = if arg2.is_empty() { None } else { Some(arg2) };
|
||||
context.set_config(key, value).await?;
|
||||
}
|
||||
"get" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let key = config::Config::from_str(arg1)?;
|
||||
let val = context.get_config(key).await;
|
||||
println!("{}={:?}", key, val);
|
||||
}
|
||||
"info" => {
|
||||
println!("{:#?}", context.get_info().await);
|
||||
}
|
||||
"connectivity" => {
|
||||
let file = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("connectivity.html");
|
||||
match context.get_connectivity_html().await {
|
||||
Ok(html) => {
|
||||
fs::write(&file, html)?;
|
||||
println!("Report written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get connectivity html: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
"maybenetwork" => {
|
||||
context.maybe_network().await;
|
||||
}
|
||||
@@ -525,9 +567,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let time_needed = std::time::SystemTime::now()
|
||||
.duration_since(time_start)
|
||||
.unwrap_or_default();
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
|
||||
let cnt = chatlist.len();
|
||||
if cnt > 0 {
|
||||
@@ -536,9 +576,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}{}{}",
|
||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
@@ -550,27 +590,31 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
if chat.is_protected() { "🛡️" } else { "" },
|
||||
if chat.is_contact_request() {
|
||||
"🆕"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
|
||||
let summary = chatlist.get_summary(&context, i, Some(&chat)).await?;
|
||||
let statestr = if chat.visibility == ChatVisibility::Archived {
|
||||
" [Archived]"
|
||||
} else {
|
||||
match lot.get_state() {
|
||||
LotState::MsgOutPending => " o",
|
||||
LotState::MsgOutDelivered => " √",
|
||||
LotState::MsgOutMdnRcvd => " √√",
|
||||
LotState::MsgOutFailed => " !!",
|
||||
match summary.state {
|
||||
MessageState::OutPending => " o",
|
||||
MessageState::OutDelivered => " √",
|
||||
MessageState::OutMdnRcvd => " √√",
|
||||
MessageState::OutFailed => " !!",
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
let timestr = dc_timestamp_to_str(lot.get_timestamp());
|
||||
let text1 = lot.get_text1();
|
||||
let text2 = lot.get_text2();
|
||||
let timestr = dc_timestamp_to_str(summary.timestamp);
|
||||
println!(
|
||||
"{}{}{}{} [{}]{}",
|
||||
text1.unwrap_or(""),
|
||||
if text1.is_some() { ": " } else { "" },
|
||||
text2.unwrap_or(""),
|
||||
"{}{}{} [{}]{}",
|
||||
summary
|
||||
.prefix
|
||||
.map_or_else(String::new, |prefix| format!("{}: ", prefix)),
|
||||
summary.text,
|
||||
statestr,
|
||||
×tr,
|
||||
if chat.is_sending_locations() {
|
||||
@@ -584,7 +628,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending_locations_to_chat(&context, None).await {
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{} chats", cnt);
|
||||
@@ -604,7 +648,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "Failed to select chat");
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist =
|
||||
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
@@ -659,44 +707,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"{} messages.",
|
||||
sel_chat.get_id().get_msg_cnt(&context).await?
|
||||
);
|
||||
|
||||
let time_noticed_start = std::time::SystemTime::now();
|
||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||
|
||||
println!(
|
||||
"{:?} to create this list, {:?} to mark all messages as noticed.",
|
||||
time_needed, time_noticed_needed
|
||||
);
|
||||
}
|
||||
"createchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let chat_id = chat::create_by_contact_id(&context, contact_id).await?;
|
||||
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
}
|
||||
"decidestartchat" | "createchatbymsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
match message::decide_on_contact_request(
|
||||
&context,
|
||||
msg_id,
|
||||
ContactRequestDecision::StartChat,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(chat_id) => {
|
||||
let chat = Chat::load_from_db(&context, chat_id).await?;
|
||||
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id);
|
||||
}
|
||||
None => println!("Cannot crate chat."),
|
||||
}
|
||||
}
|
||||
"decidenotnow" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::NotNow)
|
||||
.await;
|
||||
}
|
||||
"decideblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::Block)
|
||||
.await;
|
||||
}
|
||||
"creategroup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
@@ -704,6 +731,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!("Group#{} created successfully.", chat_id);
|
||||
}
|
||||
"createbroadcast" => {
|
||||
let chat_id = chat::create_broadcast_list(&context).await?;
|
||||
|
||||
println!("Broadcast#{} created successfully.", chat_id);
|
||||
}
|
||||
"createprotected" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
@@ -716,17 +748,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id_0: u32 = arg1.parse()?;
|
||||
if chat::add_contact_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Contact added to chat.");
|
||||
} else {
|
||||
bail!("Cannot add contact to chat.");
|
||||
}
|
||||
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
|
||||
.await?;
|
||||
println!("Contact added to chat.");
|
||||
}
|
||||
"removemember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -744,7 +768,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"groupname" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
chat::set_chat_name(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
&format!("{} {}", arg1, arg2).trim(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
@@ -764,7 +793,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!(
|
||||
"{} contacts\nLocation streaming: {}",
|
||||
contacts.len(),
|
||||
@@ -772,7 +801,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await,
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -817,7 +846,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -854,12 +883,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||
}
|
||||
"sendimage" | "sendfile" => {
|
||||
"sendimage" | "sendsticker" | "sendfile" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No file given.");
|
||||
|
||||
let mut msg = Message::new(if arg0 == "sendimage" {
|
||||
Viewtype::Image
|
||||
} else if arg0 == "sendsticker" {
|
||||
Viewtype::Sticker
|
||||
} else {
|
||||
Viewtype::File
|
||||
});
|
||||
@@ -885,6 +916,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}));
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||
None => println!("sync message not needed."),
|
||||
},
|
||||
"sendupdate" => {
|
||||
ensure!(
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <json status update> expected"
|
||||
);
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
context
|
||||
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
|
||||
.await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
@@ -892,16 +937,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
let chat = if let Some(ref sel_chat) = sel_chat {
|
||||
Some(sel_chat.get_id())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist = context.search_msgs(chat, arg1).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
|
||||
log_msglist(&context, &msglist).await?;
|
||||
println!("{} messages.", msglist.len());
|
||||
println!("{:?} to create this list", time_needed);
|
||||
}
|
||||
"draft" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -1008,12 +1051,28 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id.delete(&context).await?;
|
||||
}
|
||||
"accept" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id.accept(&context).await?;
|
||||
}
|
||||
"blockchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id.block(&context).await?;
|
||||
}
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
println!("{}", res);
|
||||
}
|
||||
"download" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
println!("Scheduling download for {:?}", id);
|
||||
id.download_full(&context).await?;
|
||||
}
|
||||
"html" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
@@ -1045,13 +1104,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut msg_ids = vec![MsgId::new(0)];
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::markseen_msgs(&context, msg_ids).await;
|
||||
message::markseen_msgs(&context, msg_ids).await?;
|
||||
}
|
||||
"delmsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut ids = [MsgId::new(0); 1];
|
||||
ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::delete_msgs(&context, &ids).await;
|
||||
message::delete_msgs(&context, &ids).await?;
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
@@ -1064,7 +1123,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Some(arg1),
|
||||
)
|
||||
.await?;
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts.", contacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
@@ -1072,7 +1131,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
if !arg2.is_empty() {
|
||||
let book = format!("{}\n{}", arg1, arg2);
|
||||
Contact::add_address_book(&context, book).await?;
|
||||
Contact::add_address_book(&context, &book).await?;
|
||||
} else {
|
||||
Contact::create(&context, "", arg1).await?;
|
||||
}
|
||||
@@ -1106,7 +1165,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
if 0 != i {
|
||||
res += ", ";
|
||||
}
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
|
||||
}
|
||||
}
|
||||
@@ -1120,28 +1179,22 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"block" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
Contact::block(&context, contact_id).await;
|
||||
Contact::block(&context, contact_id).await?;
|
||||
}
|
||||
"unblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
Contact::unblock(&context, contact_id).await;
|
||||
Contact::unblock(&context, contact_id).await?;
|
||||
}
|
||||
"listblocked" => {
|
||||
let contacts = Contact::get_all_blocked(&context).await?;
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} blocked contacts.", contacts.len());
|
||||
}
|
||||
"checkqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let res = check_qr(&context, arg1).await;
|
||||
println!(
|
||||
"state={}, id={}, text1={:?}, text2={:?}",
|
||||
res.get_state(),
|
||||
res.get_id(),
|
||||
res.get_text1(),
|
||||
res.get_text2()
|
||||
);
|
||||
let qr = check_qr(&context, arg1).await?;
|
||||
println!("qr={:?}", qr);
|
||||
}
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
@@ -1152,7 +1205,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
match provider::get_provider_info(arg1).await {
|
||||
let socks5_enabled = context
|
||||
.get_config_bool(config::Config::Socks5Enabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {}:", arg1);
|
||||
println!("status: {}", info.status as u32);
|
||||
|
||||
@@ -26,12 +26,15 @@ use rustyline::config::OutputStreamType;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
use rustyline::validate::Validator;
|
||||
use rustyline::{
|
||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyPress,
|
||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
||||
};
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use std::fs;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: EventType) {
|
||||
@@ -56,9 +59,6 @@ fn receive_event(event: EventType) {
|
||||
EventType::Error(msg) => {
|
||||
error!("{}", msg);
|
||||
}
|
||||
EventType::ErrorNetwork(msg) => {
|
||||
error!("[NETWORK] msg={}", msg);
|
||||
}
|
||||
EventType::ErrorSelfNotInGroup(msg) => {
|
||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ const IMEX_COMMANDS: [&str; 12] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 9] = [
|
||||
const DB_COMMANDS: [&str; 10] = [
|
||||
"info",
|
||||
"set",
|
||||
"get",
|
||||
@@ -164,20 +164,19 @@ const DB_COMMANDS: [&str; 9] = [
|
||||
"configure",
|
||||
"connect",
|
||||
"disconnect",
|
||||
"connectivity",
|
||||
"maybenetwork",
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 34] = [
|
||||
const CHAT_COMMANDS: [&str; 36] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
"createchat",
|
||||
"decidestartchat",
|
||||
"decideblock",
|
||||
"decidenotnow",
|
||||
"creategroup",
|
||||
"createverified",
|
||||
"createbroadcast",
|
||||
"createprotected",
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
@@ -191,6 +190,8 @@ const CHAT_COMMANDS: [&str; 34] = [
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -203,14 +204,17 @@ const CHAT_COMMANDS: [&str; 34] = [
|
||||
"protect",
|
||||
"unprotect",
|
||||
"delchat",
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 6] = [
|
||||
const MESSAGE_COMMANDS: [&str; 7] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
@@ -223,10 +227,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
@@ -237,7 +243,9 @@ const MISC_COMMANDS: [&str; 10] = [
|
||||
];
|
||||
|
||||
impl Hinter for DcHelper {
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<String> {
|
||||
type Hint = String;
|
||||
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<Self::Hint> {
|
||||
if !line.is_empty() {
|
||||
for &cmds in &[
|
||||
&IMEX_COMMANDS[..],
|
||||
@@ -259,11 +267,10 @@ impl Hinter for DcHelper {
|
||||
}
|
||||
|
||||
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
||||
static PROMPT: &str = "> ";
|
||||
|
||||
impl Highlighter for DcHelper {
|
||||
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
|
||||
if prompt == PROMPT {
|
||||
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&self, prompt: &'p str, default: bool) -> Cow<'b, str> {
|
||||
if default {
|
||||
Borrowed(COLORED_PROMPT)
|
||||
} else {
|
||||
Borrowed(prompt)
|
||||
@@ -284,13 +291,14 @@ impl Highlighter for DcHelper {
|
||||
}
|
||||
|
||||
impl Helper for DcHelper {}
|
||||
impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
@@ -317,15 +325,15 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
};
|
||||
let mut rl = Editor::with_config(config);
|
||||
rl.set_helper(Some(h));
|
||||
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
||||
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
||||
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
|
||||
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
|
||||
if rl.load_history(".dc-history.txt").is_err() {
|
||||
println!("No previous history.");
|
||||
}
|
||||
|
||||
loop {
|
||||
let p = "> ";
|
||||
let readline = rl.readline(&p);
|
||||
let readline = rl.readline(p);
|
||||
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
@@ -392,7 +400,7 @@ async fn handle_cmd(
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
} else {
|
||||
@@ -409,18 +417,31 @@ async fn handle_cmd(
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
if let Some(mut qr) = dc_get_securejoin_qr(&ctx, group).await {
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg)?;
|
||||
println!("QR code svg written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tempfile::tempdir;
|
||||
|
||||
use deltachat::chat;
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
@@ -19,7 +19,7 @@ fn cb(event: EventType) {
|
||||
EventType::Warning(msg) => {
|
||||
log::warn!("{}", msg);
|
||||
}
|
||||
EventType::Error(msg) | EventType::ErrorNetwork(msg) => {
|
||||
EventType::Error(msg) => {
|
||||
log::error!("{}", msg);
|
||||
}
|
||||
event => {
|
||||
@@ -36,7 +36,7 @@ async fn main() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), 0)
|
||||
let ctx = Context::new(dbfile.into(), 0)
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
@@ -70,7 +70,7 @@ async fn main() {
|
||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
|
||||
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
|
||||
|
||||
for i in 0..1 {
|
||||
log::info!("sending message {}", i);
|
||||
@@ -86,7 +86,7 @@ async fn main() {
|
||||
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
||||
|
||||
for i in 0..chats.len() {
|
||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
|
||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
log::info!("[{}] msg: {:?}", i, msg);
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
|
||||
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
|
||||
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
|
||||
cc 9796807baeda701227dcdcfc9fdaa93ddd556da2bb1630381bfe2e037bee73f6 # shrinks to buf = " ꫛ®a\u{11300}a", approx_chars = 0
|
||||
cc 063a4c42ac1ec9aa37af54521b210ba9cd82dcc9cc3be296ca2fedf8240072d4 # shrinks to buf = "a᪠ 0A", approx_chars = 0
|
||||
|
||||
@@ -58,12 +58,13 @@ end-to-end tests that require accounts on real e-mail servers.
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
|
||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
|
||||
|
||||
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
|
||||
These accounts exists for one 1hour and then are removed completely.
|
||||
export DCC_NEW_TMP_EMAIL=<URL you got from us>
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests:
|
||||
|
||||
pytest
|
||||
@@ -142,7 +143,7 @@ This docker image can be used to run tests and build Python wheels for all inter
|
||||
|
||||
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
|
||||
|
||||
Optionally build your own docker image
|
||||
@@ -151,9 +152,9 @@ Optionally build your own docker image
|
||||
If you want to build your own custom docker image you can do this::
|
||||
|
||||
$ cd deltachat-core # cd to deltachat-core checkout directory
|
||||
$ docker build -t deltachat/coredeps ci_scripts/docker_coredeps
|
||||
$ docker build -t deltachat/coredeps scripts/docker_coredeps
|
||||
|
||||
This will use the ``ci_scripts/docker_coredeps/Dockerfile`` to build
|
||||
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
|
||||
up docker image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
|
||||
19
python/mypy.ini
Normal file
19
python/mypy.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-deltachat.capi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pluggy.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-cffi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-imapclient.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-_pytest.*]
|
||||
ignore_missing_imports = True
|
||||
8
python/pyproject.toml
Normal file
8
python/pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
@@ -8,17 +8,11 @@ def main():
|
||||
long_description = f.read()
|
||||
setuptools.setup(
|
||||
name='deltachat',
|
||||
setup_requires=['setuptools_scm', 'cffi>=1.0.0'],
|
||||
use_scm_version = {
|
||||
"root": "..",
|
||||
"relative_to": __file__,
|
||||
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
|
||||
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
|
||||
},
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient'],
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -19,9 +19,9 @@ except DistributionNotFound:
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name, val in vars(const).items():
|
||||
for name in dir(const):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[val] = name
|
||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
configtracker = ac.configure()
|
||||
|
||||
@@ -9,8 +9,6 @@ import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
import types
|
||||
from os.path import abspath
|
||||
from os.path import dirname as dn
|
||||
|
||||
import cffi
|
||||
|
||||
@@ -50,6 +48,7 @@ def system_build_flags():
|
||||
flags.objs = []
|
||||
flags.incs = []
|
||||
flags.extra_link_args = []
|
||||
return flags
|
||||
|
||||
|
||||
def extract_functions(flags):
|
||||
@@ -151,6 +150,7 @@ def extract_defines(flags):
|
||||
| DC_PROVIDER
|
||||
| DC_KEY_GEN
|
||||
| DC_IMEX
|
||||
| DC_CONNECTIVITY
|
||||
) # End of prefix matching
|
||||
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
||||
) # Close the capturing group, this contains
|
||||
@@ -168,11 +168,8 @@ def extract_defines(flags):
|
||||
|
||||
def ffibuilder():
|
||||
projdir = os.environ.get('DCC_RS_DEV')
|
||||
if not projdir:
|
||||
p = dn(dn(dn(dn(abspath(__file__)))))
|
||||
projdir = os.environ["DCC_RS_DEV"] = p
|
||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||
if projdir:
|
||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||
flags = local_build_flags(projdir, target)
|
||||
else:
|
||||
flags = system_build_flags()
|
||||
|
||||
@@ -8,13 +8,14 @@ import os
|
||||
from array import array
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker, ConfigureTracker
|
||||
from . import hookspec
|
||||
from .events import EventThread
|
||||
from typing import Union, Any, Dict, Optional, List, Generator
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
@@ -28,7 +29,7 @@ class Account(object):
|
||||
"""
|
||||
MissingCredentials = MissingCredentials
|
||||
|
||||
def __init__(self, db_path, os_name=None, logging=True):
|
||||
def __init__(self, db_path, os_name=None, logging=True) -> None:
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
@@ -58,11 +59,11 @@ class Account(object):
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def disable_logging(self):
|
||||
def disable_logging(self) -> None:
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self):
|
||||
def enable_logging(self) -> None:
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
@@ -73,12 +74,12 @@ class Account(object):
|
||||
if self._logging:
|
||||
self._pm.hook.ac_log_line(message=msg)
|
||||
|
||||
def _check_config_key(self, name):
|
||||
def _check_config_key(self, name: str) -> None:
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
name, self._configkeys))
|
||||
|
||||
def get_info(self):
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
@@ -89,19 +90,35 @@ class Account(object):
|
||||
d[key.lower()] = value
|
||||
return d
|
||||
|
||||
def set_stock_translation(self, id, string):
|
||||
def dump_account_info(self, logfile):
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = logfile
|
||||
print(*args, **kwargs)
|
||||
|
||||
log("=============== " + self.get_config("displayname") + " ===============")
|
||||
cursor = 0
|
||||
for name, val in self.get_info().items():
|
||||
entry = "{}={}".format(name.upper(), val)
|
||||
if cursor + len(entry) > 80:
|
||||
log("")
|
||||
cursor = 0
|
||||
log(entry, end=" ")
|
||||
cursor += len(entry) + 1
|
||||
log("")
|
||||
|
||||
def set_stock_translation(self, id: int, string: str) -> None:
|
||||
""" set stock translation string.
|
||||
|
||||
:param id: id of stock string (const.DC_STR_*)
|
||||
:param value: string to set as new transalation
|
||||
:returns: None
|
||||
"""
|
||||
string = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, string)
|
||||
bytestring = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
|
||||
if res == 0:
|
||||
raise ValueError("could not set translation string")
|
||||
|
||||
def set_config(self, name, value):
|
||||
def set_config(self, name: str, value: Optional[str]) -> None:
|
||||
""" set configuration values.
|
||||
|
||||
:param name: config key name (unicode)
|
||||
@@ -109,16 +126,16 @@ class Account(object):
|
||||
:returns: None
|
||||
"""
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
if name == b"addr" and self.is_configured():
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if value is not None:
|
||||
value = value.encode("utf8")
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
value = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, name, value)
|
||||
valuebytes = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name):
|
||||
def get_config(self, name: str) -> str:
|
||||
""" return unicode string value.
|
||||
|
||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||
@@ -127,12 +144,12 @@ class Account(object):
|
||||
"""
|
||||
if name != "sys.config_keys":
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, name)
|
||||
namebytes = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, namebytes)
|
||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr, public, secret):
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -144,7 +161,7 @@ class Account(object):
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def update_config(self, kwargs):
|
||||
def update_config(self, kwargs: Dict[str, Any]) -> None:
|
||||
""" update config values.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
@@ -154,7 +171,7 @@ class Account(object):
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
|
||||
def is_configured(self):
|
||||
def is_configured(self) -> bool:
|
||||
""" determine if the account is configured already; an initial connection
|
||||
to SMTP/IMAP has been verified.
|
||||
|
||||
@@ -162,7 +179,13 @@ class Account(object):
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path):
|
||||
def is_open(self) -> bool:
|
||||
"""Determine if account is open
|
||||
|
||||
:returns True if account is open."""
|
||||
return True if lib.dc_context_is_open(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
:raises ValueError: if profile image could not be set
|
||||
@@ -174,20 +197,18 @@ class Account(object):
|
||||
assert os.path.exists(img_path), img_path
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
def check_is_configured(self):
|
||||
def check_is_configured(self) -> None:
|
||||
""" Raise ValueError if this account is not configured. """
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
def get_latest_backupfile(self, backupdir) -> Optional[str]:
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
if res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(res)
|
||||
return from_optional_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self):
|
||||
def get_blobdir(self) -> str:
|
||||
""" return the directory for files.
|
||||
|
||||
All sent files are copied to this directory if necessary.
|
||||
@@ -195,15 +216,15 @@ class Account(object):
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||
|
||||
def get_self_contact(self):
|
||||
def get_self_contact(self) -> Contact:
|
||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
|
||||
"""create a (new) Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
@@ -220,13 +241,13 @@ class Account(object):
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj):
|
||||
def get_contact(self, obj) -> Optional[Contact]:
|
||||
if isinstance(obj, Contact):
|
||||
return obj
|
||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
||||
return self.get_contact_by_addr(addr)
|
||||
|
||||
def get_contact_addr_and_name(self, obj, name=None):
|
||||
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("can only add addresses from configured accounts")
|
||||
@@ -244,7 +265,7 @@ class Account(object):
|
||||
name = displayname
|
||||
return (name, addr)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
def delete_contact(self, contact: Contact) -> bool:
|
||||
""" delete a Contact.
|
||||
|
||||
:param contact: contact object obtained
|
||||
@@ -255,22 +276,23 @@ class Account(object):
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email):
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
if contact_id:
|
||||
return self.get_contact_by_id(contact_id)
|
||||
return None
|
||||
|
||||
def get_contact_by_id(self, contact_id):
|
||||
""" return Contact instance or None.
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
""" return Contact instance or raise an exception.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self):
|
||||
def get_blocked_contacts(self) -> List[Contact]:
|
||||
""" return a list of all blocked contacts.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
@@ -281,8 +303,13 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||
""" get a (filtered) list of contacts.
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
only_verified: bool = False,
|
||||
) -> List[Contact]:
|
||||
"""get a (filtered) list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
@@ -302,7 +329,7 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self):
|
||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
||||
""" yield all fresh messages from all chats. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_fresh_msgs(self._dc_context),
|
||||
@@ -310,15 +337,17 @@ class Account(object):
|
||||
)
|
||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||
|
||||
def create_chat(self, obj):
|
||||
def create_chat(self, obj) -> Chat:
|
||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def _create_chat_by_message_id(self, msg_id):
|
||||
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
|
||||
|
||||
def create_group_chat(self, name, contacts=None, verified=False):
|
||||
""" create a new group chat object.
|
||||
def create_group_chat(
|
||||
self,
|
||||
name: str,
|
||||
contacts: Optional[List[Contact]] = None,
|
||||
verified: bool = False,
|
||||
) -> Chat:
|
||||
"""create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
@@ -334,7 +363,7 @@ class Account(object):
|
||||
chat.add_contact(contact)
|
||||
return chat
|
||||
|
||||
def get_chats(self):
|
||||
def get_chats(self) -> List[Chat]:
|
||||
""" return list of chats.
|
||||
|
||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||
@@ -351,20 +380,17 @@ class Account(object):
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
|
||||
def get_deaddrop_chat(self):
|
||||
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
||||
|
||||
def get_device_chat(self):
|
||||
def get_device_chat(self) -> Chat:
|
||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
||||
|
||||
def get_message_by_id(self, msg_id):
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id):
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
:returns: :class:`deltachat.chat.Chat` instance.
|
||||
@@ -376,19 +402,21 @@ class Account(object):
|
||||
lib.dc_chat_unref(res)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def mark_seen_messages(self, messages):
|
||||
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
|
||||
""" mark the given set of messages as seen.
|
||||
|
||||
:param messages: a list of message ids or Message instances.
|
||||
"""
|
||||
arr = array("i")
|
||||
for msg in messages:
|
||||
msg = getattr(msg, "id", msg)
|
||||
arr.append(msg)
|
||||
if isinstance(msg, Message):
|
||||
arr.append(msg.id)
|
||||
else:
|
||||
arr.append(msg)
|
||||
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||
|
||||
def forward_messages(self, messages, chat):
|
||||
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
|
||||
""" Forward list of messages to a chat.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -398,7 +426,7 @@ class Account(object):
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def delete_messages(self, messages):
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
""" delete messages (local and remote).
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -456,7 +484,7 @@ class Account(object):
|
||||
def imex(self, path, imex_cmd):
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
@@ -467,7 +495,7 @@ class Account(object):
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self):
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -517,7 +545,9 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
def set_location(
|
||||
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
|
||||
) -> None:
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
|
||||
@@ -558,6 +588,15 @@ class Account(object):
|
||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
def get_connectivity(self):
|
||||
return lib.dc_get_connectivity(self._dc_context)
|
||||
|
||||
def get_connectivity_html(self) -> str:
|
||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
||||
|
||||
def all_work_done(self):
|
||||
return lib.dc_all_work_done(self._dc_context)
|
||||
|
||||
def start_io(self):
|
||||
""" start this account's IO scheduling (Rust-core async scheduler)
|
||||
|
||||
@@ -602,7 +641,7 @@ class Account(object):
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure=False):
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
""" Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
@@ -615,11 +654,11 @@ class Account(object):
|
||||
lib.dc_configure(self._dc_context)
|
||||
return configtracker
|
||||
|
||||
def wait_shutdown(self):
|
||||
def wait_shutdown(self) -> None:
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
|
||||
def stop_io(self):
|
||||
def stop_io(self) -> None:
|
||||
""" stop core IO scheduler if it is running. """
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
@@ -627,7 +666,7 @@ class Account(object):
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
if self._dc_context is None:
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import mimetypes
|
||||
import calendar
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .message import Message
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Chat(object):
|
||||
@@ -17,20 +18,20 @@ class Chat(object):
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, id):
|
||||
def __init__(self, account, id) -> None:
|
||||
from .account import Account
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.id == getattr(other, "id", None) and \
|
||||
self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
|
||||
@property
|
||||
@@ -40,7 +41,7 @@ class Chat(object):
|
||||
lib.dc_chat_unref
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -50,29 +51,37 @@ class Chat(object):
|
||||
"""
|
||||
lib.dc_delete_chat(self.account._dc_context, self.id)
|
||||
|
||||
def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
lib.dc_block_chat(self.account._dc_context, self.id)
|
||||
|
||||
def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
lib.dc_accept_chat(self.account._dc_context, self.id)
|
||||
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self):
|
||||
def is_group(self) -> bool:
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_deaddrop(self):
|
||||
""" return true if this chat is a deaddrop chat.
|
||||
|
||||
:returns: True if chat is the deaddrop chat, False otherwise.
|
||||
"""
|
||||
return self.id == const.DC_CHAT_ID_DEADDROP
|
||||
|
||||
def is_muted(self):
|
||||
def is_muted(self) -> bool:
|
||||
""" return true if this chat is muted.
|
||||
|
||||
:returns: True if chat is muted, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_muted(self._dc_chat)
|
||||
|
||||
def is_contact_request(self):
|
||||
""" return True if this chat is a contact request chat.
|
||||
|
||||
:returns: True if chat is a contact request chat, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_contact_request(self._dc_chat)
|
||||
|
||||
def is_promoted(self):
|
||||
""" return True if this chat is promoted, i.e.
|
||||
the member contacts are aware of their membership,
|
||||
@@ -82,38 +91,38 @@ class Chat(object):
|
||||
"""
|
||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||
|
||||
def can_send(self):
|
||||
def can_send(self) -> bool:
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the deaddrop or for the device-talk
|
||||
This is not true eg. for the contact requests or for the device-talk
|
||||
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
return lib.dc_chat_can_send(self._dc_chat)
|
||||
|
||||
def is_protected(self):
|
||||
def is_protected(self) -> bool:
|
||||
""" return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_protected(self._dc_chat)
|
||||
|
||||
def get_name(self):
|
||||
def get_name(self) -> Optional[str]:
|
||||
""" return name of this chat.
|
||||
|
||||
:returns: unicode name
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||
|
||||
def set_name(self, name):
|
||||
def set_name(self, name: str) -> bool:
|
||||
""" set name of this chat.
|
||||
|
||||
:param name: as a unicode string.
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
|
||||
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
|
||||
|
||||
def mute(self, duration=None):
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
""" mutes the chat
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
@@ -127,7 +136,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self):
|
||||
def unmute(self) -> None:
|
||||
""" unmutes the chat
|
||||
|
||||
:returns: None
|
||||
@@ -136,7 +145,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
def get_mute_duration(self):
|
||||
def get_mute_duration(self) -> int:
|
||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||
|
||||
:param duration:
|
||||
@@ -144,37 +153,37 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
def get_ephemeral_timer(self) -> int:
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer):
|
||||
def set_ephemeral_timer(self, timer: int) -> bool:
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
|
||||
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
|
||||
|
||||
def get_type(self):
|
||||
def get_type(self) -> int:
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat)
|
||||
|
||||
def get_encryption_info(self):
|
||||
def get_encryption_info(self) -> Optional[str]:
|
||||
"""Return encryption info for this chat.
|
||||
|
||||
:returns: a string with encryption preferences of all chat members"""
|
||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_join_qr(self):
|
||||
def get_join_qr(self) -> Optional[str]:
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -186,7 +195,7 @@ class Chat(object):
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
|
||||
def send_msg(self, msg):
|
||||
def send_msg(self, msg: Message) -> Message:
|
||||
"""send a message by using a ready Message object.
|
||||
|
||||
:param msg: a :class:`deltachat.message.Message` instance
|
||||
@@ -504,8 +513,9 @@ class Chat(object):
|
||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
||||
timestamp=datetime.utcfromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i)
|
||||
timestamp=datetime.fromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i),
|
||||
timezone.utc
|
||||
),
|
||||
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from typing import Any, List
|
||||
|
||||
from .capi import lib
|
||||
|
||||
|
||||
for name in dir(lib):
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name.startswith("DC_"):
|
||||
globals()[name] = getattr(lib, name)
|
||||
del name
|
||||
return getattr(lib, name)
|
||||
return globals()[name]
|
||||
|
||||
|
||||
def __dir__() -> List[str]:
|
||||
return sorted(name for name in dir(lib) if name.startswith("DC_"))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
""" Contact object. """
|
||||
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .chat import Chat
|
||||
from . import const
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
||||
|
||||
|
||||
class Contact(object):
|
||||
@@ -35,18 +37,25 @@ class Contact(object):
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
def addr(self) -> str:
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
display_name = name
|
||||
|
||||
@props.with_doc
|
||||
def last_seen(self) -> date:
|
||||
"""Last seen timestamp."""
|
||||
return datetime.fromtimestamp(
|
||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
||||
)
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
@@ -67,15 +76,13 @@ class Contact(object):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_profile_image(self):
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
"""
|
||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
return from_optional_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, TypeVar, Generator, Callable
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
@@ -11,21 +14,28 @@ def as_dc_charpointer(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor):
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
def from_dc_charpointer(obj) -> str:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
raise ValueError
|
||||
|
||||
|
||||
def from_optional_dc_charpointer(obj) -> Optional[str]:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
return None
|
||||
|
||||
|
||||
class DCLot:
|
||||
def __init__(self, dc_lot):
|
||||
def __init__(self, dc_lot) -> None:
|
||||
self._dc_lot = dc_lot
|
||||
|
||||
def id(self):
|
||||
def id(self) -> int:
|
||||
return lib.dc_lot_get_id(self._dc_lot)
|
||||
|
||||
def state(self):
|
||||
@@ -44,4 +54,4 @@ class DCLot:
|
||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
||||
if ts == 0:
|
||||
return None
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@@ -11,7 +11,7 @@ from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const
|
||||
from deltachat import const, Account
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
@@ -47,8 +47,9 @@ def dc_account_extra_configure(account):
|
||||
|
||||
except Exception as e:
|
||||
# Uncaught exceptions here would lead to a timeout without any note written to the log
|
||||
account.log("=============================== CAN'T RESET ACCOUNT: ===============================")
|
||||
account.log("===================", e, "===================")
|
||||
# start with DC_EVENT_WARNING so that the line is printed in yellow and won't be overlooked when reading
|
||||
account.log("DC_EVENT_WARNING =================== DIRECT_IMAP CAN'T RESET ACCOUNT: ===================")
|
||||
account.log("DC_EVENT_WARNING =================== " + str(e) + " ===================")
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
@@ -61,7 +62,7 @@ def dc_account_after_shutdown(account):
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account):
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
@@ -172,21 +173,6 @@ class DirectImap:
|
||||
def get_unread_cnt(self):
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_account_info(self, logfile):
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = logfile
|
||||
print(*args, **kwargs)
|
||||
|
||||
cursor = 0
|
||||
for name, val in self.account.get_info().items():
|
||||
entry = "{}={}".format(name.upper(), val)
|
||||
if cursor + len(entry) > 80:
|
||||
log("")
|
||||
cursor = 0
|
||||
log(entry, end=" ")
|
||||
cursor += len(entry) + 1
|
||||
log("")
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
assert not self._idling
|
||||
stream = io.StringIO()
|
||||
@@ -265,7 +251,16 @@ class DirectImap:
|
||||
return res
|
||||
|
||||
def append(self, folder, msg):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(folder, msg)
|
||||
|
||||
def get_uid_by_message_id(self, message_id):
|
||||
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
@@ -9,11 +9,11 @@ from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_dc_charpointer
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name, data1, data2):
|
||||
def __init__(self, name: str, data1, data2):
|
||||
self.name = name
|
||||
self.data1 = data1
|
||||
self.data2 = data2
|
||||
@@ -29,18 +29,18 @@ class FFIEventLogger:
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
||||
self.account.log(str(ffi_event))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_log_line(self, message):
|
||||
t = threading.currentThread()
|
||||
t = threading.current_thread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
@@ -69,7 +69,7 @@ class FFIEventTracker:
|
||||
self._event_queue = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent):
|
||||
self._event_queue.put(ffi_event)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
@@ -96,7 +96,7 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_contains(self, regex):
|
||||
def get_info_contains(self, regex: str) -> FFIEvent:
|
||||
rex = re.compile(regex)
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
@@ -111,6 +111,33 @@ class FFIEventTracker:
|
||||
if m is not None:
|
||||
return m.groups()
|
||||
|
||||
def wait_for_connectivity(self, connectivity):
|
||||
"""Wait for the specified connectivity.
|
||||
This only works reliably if the connectivity doesn't change
|
||||
again too quickly, otherwise we might miss it."""
|
||||
while 1:
|
||||
if self.account.get_connectivity() == connectivity:
|
||||
return
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def wait_for_connectivity_change(self, previous, expected_next):
|
||||
"""Wait until the connectivity changes to `expected_next`.
|
||||
Fails the test if it changes to something else."""
|
||||
while 1:
|
||||
current = self.account.get_connectivity()
|
||||
if current == expected_next:
|
||||
return
|
||||
elif current != previous:
|
||||
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
|
||||
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def wait_for_all_work_done(self):
|
||||
while 1:
|
||||
if self.account.all_work_done():
|
||||
return
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
@@ -149,6 +176,7 @@ class FFIEventTracker:
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_msg_delivered(self, msg):
|
||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
@@ -162,10 +190,10 @@ class EventThread(threading.Thread):
|
||||
|
||||
With each Account init this callback thread is started.
|
||||
"""
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self.daemon = True
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
@@ -175,17 +203,17 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def mark_shutdown(self):
|
||||
def mark_shutdown(self) -> None:
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self, timeout=None) -> None:
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
""" get and run events until shutdown. """
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
self._inner_run()
|
||||
@@ -207,7 +235,7 @@ class EventThread(threading.Thread):
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
data2 = lib.dc_event_get_data2_int(event)
|
||||
|
||||
@@ -223,7 +251,7 @@ class EventThread(threading.Thread):
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
account = self.account
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
|
||||
@@ -43,7 +43,7 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_incoming_message(self, message):
|
||||
""" Called on any incoming message (to deaddrop or chat). """
|
||||
""" Called on any incoming message (both existing chats and contact requests). """
|
||||
|
||||
@account_hookspec
|
||||
def ac_outgoing_message(self, message):
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -61,16 +62,13 @@ class Message(object):
|
||||
def create_chat(self):
|
||||
""" create or get an existing chat (group) object for this message.
|
||||
|
||||
If the message is a deaddrop contact request
|
||||
If the message is a contact request
|
||||
the sender will become an accepted contact.
|
||||
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
from .chat import Chat
|
||||
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
|
||||
ctx = self.account._dc_context
|
||||
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
|
||||
return Chat(self.account, chat_id)
|
||||
self.chat.accept()
|
||||
return self.chat
|
||||
|
||||
@props.with_doc
|
||||
def id(self):
|
||||
@@ -78,7 +76,7 @@ class Message(object):
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
def text(self) -> str:
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||
|
||||
@@ -86,6 +84,23 @@ class Message(object):
|
||||
"""set text of this message. """
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
def html(self) -> str:
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
|
||||
def has_html(self):
|
||||
"""return True if this message has an html part, False otherwise."""
|
||||
return lib.dc_msg_has_html(self._dc_msg)
|
||||
|
||||
def set_html(self, html_text):
|
||||
"""set the html part of this message.
|
||||
|
||||
It is possible to have text and html part at the same time.
|
||||
"""
|
||||
lib.dc_msg_set_html(self._dc_msg, as_dc_charpointer(html_text))
|
||||
|
||||
@props.with_doc
|
||||
def filename(self):
|
||||
"""filename if there was an attachment, otherwise empty string. """
|
||||
@@ -99,12 +114,13 @@ class Message(object):
|
||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||
|
||||
@props.with_doc
|
||||
def basename(self):
|
||||
def basename(self) -> str:
|
||||
"""basename of the attachment if it exists, otherwise empty string. """
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self):
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
@@ -116,7 +132,7 @@ class Message(object):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self):
|
||||
def get_setupcodebegin(self) -> str:
|
||||
""" return the first characters of a setup code in a setup message. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
@@ -124,11 +140,15 @@ class Message(object):
|
||||
""" return True if this message was encrypted. """
|
||||
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
|
||||
|
||||
def is_bot(self):
|
||||
""" return True if this message is submitted automatically. """
|
||||
return bool(lib.dc_msg_is_bot(self._dc_msg))
|
||||
|
||||
def is_forwarded(self):
|
||||
""" return True if this message was forwarded. """
|
||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
||||
|
||||
def get_message_info(self):
|
||||
def get_message_info(self) -> str:
|
||||
""" Return informational text for a single message.
|
||||
|
||||
The text is multiline and may contain eg. the raw text of the message.
|
||||
@@ -152,7 +172,7 @@ class Message(object):
|
||||
:returns: naive datetime.datetime() object.
|
||||
"""
|
||||
ts = lib.dc_msg_get_timestamp(self._dc_msg)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@props.with_doc
|
||||
def time_received(self):
|
||||
@@ -162,7 +182,7 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
@@ -182,14 +202,14 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
|
||||
:returns: Quoted text"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
@@ -205,6 +225,10 @@ class Message(object):
|
||||
"""Quote setter"""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -222,9 +246,9 @@ class Message(object):
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
def chat(self):
|
||||
@@ -236,6 +260,20 @@ class Message(object):
|
||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def override_sender_name(self) -> Optional[str]:
|
||||
"""the name that should be shown over the message instead of the contact display name.
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
|
||||
def set_override_sender_name(self, name):
|
||||
"""set different sender name for a message. """
|
||||
lib.dc_msg_set_override_sender_name(
|
||||
self._dc_msg, as_dc_charpointer(name))
|
||||
|
||||
def get_sender_chat(self):
|
||||
"""return the 1:1 chat with the sender of this message.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class Provider(object):
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, account, addr):
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
@@ -24,19 +24,19 @@ class Provider(object):
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self):
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self):
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_before_login_hints(self._provider))
|
||||
lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
|
||||
@@ -9,6 +9,7 @@ import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
from typing import List, Dict, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
def __init__(self, fn) -> None:
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
configlist: List[Dict[str, str]]
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@@ -179,7 +182,7 @@ def session_liveconfig(request):
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# trying to find test data heuristically
|
||||
# because we are run from a dev-setup with pytest direct,
|
||||
# through tox, and then maybe also from deltachat-binding
|
||||
@@ -210,7 +213,10 @@ def data(request):
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
@@ -297,21 +303,20 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
def get_online_configuring_account(self, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_watch"] = str(int(mvbox))
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
def get_one_online_account(self, pre_generated_key=True, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
pre_generated_key=pre_generated_key, move=move)
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
@@ -330,7 +335,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
|
||||
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
|
||||
direct_imap object of an online account. This simulates the user setting
|
||||
up a new device without importing a backup.
|
||||
|
||||
@@ -345,7 +350,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_watch=account.get_config("mvbox_watch"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
@@ -414,16 +418,16 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
def dump_imap_summary(self, logfile):
|
||||
for ac in self._accounts:
|
||||
ac.dump_account_info(logfile=logfile)
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
try:
|
||||
imap.idle_done()
|
||||
except Exception:
|
||||
pass
|
||||
imap.dump_account_info(logfile=logfile)
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1, ac2):
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
@@ -451,7 +455,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
|
||||
class BotProcess:
|
||||
def __init__(self, popen, bot_cfg):
|
||||
stdout_queue: queue.Queue
|
||||
|
||||
def __init__(self, popen, bot_cfg) -> None:
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
|
||||
@@ -459,10 +465,10 @@ class BotProcess:
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.setDaemon(1)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self):
|
||||
def _run_stdout_thread(self) -> None:
|
||||
try:
|
||||
while 1:
|
||||
line = self.popen.stdout.readline()
|
||||
@@ -474,10 +480,10 @@ class BotProcess:
|
||||
finally:
|
||||
self.stdout_queue.put(None)
|
||||
|
||||
def kill(self):
|
||||
def kill(self) -> None:
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30):
|
||||
def wait(self, timeout=30) -> None:
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
@@ -509,14 +515,14 @@ def tmp_db_path(tmpdir):
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
def sec(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg):
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -90,11 +90,11 @@ class ConfigureTracker:
|
||||
if data1 is None or evdata == data1:
|
||||
break
|
||||
|
||||
def wait_finish(self):
|
||||
def wait_finish(self, timeout=None):
|
||||
""" wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
if not self._configure_events.get():
|
||||
if not self._configure_events.get(timeout=timeout):
|
||||
content = "\n".join(map(str, self._ffi_events))
|
||||
raise ConfigureFailed(content)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
workspacedir = sys.argv[1]
|
||||
arch = platform.machine()
|
||||
for relpath in os.listdir(workspacedir):
|
||||
if relpath.startswith("deltachat"):
|
||||
p = os.path.join(workspacedir, relpath)
|
||||
subprocess.check_call(
|
||||
["auditwheel", "repair", p, "-w", workspacedir,
|
||||
"--plat", "manylinux2014_x86_64"])
|
||||
"--plat", "manylinux2014_" + arch])
|
||||
|
||||
@@ -10,7 +10,7 @@ from deltachat.tracker import ImexTracker
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
@@ -41,8 +41,8 @@ class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
p.write("123")
|
||||
with pytest.raises(ValueError):
|
||||
Account(p.strpath)
|
||||
account = Account(p.strpath)
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
@@ -57,7 +57,7 @@ class TestOfflineAccountBasic:
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.com", alice_public, alice_secret)
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -265,23 +265,23 @@ class TestOfflineChat:
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
msg = chat.get_draft()
|
||||
assert msg.text == "xyz title1"
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
@@ -447,7 +447,7 @@ class TestOfflineChat:
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
past1s = datetime.utcnow() - timedelta(seconds=1)
|
||||
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
msg = chat1.send_text("msg1")
|
||||
ts = msg.time_sent
|
||||
assert msg.time_received is None
|
||||
@@ -737,7 +737,6 @@ class TestOnlineAccount:
|
||||
# make sure we are not sending message to ourselves
|
||||
assert self_addr not in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
|
||||
lp.sec("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
@@ -753,7 +752,6 @@ class TestOnlineAccount:
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
assert ac1.direct_imap.idle_wait_for_seen()
|
||||
|
||||
# Second client receives only second message, but not the first
|
||||
@@ -816,9 +814,51 @@ class TestOnlineAccount:
|
||||
assert open(msg.filename).read() == content
|
||||
assert msg.filename.endswith(basename)
|
||||
|
||||
def test_html_message(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
html_text = "<p>hello HTML world</p>"
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.has_html()
|
||||
assert msg1.html == ""
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.has_html()
|
||||
assert msg2.html == ""
|
||||
|
||||
lp.sec("ac1: prepare and send HTML+text message to ac2")
|
||||
msg1 = Message.new_empty(ac1, "text")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_html(html_text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.has_html()
|
||||
assert html_text in msg1.html
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert msg2.has_html()
|
||||
assert html_text in msg2.html
|
||||
|
||||
lp.sec("ac1: prepare and send HTML-only message to ac2")
|
||||
msg1 = Message.new_empty(ac1, "text")
|
||||
msg1.set_html(html_text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert "<p>" not in msg2.text
|
||||
assert "hello HTML world" in msg2.text
|
||||
assert msg2.has_html()
|
||||
assert html_text in msg2.html
|
||||
|
||||
def test_mvbox_sentbox_threads(self, acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True, sentbox=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True, sentbox=True)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
@@ -832,16 +872,20 @@ class TestOnlineAccount:
|
||||
|
||||
def test_move_works(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
def test_move_works_on_self_sent(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
@@ -867,12 +911,11 @@ class TestOnlineAccount:
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
assert msg_in.text == "message2"
|
||||
|
||||
lp.sec("ac2: check that the message arrive in deaddrop")
|
||||
lp.sec("ac2: check that the message arrived in a chat")
|
||||
chat2 = msg_in.chat
|
||||
assert msg_in in chat2.get_messages()
|
||||
assert not msg_in.is_forwarded()
|
||||
assert chat2.is_deaddrop()
|
||||
assert chat2 == ac2.get_deaddrop_chat()
|
||||
assert chat2.is_contact_request()
|
||||
|
||||
lp.sec("ac2: create new chat and forward message to it")
|
||||
chat3 = ac2.create_group_chat("newgroup")
|
||||
@@ -912,7 +955,7 @@ class TestOnlineAccount:
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
def test_send_self_message(self, acfactory, lp):
|
||||
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
|
||||
ac1 = acfactory.get_one_online_account(move=True)
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
chat.send_text("hello")
|
||||
@@ -932,21 +975,22 @@ class TestOnlineAccount:
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert not msg2.is_forwarded()
|
||||
assert msg2.get_sender_contact().display_name == ac1.get_config("displayname")
|
||||
|
||||
lp.sec("check the message arrived in contact-requests/deaddrop")
|
||||
lp.sec("check the message arrived in contact request chat")
|
||||
chat2 = msg2.chat
|
||||
assert msg2 in chat2.get_messages()
|
||||
assert chat2.is_deaddrop()
|
||||
assert chat2.count_fresh_messages() == 0
|
||||
assert msg2.time_received >= msg1.time_sent
|
||||
assert chat2.is_contact_request()
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
# Like it or not, this assert is flaky
|
||||
# assert msg2.time_received >= msg1.time_sent
|
||||
|
||||
lp.sec("create new chat with contact and verify it's proper")
|
||||
chat2b = msg2.create_chat()
|
||||
assert not chat2b.is_deaddrop()
|
||||
assert not chat2b.is_contact_request()
|
||||
assert chat2b.count_fresh_messages() == 1
|
||||
|
||||
lp.sec("mark chat as noticed")
|
||||
@@ -991,16 +1035,114 @@ class TestOnlineAccount:
|
||||
except queue.Empty:
|
||||
pass # mark_seen_messages() has generated events before it returns
|
||||
|
||||
def test_moved_markseen(self, acfactory, lp):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
acfactory.wait_configure_and_start_io([ac1, ac2])
|
||||
|
||||
ac2.stop_io()
|
||||
ac2.direct_imap.idle_start()
|
||||
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
|
||||
# Wait for the message to arrive.
|
||||
ac2.direct_imap.idle_check(terminate=True)
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
# mailcow server contains this rule by default.
|
||||
ac2.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.idle_start()
|
||||
ac2.start_io()
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
# Accept the contact request.
|
||||
msg.chat.accept()
|
||||
ac2.mark_seen_messages([msg])
|
||||
ac2.direct_imap.idle_wait_for_seen()
|
||||
ac2.direct_imap.idle_done()
|
||||
|
||||
fetch = list(ac2.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
flags = fetch[-1][b'FLAGS']
|
||||
is_seen = b'\\Seen' in flags
|
||||
assert is_seen
|
||||
|
||||
def test_multidevice_sync_seen(self, acfactory, lp):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1_clone = acfactory.clone_online_account(ac1)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert ac1_chat.count_fresh_messages() == 1
|
||||
assert ac1_clone_chat.count_fresh_messages() == 1
|
||||
assert ac1_message.is_in_fresh
|
||||
assert ac1_clone_message.is_in_fresh
|
||||
|
||||
lp.sec("ac1 marks message as seen on the first device")
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
|
||||
lp.sec("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
|
||||
def test_message_override_sender_name(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
overridden_name = "someone else"
|
||||
|
||||
ac1.set_config("displayname", "ac1")
|
||||
|
||||
lp.sec("sending text message with overridden name from ac1 to ac2")
|
||||
msg1 = Message.new_empty(ac1, "text")
|
||||
msg1.set_override_sender_name(overridden_name)
|
||||
msg1.set_text("message1")
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.override_sender_name == overridden_name
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert msg2.get_sender_contact().name == ac1.get_config("displayname")
|
||||
assert msg2.override_sender_name == overridden_name
|
||||
|
||||
lp.sec("sending normal text message from ac1 to ac2")
|
||||
msg1 = Message.new_empty(ac1, "text")
|
||||
msg1.set_text("message2")
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert not msg1.override_sender_name
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message2"
|
||||
assert msg2.get_sender_contact().name == ac1.get_config("displayname")
|
||||
assert not msg2.override_sender_name
|
||||
|
||||
@pytest.mark.parametrize("mvbox_move", [True, False])
|
||||
def test_markseen_message_and_mdn(self, acfactory, mvbox_move):
|
||||
# Please only change this test if you are very sure that it will still catch the issues it catches now.
|
||||
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
folder = "mvbox" if mvbox_move else "inbox"
|
||||
ac1.direct_imap.select_config_folder(folder)
|
||||
@@ -1008,6 +1150,9 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.direct_imap.idle_start()
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
ac1.direct_imap.idle_wait_for_seen() # Check that the mdn is marked as seen
|
||||
@@ -1022,7 +1167,7 @@ class TestOnlineAccount:
|
||||
group1.add_contact(ac2)
|
||||
group1.send_text("hello")
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
group2 = msg2.create_chat()
|
||||
assert group2.get_name() == group1.get_name()
|
||||
|
||||
@@ -1039,7 +1184,7 @@ class TestOnlineAccount:
|
||||
assert not msg_reply1.chat.is_group()
|
||||
assert msg_reply1.chat.id == private_chat1.id
|
||||
|
||||
def test_mdn_asymetric(self, acfactory, lp):
|
||||
def test_mdn_asymmetric(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(move=True)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
@@ -1063,6 +1208,9 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
@@ -1072,6 +1220,9 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
assert ac1.direct_imap.idle_wait_for_seen()
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
@@ -1085,7 +1236,7 @@ class TestOnlineAccount:
|
||||
chat.send_text("message1")
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
|
||||
lp.sec("create new chat with contact and send back (encrypted) message")
|
||||
@@ -1118,6 +1269,40 @@ class TestOnlineAccount:
|
||||
assert not msg.is_encrypted()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
def test_gossip_optimization(self, acfactory, lp):
|
||||
"""Test that gossip timestamp is updated when someone else sends gossip,
|
||||
so we don't have to send gossip ourselves.
|
||||
"""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
acfactory.introduce_each_other([ac2, ac3])
|
||||
|
||||
lp.sec("ac1 creates a group chat with ac2")
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
# No Autocrypt gossip was sent yet.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == 0
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.text == "hi"
|
||||
|
||||
lp.sec("ac2 adds ac3 to the group")
|
||||
msg.chat.add_contact(ac3)
|
||||
|
||||
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
|
||||
# ac1 does not need to send gossip because ac2 already did it.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == int(msg.time_sent.timestamp())
|
||||
|
||||
def test_gossip_encryption_preference(self, acfactory, lp):
|
||||
"""Test that encryption preference of group members is gossiped to new members.
|
||||
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
|
||||
@@ -1215,12 +1400,14 @@ class TestOnlineAccount:
|
||||
assert not device_chat.can_send()
|
||||
assert device_chat.get_draft() is None
|
||||
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory):
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email."""
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
|
||||
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.com").create_chat()
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
@@ -1234,16 +1421,16 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.append("Drafts", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.com
|
||||
To: alice@example.org
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Sent", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.com
|
||||
To: alice@example.org
|
||||
Message-ID: <hsabaeni@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
@@ -1251,6 +1438,7 @@ class TestOnlineAccount:
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
msg = ac1._evtracker.wait_next_messages_changed()
|
||||
@@ -1261,6 +1449,47 @@ class TestOnlineAccount:
|
||||
assert msg.text == "subj – message in Sent"
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.stop_io()
|
||||
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
|
||||
ac1.direct_imap.select_folder("Drafts")
|
||||
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1.direct_imap.conn.move(uid, "Sent")
|
||||
|
||||
ac1.start_io()
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
def test_no_old_msg_is_fresh(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1_clone = acfactory.clone_online_account(ac1)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac1_clone.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "0")
|
||||
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone.create_chat(ac2)
|
||||
|
||||
lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg_id = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
assert ev.data1 == first_msg_id.chat.id
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
def test_prefer_encrypt(self, acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
@@ -1311,6 +1540,33 @@ class TestOnlineAccount:
|
||||
# Majority prefers encryption now
|
||||
assert msg5.is_encrypted()
|
||||
|
||||
def test_bot(self, acfactory, lp):
|
||||
"""Test that bot messages can be identified as such"""
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("bot", "0")
|
||||
ac2.set_config("bot", "1")
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("sending a message from ac1 to ac2")
|
||||
text1 = "hello"
|
||||
chat.send_text(text1)
|
||||
|
||||
lp.sec("wait for ac2 to receive a message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == text1
|
||||
assert not msg_in.is_bot()
|
||||
|
||||
lp.sec("sending a message from ac2 to ac1")
|
||||
text2 = "reply"
|
||||
msg_in.chat.send_text(text2)
|
||||
|
||||
lp.sec("wait for ac1 to receive a message")
|
||||
msg_in = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == text2
|
||||
assert msg_in.is_bot()
|
||||
|
||||
def test_quote_encrypted(self, acfactory, lp):
|
||||
"""Test that replies to encrypted messages with quotes are encrypted."""
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -1323,7 +1579,7 @@ class TestOnlineAccount:
|
||||
assert not msg1.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert not msg2.is_encrypted()
|
||||
|
||||
@@ -1372,7 +1628,7 @@ class TestOnlineAccount:
|
||||
chat1.send_text("hi")
|
||||
|
||||
lp.sec("ac2 receives contact request from ac1")
|
||||
received_message = ac2._evtracker.wait_next_messages_changed()
|
||||
received_message = ac2._evtracker.wait_next_incoming_message()
|
||||
assert received_message.text == "hi"
|
||||
|
||||
basename = "attachment.txt"
|
||||
@@ -1407,7 +1663,7 @@ class TestOnlineAccount:
|
||||
assert msg_out.get_mime_headers() is None
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
in_id = ev.data2
|
||||
mime = ac2.get_message_by_id(in_id).get_mime_headers()
|
||||
assert mime.get_all("From")
|
||||
@@ -1498,6 +1754,12 @@ class TestOnlineAccount:
|
||||
original_image_path = data.get_path("d.png")
|
||||
chat1.send_image(original_image_path)
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmpdir.join("attachment.txt")
|
||||
with open(path, "w") as file:
|
||||
file.truncate(100000)
|
||||
chat1.send_file(path.strpath)
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
@@ -1505,7 +1767,7 @@ class TestOnlineAccount:
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert len(messages) == 3
|
||||
assert messages[0].text == "msg1"
|
||||
assert messages[1].filemime == "image/png"
|
||||
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
|
||||
@@ -1633,7 +1895,7 @@ class TestOnlineAccount:
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
def test_qr_verified_group_and_chatting(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
@@ -1664,6 +1926,29 @@ class TestOnlineAccount:
|
||||
assert msg.text == "world"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac1: create QR code and let ac3 scan it, starting the securejoin")
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
|
||||
lp.sec("ac3: start QR-code based setup contact protocol")
|
||||
ch = ac3.qr_setup_contact(qr)
|
||||
assert ch.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac1: add ac3 to verified group")
|
||||
chat1.add_contact(ac3)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.is_system_message()
|
||||
assert not msg.error
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# Skip system message about added member
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_set_get_contact_avatar(self, acfactory, data, lp):
|
||||
lp.sec("configuring ac1 and ac2")
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -1677,8 +1962,8 @@ class TestOnlineAccount:
|
||||
ac1.create_chat(ac2).send_text("with avatar!")
|
||||
|
||||
lp.sec("ac2: wait for receiving message and avatar from ac1")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
assert msg2.chat.is_deaddrop()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.chat.is_contact_request()
|
||||
received_path = msg2.get_sender_contact().get_profile_image()
|
||||
assert open(received_path, "rb").read() == open(p, "rb").read()
|
||||
|
||||
@@ -1753,6 +2038,8 @@ class TestOnlineAccount:
|
||||
ev = in_list.get(timeout=10)
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
assert ev.action == "added"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
assert ev.contact.addr == "devnull@testrun.org"
|
||||
@@ -1867,8 +2154,125 @@ class TestOnlineAccount:
|
||||
assert msg_back.chat == chat
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_connectivity(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec("Test stop_io() and start_io()")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " +
|
||||
"all messages are fetched")
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
|
||||
ac1.direct_imap.idle_check(terminate=False)
|
||||
ac1.maybe_network()
|
||||
|
||||
ac1._evtracker.wait_for_all_work_done()
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
ac2.create_chat(ac1).send_text("Hi 2")
|
||||
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
|
||||
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
|
||||
|
||||
ac1.maybe_network()
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
|
||||
ac1.create_contact(ac2).block()
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.maybe_network()
|
||||
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
|
||||
|
||||
ac1.set_config("configured_mail_pw", "abc")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
def test_fetch_deleted_msg(self, acfactory, lp):
|
||||
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
|
||||
hundreds of times, because uid_next was not updated.
|
||||
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2429.
|
||||
"""
|
||||
ac1 = acfactory.get_one_online_account()
|
||||
ac1.stop_io()
|
||||
|
||||
ac1.direct_imap.append("INBOX", """
|
||||
From: alice <alice@example.org>
|
||||
Subject: subj
|
||||
To: bob@example.com
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Deleted message
|
||||
""")
|
||||
ac1.direct_imap.delete("1:*", expunge=False)
|
||||
ac1.start_io()
|
||||
|
||||
for ev in ac1._evtracker.iter_events():
|
||||
if ev.name == "DC_EVENT_MSGS_CHANGED":
|
||||
pytest.fail("A deleted message was shown to the user")
|
||||
|
||||
if ev.name == "DC_EVENT_INFO" and "1 mails read from" in ev.data2:
|
||||
break
|
||||
|
||||
# The message was downloaded once, now check that it's not downloaded again
|
||||
|
||||
for ev in ac1._evtracker.iter_events():
|
||||
if ev.name == "DC_EVENT_INFO" and "1 mails read from" in ev.data2:
|
||||
pytest.fail("The same email was read twice")
|
||||
|
||||
if ev.name == "DC_EVENT_MSGS_CHANGED":
|
||||
pytest.fail("A deleted message was shown to the user")
|
||||
|
||||
if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2:
|
||||
break # DC is done with reading messages
|
||||
|
||||
def test_send_receive_locations(self, acfactory, lp):
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
@@ -1991,7 +2395,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
ac2 = acfactory.get_online_configuring_account(move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
@@ -2255,7 +2659,7 @@ class TestOnlineAccount:
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,", [
|
||||
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX") # ...emails are moved from the spam folder to the Inbox
|
||||
("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox
|
||||
])
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
@@ -2287,8 +2691,7 @@ class TestOnlineAccount:
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
# Wait until the message was moved (if at all) and we are IDLEing again:
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
@@ -2311,7 +2714,7 @@ class TestOnlineAccount:
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac1.set_config("sentbox_move", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
|
||||
@@ -2378,7 +2781,7 @@ class TestOnlineAccount:
|
||||
|
||||
lp.sec("receive a message")
|
||||
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
|
||||
ac1._evtracker.wait_next_messages_changed()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.direct_imap.idle_start()
|
||||
@@ -2409,6 +2812,22 @@ class TestOnlineAccount:
|
||||
# We can't decrypt the message in this chat, so the chat is empty:
|
||||
assert len(private_messages) == 0
|
||||
|
||||
def test_delete_deltachat_folder(self, acfactory):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure(ac1)
|
||||
|
||||
ac1.direct_imap.conn.delete_folder("DeltaChat")
|
||||
assert len(ac1.direct_imap.conn.list_folders(pattern="DeltaChat")) == 0
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac2.create_chat(ac1).send_text("hello")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert len(ac1.direct_imap.conn.list_folders(pattern="DeltaChat")) == 1
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
@@ -2523,7 +2942,8 @@ class TestGroupStressTests:
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert msg.chat.num_contacts() == chat.num_contacts()
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
@@ -2534,7 +2954,6 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
|
||||
def test_invalid_user(self, acfactory):
|
||||
ac1, configdict = acfactory.get_online_config()
|
||||
@@ -2542,7 +2961,6 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
|
||||
def test_invalid_domain(self, acfactory):
|
||||
ac1, configdict = acfactory.get_online_config()
|
||||
@@ -2550,4 +2968,3 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
|
||||
@@ -6,8 +6,6 @@ import shutil
|
||||
import pytest
|
||||
from filecmp import cmp
|
||||
|
||||
from deltachat import const
|
||||
|
||||
|
||||
def wait_msg_delivered(account, msg_list):
|
||||
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """
|
||||
@@ -102,14 +100,10 @@ class TestOnlineInCreation:
|
||||
])
|
||||
|
||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
received_original = ac2.get_message_by_id(ev1.data2)
|
||||
received_original = ac2._evtracker.wait_next_incoming_message()
|
||||
assert cmp(received_original.filename, orig, shallow=False)
|
||||
|
||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev2.data1 != ev1.data1
|
||||
received_copy = ac2.get_message_by_id(ev2.data2)
|
||||
received_copy = ac2._evtracker.wait_next_incoming_message()
|
||||
assert received_copy.id != received_original.id
|
||||
assert cmp(received_copy.filename, orig, shallow=False)
|
||||
|
||||
@@ -36,7 +36,8 @@ def test_wrong_db(tmpdir):
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
|
||||
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
assert not lib.dc_context_is_open(context)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[tox]
|
||||
# make sure to update environment list in travis.yml and appveyor.yml
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py37
|
||||
py3
|
||||
lint
|
||||
mypy
|
||||
auditwheels
|
||||
|
||||
[testenv]
|
||||
@@ -43,12 +44,19 @@ commands =
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
mypy
|
||||
typing
|
||||
types-setuptools
|
||||
types-requests
|
||||
commands =
|
||||
mypy --no-incremental src/
|
||||
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# With Python 3.7 and Sphinx 3.5.0, it throws an exception.
|
||||
# Pin the version to the working one.
|
||||
sphinx==3.4.3
|
||||
sphinx
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.50.0
|
||||
1.54.0
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
# Continuous Integration Scripts for Delta Chat
|
||||
|
||||
Continuous Integration, run through [GitHub
|
||||
Actions](https://docs.github.com/actions),
|
||||
[CircleCI](https://app.circleci.com/) and an own build machine.
|
||||
Actions](https://docs.github.com/actions)
|
||||
and an own build machine.
|
||||
|
||||
## Description of scripts
|
||||
|
||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
||||
|
||||
- `../.circleci/config.yml` describing the build jobs that are run
|
||||
by CircleCI.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
`run-python-test.sh` remotely on the build machine.
|
||||
|
||||
@@ -30,8 +27,8 @@ There is experimental support for triggering a remote Python or Rust test run
|
||||
from your local checkout/branch. You will need to be authorized to login to
|
||||
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
|
||||
|
||||
ci_scripts/manual_remote_tests.sh rust
|
||||
ci_scripts/manual_remote_tests.sh python
|
||||
scripts/manual_remote_tests.sh rust
|
||||
scripts/manual_remote_tests.sh python
|
||||
|
||||
This will **rsync** your current checkout to the remote build machine
|
||||
(no need to commit before) and then run either rust or python tests.
|
||||
@@ -45,6 +42,10 @@ python tests and build wheels (binary packages for Python)
|
||||
You can build the docker images yourself locally
|
||||
to avoid the relatively large download::
|
||||
|
||||
cd ci_scripts # where all CI things are
|
||||
cd scripts # where all CI things are
|
||||
docker build -t deltachat/coredeps docker-coredeps
|
||||
docker build -t deltachat/doxygen docker-doxygen
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
|
||||
21
scripts/concourse/README.md
Normal file
21
scripts/concourse/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Concourse CI pipeline
|
||||
|
||||
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
|
||||
that builds C documentation, Python documentation, Python wheels for `x86_64`
|
||||
and `aarch64` and Python source packages, and uploads them.
|
||||
|
||||
To setup the pipeline run
|
||||
```
|
||||
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
|
||||
```
|
||||
where `secret.yml` contains the following secrets:
|
||||
```
|
||||
c.delta.chat:
|
||||
private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----
|
||||
devpi:
|
||||
login: dc
|
||||
password: ...
|
||||
```
|
||||
232
scripts/concourse/docs_wheels.yml
Normal file
232
scripts/concourse/docs_wheels.yml
Normal file
@@ -0,0 +1,232 @@
|
||||
resources:
|
||||
- name: deltachat-core-rust
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: master
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: master
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
tag_filter: "py-*"
|
||||
|
||||
jobs:
|
||||
- name: doxygen
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
trigger: true
|
||||
|
||||
# Build Doxygen documentation
|
||||
- task: build-doxygen
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust
|
||||
outputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: hrektts/doxygen
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
cd deltachat-core-rust
|
||||
bash scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
|
||||
|
||||
- name: python-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: vito/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-docs
|
||||
path: ./python/doc/_build/
|
||||
# Source packages
|
||||
- name: py-dist
|
||||
path: ./python/.docker-tox/dist/
|
||||
# Binary wheels
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload python docs to py.delta.chat
|
||||
- task: upload-py-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: py-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
|
||||
|
||||
# Upload x86_64 wheels and source packages
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
- name: py-dist
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
devpi upload py-dist/*
|
||||
|
||||
- name: python-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: vito/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
|
||||
UNPACK_ROOTFS: "true"
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
26
scripts/coverage.sh
Executable file
26
scripts/coverage.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if ! command -v grcov >/dev/null; then
|
||||
echo >&2 '`grcov` not found. Check README at https://github.com/mozilla/grcov for setup instructions.'
|
||||
echo >&2 'Run `cargo install grcov` to build `grcov` from source.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Allow `-Z` flags without using nightly Rust.
|
||||
export RUSTC_BOOTSTRAP=1
|
||||
|
||||
# We are using `-Zprofile` instead of source-based coverage [1]
|
||||
# (`-Zinstrument-coverage`) due to a bug resulting in empty reports [2].
|
||||
#
|
||||
# [1] https://blog.rust-lang.org/inside-rust/2020/11/12/source-based-code-coverage.html
|
||||
# [2] https://github.com/mozilla/grcov/issues/595
|
||||
|
||||
export CARGO_INCREMENTAL=0
|
||||
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
export RUSTDOCFLAGS="-Cpanic=abort"
|
||||
cargo clean
|
||||
cargo build
|
||||
cargo test
|
||||
|
||||
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./coverage/
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM quay.io/pypa/manylinux2010_x86_64
|
||||
FROM quay.io/pypa/manylinux2014_aarch64
|
||||
|
||||
# Configure ld.so/ldconfig and pkg-config
|
||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||
RUN rm /usr/bin/perl
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.30.0
|
||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||
PERL_VERSION=5.34.0
|
||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
14
scripts/docker-coredeps-arm64/deps/build_python.sh
Executable file
14
scripts/docker-coredeps-arm64/deps/build_python.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
18
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
18
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
21
scripts/docker-coredeps/Dockerfile
Normal file
21
scripts/docker-coredeps/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM quay.io/pypa/manylinux2014_x86_64
|
||||
|
||||
# Configure ld.so/ldconfig and pkg-config
|
||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||
RUN rm /usr/bin/perl
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||
|
||||
# Install python tools (auditwheels,tox, ...)
|
||||
ADD deps/build_python.sh /builder/build_python.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||
|
||||
# Install Rust
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
21
scripts/docker-coredeps/deps/build_openssl.sh
Executable file
21
scripts/docker-coredeps/deps/build_openssl.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
OPENSSL_VERSION=1.1.1a
|
||||
OPENSSL_SHA256=fc20130f8b7cbd2fb918b2f14e2f429e109c31ddd0fb38fc5d71d9ffed3f9f41
|
||||
|
||||
curl -O https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
|
||||
echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar xzf openssl-${OPENSSL_VERSION}.tar.gz
|
||||
cd openssl-${OPENSSL_VERSION}
|
||||
./config shared no-ssl2 no-ssl3 -fPIC --prefix=/usr/local
|
||||
|
||||
sed -i "s/^SHLIB_MAJOR=.*/SHLIB_MAJOR=200/" Makefile && \
|
||||
sed -i "s/^SHLIB_MINOR=.*/SHLIB_MINOR=0.0/" Makefile && \
|
||||
sed -i "s/^SHLIB_VERSION_NUMBER=.*/SHLIB_VERSION_NUMBER=200.0.0/" Makefile
|
||||
|
||||
make depend
|
||||
make
|
||||
make install_sw install_ssldirs
|
||||
ldconfig -v | grep ssl
|
||||
12
scripts/docker-coredeps/deps/build_perl.sh
Executable file
12
scripts/docker-coredeps/deps/build_perl.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.34.0
|
||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
cd perl-${PERL_VERSION}
|
||||
|
||||
./Configure -de
|
||||
make
|
||||
make install
|
||||
14
scripts/docker-coredeps/deps/build_python.sh
Executable file
14
scripts/docker-coredeps/deps/build_python.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
18
scripts/docker-coredeps/deps/build_rust.sh
Executable file
18
scripts/docker-coredeps/deps/build_rust.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
8
scripts/manual_remote_tests.sh
Executable file
8
scripts/manual_remote_tests.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -xe
|
||||
|
||||
JOB=${1:?need to specify 'rust' or 'python'}
|
||||
BRANCH="$(git branch | grep \* | cut -d ' ' -f2)"
|
||||
REPONAME="$(basename $(git rev-parse --show-toplevel))"
|
||||
|
||||
time bash "scripts/remote_tests_$JOB.sh" "$USER-$BRANCH-$REPONAME"
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILDDIR=ci_builds/$BUILD_ID
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
@@ -18,7 +17,7 @@ rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
set +x
|
||||
|
||||
echo "--- Running $CIRCLE_JOB remotely"
|
||||
echo "--- Running Python tests remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
@@ -42,5 +41,5 @@ ssh $SSHTARGET <<_HERE
|
||||
source \$HOME/venv/bin/activate
|
||||
which python
|
||||
|
||||
bash ci_scripts/run-python-test.sh
|
||||
bash scripts/run-python-test.sh
|
||||
_HERE
|
||||
29
scripts/remote_tests_rust.sh
Executable file
29
scripts/remote_tests_rust.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILDDIR=ci_builds/$BUILD_ID
|
||||
|
||||
set -e
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
echo "--- Running Rust tests remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=x86_64-unknown-linux-gnu
|
||||
export RUSTC_WRAPPER=sccache
|
||||
|
||||
bash scripts/run-rust-test.sh
|
||||
_HERE
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
# purposes. Any arguments are passed straight to tox. E.g. to run
|
||||
# only one environment run with:
|
||||
#
|
||||
# ./run-integration-tests.sh -e py35
|
||||
# scripts/run-integration-tests.sh -e py35
|
||||
#
|
||||
# To also run with `pytest -x` use:
|
||||
#
|
||||
# ./run-integration-tests.sh -e py35 -- -x
|
||||
# scripts/run-integration-tests.sh -e py35 -- -x
|
||||
|
||||
export DCC_RS_DEV=$(pwd)
|
||||
export DCC_RS_TARGET=${DCC_RS_TARGET:-release}
|
||||
@@ -19,14 +19,15 @@ export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.5
|
||||
ln -s /opt/python/cp35-cp35m/bin/python3.5
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
rm -f python3.7
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
rm -f python3.8
|
||||
ln -s /opt/python/cp38-cp38/bin/python3.8
|
||||
rm -f python3.9
|
||||
ln -s /opt/python/cp39-cp39/bin/python3.9
|
||||
popd
|
||||
|
||||
pushd python
|
||||
@@ -40,7 +41,7 @@ mkdir -p $TOXWORKDIR
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
@@ -82,9 +82,12 @@ def main():
|
||||
subprocess.call(["git", "add", "-u"])
|
||||
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||
|
||||
print("after commit make sure to: ")
|
||||
print("after commit, on master make sure to: ")
|
||||
print("")
|
||||
print(" git tag {}".format(newversion))
|
||||
print(" git tag -a {}".format(newversion))
|
||||
print(" git push origin {}".format(newversion))
|
||||
print(" git tag -a py-{}".format(newversion))
|
||||
print(" git push origin py-{}".format(newversion))
|
||||
print("")
|
||||
|
||||
|
||||
63
spec.md
63
spec.md
@@ -1,6 +1,6 @@
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.32.0
|
||||
Version: 0.33.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -301,9 +301,9 @@ to add a `Chat-Group-Avatar` only on image changes.
|
||||
|
||||
A user MAY have a profile-image that MAY be distributed to their contacts.
|
||||
To change or set the profile-image,
|
||||
the messenger MUST attach an image file to a message
|
||||
and MUST add the header `Chat-User-Avatar`
|
||||
with the value set to the image name.
|
||||
the messenger MUST add the header `Chat-User-Avatar: base64:IMAGEDATA`.
|
||||
To bypass limits of headers, it is recommended not to use the outer header
|
||||
and to limit the size to 20k.
|
||||
|
||||
To remove the profile-image,
|
||||
the messenger MUST add the header `Chat-User-Avatar: 0`.
|
||||
@@ -320,19 +320,14 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Chat-User-Avatar: photo.jpg
|
||||
Subject: Chat: Hello, ...
|
||||
Content-Type: multipart/mixed; boundary="==break=="
|
||||
|
||||
--==break==
|
||||
Content-Type: text/plain
|
||||
Chat-User-Avatar: base64:AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQY ...
|
||||
|
||||
Hello, I've changed my profile image.
|
||||
--==break==
|
||||
Content-Type: image/jpeg
|
||||
Content-Disposition: attachment; filename="photo.jpg"
|
||||
|
||||
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
|
||||
--==break==--
|
||||
|
||||
The image format SHOULD be image/jpeg or image/png.
|
||||
@@ -342,6 +337,11 @@ in the same message.
|
||||
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
|
||||
only on image changes.
|
||||
|
||||
In older specs, the profile-image was sent as an attachment
|
||||
and `Chat-User-Avatar:` specified its name.
|
||||
However, it turned out that these attachements are kind of unuexpected to users,
|
||||
therefore the profile-image go to the header now.
|
||||
|
||||
|
||||
# Locations
|
||||
|
||||
@@ -401,9 +401,41 @@ it is fine if the location is detected on forwarding etc.
|
||||
</kml>
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
# Stickers
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
Stickers are send as normal images
|
||||
with the additional header `Chat-Content: sticker`.
|
||||
|
||||
It is discouraged to send stickers together with user generated text,
|
||||
however, stickers can be used as a reply to a message
|
||||
and also the footer should be set as usual.
|
||||
|
||||
From: alice@example.org
|
||||
To: bob@example.com
|
||||
Chat-Version: 1.0
|
||||
Chat-Content: sticker
|
||||
Message-ID: Mr.12345uvwxyZ.0005@example.org
|
||||
Subject: Message from Alice
|
||||
Content-Type: multipart/mixed; boundary="==break=="
|
||||
|
||||
--==break==
|
||||
Content-Type: text/plain
|
||||
|
||||
--
|
||||
Hi there! I am using this new messenger!
|
||||
--==break==
|
||||
Content-Type: image/png
|
||||
Content-Disposition: attachment; filename="sticker.png"
|
||||
|
||||
R0lGODlhpAGkAfe9AP+zd2eQkZhrI//z9v++PMb///+scrdDT3BtbtrZ2f/LQSsREcdIVf9 ...
|
||||
--==break==--
|
||||
|
||||
Typical sticker formats are `image/png`, `image/gif` and `image/webp`.
|
||||
Animated stickers are supported
|
||||
by just using an image format that supports animation.
|
||||
|
||||
|
||||
# Voice messages
|
||||
|
||||
Messengers SHOULD add a `Chat-Voice-message: 1` header
|
||||
if an attached audio file is a voice message.
|
||||
@@ -417,6 +449,11 @@ This allows the receiver to show the time without knowing the file format.
|
||||
Chat-Voice-Message: 1
|
||||
Chat-Duration: 10000
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
Messengers MAY send and receive Message Disposition Notifications
|
||||
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
|
||||
[RFC 3503](https://tools.ietf.org/html/rfc3503))
|
||||
@@ -437,4 +474,4 @@ as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
Copyright © 2017-2020 Delta Chat contributors.
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
576
src/accounts.rs
576
src/accounts.rs
@@ -1,5 +1,8 @@
|
||||
//! # Account manager module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use async_std::channel::{self, Receiver, Sender};
|
||||
use async_std::fs;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
@@ -10,39 +13,37 @@ use anyhow::{ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::events::{Event, EventType, Events};
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
|
||||
accounts: BTreeMap<u32, Context>,
|
||||
emitter: EventEmitter,
|
||||
|
||||
/// Event channel to emit account manager errors.
|
||||
events: Events,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(os_name: String, dir: PathBuf) -> Result<Self> {
|
||||
pub async fn new(dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists().await {
|
||||
Accounts::create(os_name, &dir).await?;
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure, including a default account.
|
||||
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
// create default account
|
||||
let config = Config::new(os_name.clone(), dir).await?;
|
||||
let account_config = config.new_account(dir).await?;
|
||||
|
||||
Context::new(os_name, account_config.dbfile().into(), account_config.id)
|
||||
.await
|
||||
.context("failed to create default account")?;
|
||||
Config::new(dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -55,53 +56,88 @@ impl Accounts {
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists().await, "accounts.toml does not exist");
|
||||
|
||||
let config = Config::from_file(config_file).await?;
|
||||
let accounts = config.load_accounts().await?;
|
||||
let config = Config::from_file(config_file)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let accounts = config
|
||||
.load_accounts()
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
let emitter = EventEmitter::new();
|
||||
|
||||
let events = Events::default();
|
||||
|
||||
emitter.sender.send(events.get_emitter()).await?;
|
||||
|
||||
for account in accounts.values() {
|
||||
emitter.add_account(account).await.with_context(|| {
|
||||
format!("failed to add account {} to event emitter", account.id)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
dir,
|
||||
config,
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
accounts,
|
||||
emitter,
|
||||
events,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.read().await.get(&id).cloned()
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub async fn get_selected_account(&self) -> Context {
|
||||
pub async fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account().await;
|
||||
self.accounts
|
||||
.read()
|
||||
.await
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.expect("inconsistent state")
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the currently selected account's id or None if no account is selected.
|
||||
pub async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account().await {
|
||||
0 => None,
|
||||
id => Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the given account.
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
self.config.select_account(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account.
|
||||
pub async fn add_account(&self) -> Result<u32> {
|
||||
let os_name = self.config.os_name().await;
|
||||
/// Add a new account and opens it.
|
||||
///
|
||||
/// Returns account ID.
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Adds a new closed account.
|
||||
pub async fn add_closed_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new_closed(account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.write().await.remove(&id);
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.remove(&id);
|
||||
ensure!(ctx.is_some(), "no account with this id: {}", id);
|
||||
let ctx = ctx.unwrap();
|
||||
ctx.stop_io().await;
|
||||
@@ -118,8 +154,9 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Migrate an existing account into this structure.
|
||||
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
|
||||
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
|
||||
let blobdir = Context::derive_blobdir(&dbfile);
|
||||
let walfile = Context::derive_walfile(&dbfile);
|
||||
|
||||
ensure!(
|
||||
dbfile.exists().await,
|
||||
@@ -143,6 +180,7 @@ impl Accounts {
|
||||
|
||||
let new_dbfile = account_config.dbfile().into();
|
||||
let new_blobdir = Context::derive_blobdir(&new_dbfile);
|
||||
let new_walfile = Context::derive_walfile(&new_dbfile);
|
||||
|
||||
let res = {
|
||||
fs::create_dir_all(&account_config.dir)
|
||||
@@ -154,19 +192,19 @@ impl Accounts {
|
||||
fs::rename(&blobdir, &new_blobdir)
|
||||
.await
|
||||
.context("failed to rename blobdir")?;
|
||||
if walfile.exists().await {
|
||||
fs::rename(&walfile, &new_walfile)
|
||||
.await
|
||||
.context("failed to rename walfile")?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(
|
||||
self.config.os_name().await,
|
||||
new_dbfile,
|
||||
new_blobdir,
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
let ctx = Context::new(new_dbfile, account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -187,75 +225,114 @@ impl Accounts {
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.read().await.keys().copied().collect()
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Import a backup using a new account and selects it.
|
||||
pub async fn import_account(&self, file: PathBuf) -> Result<u32> {
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
let id = self.add_account().await?;
|
||||
let ctx = self.get_account(id).await.expect("just added");
|
||||
|
||||
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
|
||||
Ok(_) => Ok(id),
|
||||
Err(err) => {
|
||||
// remove temp account
|
||||
self.remove_account(id).await?;
|
||||
// set selection back
|
||||
self.select_account(old_id).await?;
|
||||
Err(err)
|
||||
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
///
|
||||
/// Returns whether all accounts finished their background work.
|
||||
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
///
|
||||
/// iOS can:
|
||||
/// - call dc_start_io() (in case IO was not running)
|
||||
/// - call dc_maybe_network()
|
||||
/// - while dc_accounts_all_work_done() returns false:
|
||||
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
for account in self.accounts.values() {
|
||||
if !account.all_work_done().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn start_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.stop_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_network(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
let emitters: Vec<_> = self
|
||||
.accounts
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(_id, a)| a.get_event_emitter())
|
||||
.collect();
|
||||
pub async fn maybe_network_lost(&self) {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network_lost().await;
|
||||
}
|
||||
}
|
||||
|
||||
EventEmitter(futures::stream::select_all(emitters))
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
self.events.emit(Event { id: 0, typ: event })
|
||||
}
|
||||
|
||||
/// Returns unified event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.emitter.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
|
||||
/// Unified event emitter for multiple accounts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventEmitter {
|
||||
/// Aggregate stream of events from all accounts.
|
||||
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
|
||||
|
||||
/// Sender for the channel where new account emitters will be pushed.
|
||||
sender: Sender<crate::events::EventEmitter>,
|
||||
|
||||
/// Receiver for the channel where new account emitters will be pushed.
|
||||
receiver: Receiver<crate::events::EventEmitter>,
|
||||
}
|
||||
|
||||
impl EventEmitter {
|
||||
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
|
||||
pub fn recv_sync(&mut self) -> Option<Event> {
|
||||
async_std::task::block_on(self.recv())
|
||||
pub fn new() -> Self {
|
||||
let (sender, receiver) = channel::unbounded();
|
||||
Self {
|
||||
stream: Arc::new(RwLock::new(futures::stream::SelectAll::new())),
|
||||
sender,
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
|
||||
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
|
||||
pub async fn recv(&mut self) -> Option<Event> {
|
||||
self.0.next().await
|
||||
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
|
||||
pub fn recv_sync(&mut self) -> Option<Event> {
|
||||
async_std::task::block_on(self.recv()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Async recv of an event. Return `None` if all `Sender`s have been dropped.
|
||||
pub async fn recv(&mut self) -> Result<Option<Event>> {
|
||||
let mut stream = self.stream.write().await;
|
||||
loop {
|
||||
match futures::future::select(self.receiver.recv(), stream.next()).await {
|
||||
futures::future::Either::Left((emitter, _)) => {
|
||||
stream.push(emitter?);
|
||||
}
|
||||
futures::future::Either::Right((ev, _)) => return Ok(ev),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add event emitter of a new account to the aggregate event emitter.
|
||||
pub async fn add_account(&self, context: &Context) -> Result<()> {
|
||||
self.sender.send(context.get_event_emitter()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventEmitter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,22 +343,25 @@ impl async_std::stream::Stream for EventEmitter {
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
std::pin::Pin::new(&mut self.0).poll_next(cx)
|
||||
std::pin::Pin::new(&mut self).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
pub const DB_NAME: &str = "dc.db";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
file: PathBuf,
|
||||
inner: Arc<RwLock<InnerConfig>>,
|
||||
inner: InnerConfig,
|
||||
}
|
||||
|
||||
/// Account manager configuration file contents.
|
||||
///
|
||||
/// This is serialized into TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct InnerConfig {
|
||||
pub os_name: String,
|
||||
/// The currently selected account.
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
@@ -289,15 +369,15 @@ struct InnerConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
|
||||
pub async fn new(dir: &PathBuf) -> Result<Self> {
|
||||
let inner = InnerConfig {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
let cfg = Config {
|
||||
file: dir.join(CONFIG_NAME),
|
||||
inner: Arc::new(RwLock::new(InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
})),
|
||||
inner,
|
||||
};
|
||||
|
||||
cfg.sync().await?;
|
||||
@@ -305,18 +385,11 @@ impl Config {
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub async fn os_name(&self) -> String {
|
||||
self.inner.read().await.os_name.clone()
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(
|
||||
&self.file,
|
||||
toml::to_string_pretty(&*self.inner.read().await)?,
|
||||
)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
@@ -324,22 +397,21 @@ impl Config {
|
||||
let bytes = fs::read(&file).await.context("failed to read file")?;
|
||||
let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?;
|
||||
|
||||
Ok(Config {
|
||||
file,
|
||||
inner: Arc::new(RwLock::new(inner)),
|
||||
})
|
||||
Ok(Config { file, inner })
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let cfg = &*self.inner.read().await;
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &cfg.accounts {
|
||||
let ctx = Context::new(
|
||||
cfg.os_name.clone(),
|
||||
account_config.dbfile().into(),
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile()
|
||||
)
|
||||
})?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
@@ -347,70 +419,67 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Create a new account in the given root directory.
|
||||
pub async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
async fn new_account(&mut self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
let id = {
|
||||
let inner = &mut self.inner.write().await;
|
||||
let id = inner.next_id;
|
||||
let id = self.inner.next_id;
|
||||
let uuid = Uuid::new_v4();
|
||||
let target_dir = dir.join(uuid.to_simple_ref().to_string());
|
||||
|
||||
inner.accounts.push(AccountConfig {
|
||||
self.inner.accounts.push(AccountConfig {
|
||||
id,
|
||||
dir: target_dir.into(),
|
||||
uuid,
|
||||
});
|
||||
inner.next_id += 1;
|
||||
self.inner.next_id += 1;
|
||||
id
|
||||
};
|
||||
|
||||
self.sync().await?;
|
||||
|
||||
self.select_account(id).await.expect("just added");
|
||||
let cfg = self.get_account(id).await.expect("just added");
|
||||
self.select_account(id)
|
||||
.await
|
||||
.context("failed to select just added account")?;
|
||||
let cfg = self
|
||||
.get_account(id)
|
||||
.await
|
||||
.context("failed to get just added account")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
/// Removes an existing acccount entirely.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
if let Some(idx) = inner.accounts.iter().position(|e| e.id == id) {
|
||||
if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
|
||||
// remove account from the configs
|
||||
inner.accounts.remove(idx);
|
||||
self.inner.accounts.remove(idx);
|
||||
}
|
||||
if inner.selected_account == id {
|
||||
if self.inner.selected_account == id {
|
||||
// reset selected account
|
||||
inner.selected_account = inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
self.inner.selected_account =
|
||||
self.inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
self.sync().await
|
||||
}
|
||||
|
||||
pub async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.cloned()
|
||||
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner.accounts.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
self.inner.read().await.selected_account
|
||||
self.inner.selected_account
|
||||
}
|
||||
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
ensure!(
|
||||
inner.accounts.iter().any(|e| e.id == id),
|
||||
self.inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {}",
|
||||
id
|
||||
);
|
||||
|
||||
inner.selected_account = id;
|
||||
self.inner.selected_account = id;
|
||||
}
|
||||
|
||||
self.sync().await?;
|
||||
@@ -418,8 +487,9 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration of a single account.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct AccountConfig {
|
||||
struct AccountConfig {
|
||||
/// Unique id.
|
||||
pub id: u32,
|
||||
/// Root directory for all data for this account.
|
||||
@@ -443,21 +513,17 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1").into();
|
||||
|
||||
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(
|
||||
&*accounts1.config.inner.read().await,
|
||||
&*accounts2.config.inner.read().await,
|
||||
);
|
||||
assert_eq!(
|
||||
accounts1.accounts.read().await.len(),
|
||||
accounts2.accounts.read().await.len()
|
||||
);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -465,22 +531,47 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
assert_eq!(accounts.accounts.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_accounts_remove_last() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let id = accounts.add_account().await?;
|
||||
assert!(accounts.get_selected_account().await.is_some());
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
|
||||
accounts.remove_account(id).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -488,14 +579,12 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other").into();
|
||||
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
|
||||
.await
|
||||
.unwrap();
|
||||
let ctx = Context::new(extern_dbfile.clone(), 0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -506,10 +595,10 @@ mod tests {
|
||||
.migrate_account(extern_dbfile.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let ctx = accounts.get_selected_account().await;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
@@ -525,9 +614,9 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..10 {
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
@@ -537,4 +626,157 @@ mod tests {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
|
||||
.await?;
|
||||
|
||||
let id1 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id1).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
|
||||
.await?;
|
||||
|
||||
// add and remove some accounts and force a gap (ids must not be reused)
|
||||
for _ in 0..dummy_accounts {
|
||||
let to_delete = accounts.add_account().await?;
|
||||
accounts.remove_account(to_delete).await?;
|
||||
}
|
||||
|
||||
let id2 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
|
||||
.await?;
|
||||
|
||||
accounts.select_account(id1).await?;
|
||||
|
||||
(id0, id1, id2)
|
||||
};
|
||||
assert!(id0 > 0);
|
||||
assert!(id1 > id0);
|
||||
assert!(id2 > id1 + dummy_accounts);
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 3);
|
||||
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("one@example.org".to_string())
|
||||
);
|
||||
|
||||
let id1 = *ids.get(1).unwrap();
|
||||
let t = accounts.get_account(id1).await.unwrap();
|
||||
assert_eq!(
|
||||
t.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let id2 = *ids.get(2).unwrap();
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("three@example.org".to_string())
|
||||
);
|
||||
|
||||
(id0, id1, id2)
|
||||
};
|
||||
assert_eq!(id0, id0_reopened);
|
||||
assert_eq!(id1, id1_reopened);
|
||||
assert_eq!(id2, id2_reopened);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_no_accounts_event_emitter() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
// Create event emitter.
|
||||
let mut event_emitter = accounts.get_event_emitter().await;
|
||||
|
||||
// Test that event emitter does not return `None` immediately.
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
assert!(async_std::future::timeout(duration, event_emitter.recv())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// When account manager is dropped, event emitter is exhausted.
|
||||
drop(accounts);
|
||||
assert_eq!(event_emitter.recv().await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_encrypted_account() -> Result<()> {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let account_id = accounts
|
||||
.add_closed_account()
|
||||
.await
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
.open("foobar".to_string())
|
||||
.await
|
||||
.context("failed to set passphrase")?;
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.is_open().await, false);
|
||||
|
||||
// Try wrong passphrase.
|
||||
assert_eq!(account.open("barfoo".to_string()).await?, false);
|
||||
assert_eq!(account.open("".to_string()).await?, false);
|
||||
|
||||
assert_eq!(account.open("foobar".to_string()).await?, true);
|
||||
assert_eq!(account.is_open().await, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! # Autocrypt header module
|
||||
//! # Autocrypt header module.
|
||||
//!
|
||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
@@ -37,13 +37,13 @@ impl fmt::Display for EncryptPreference {
|
||||
}
|
||||
|
||||
impl str::FromStr for EncryptPreference {
|
||||
type Err = ();
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
_ => Err(()),
|
||||
_ => bail!("Cannot parse encryption preference {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,28 +70,27 @@ impl Aheader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to parse Autocrypt header.
|
||||
///
|
||||
/// If there is none, returns None. If the header is present but cannot be parsed, returns an
|
||||
/// error.
|
||||
pub fn from_headers(
|
||||
context: &Context,
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Option<Self> {
|
||||
) -> Result<Option<Self>> {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Self::from_str(&value) {
|
||||
Ok(header) => {
|
||||
if addr_cmp(&header.addr, wanted_from) {
|
||||
return Some(header);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"found invalid autocrypt header {}: {:?}", value, err
|
||||
);
|
||||
}
|
||||
let header = Self::from_str(&value)?;
|
||||
if !addr_cmp(&header.addr, wanted_from) {
|
||||
bail!(
|
||||
"Autocrypt header address {:?} is not {:?}",
|
||||
header.addr,
|
||||
wanted_from
|
||||
);
|
||||
}
|
||||
Ok(Some(header))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +119,9 @@ impl fmt::Display for Aheader {
|
||||
}
|
||||
|
||||
impl str::FromStr for Aheader {
|
||||
type Err = ();
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut attributes: BTreeMap<String, String> = s
|
||||
.split(';')
|
||||
.filter_map(|a| {
|
||||
@@ -136,15 +135,19 @@ impl str::FromStr for Aheader {
|
||||
|
||||
let addr = match attributes.remove("addr") {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
return Err(());
|
||||
}
|
||||
None => bail!("Autocrypt header has no addr"),
|
||||
};
|
||||
let public_key: SignedPublicKey = attributes
|
||||
.remove("keydata")
|
||||
.ok_or(())
|
||||
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
|
||||
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
|
||||
.context("keydata attribute is not found")
|
||||
.and_then(|raw| {
|
||||
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
|
||||
})
|
||||
.and_then(|key| {
|
||||
key.verify()
|
||||
.and(Ok(key))
|
||||
.context("autocrypt key cannot be verified")
|
||||
})?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
@@ -154,7 +157,7 @@ impl str::FromStr for Aheader {
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
return Err(());
|
||||
bail!("Unknown Autocrypt attribute found");
|
||||
}
|
||||
|
||||
Ok(Aheader {
|
||||
@@ -172,38 +175,40 @@ mod tests {
|
||||
const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
fn test_from_str() -> Result<()> {
|
||||
let h: Aheader = format!(
|
||||
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
|
||||
RAWKEY
|
||||
)
|
||||
.parse()
|
||||
.expect("failed to parse");
|
||||
.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
#[test]
|
||||
fn test_from_str_reset() {
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!(
|
||||
"addr=reset@example.com; prefer-encrypt=reset; keydata={}",
|
||||
RAWKEY
|
||||
);
|
||||
let h: Aheader = raw.parse().expect("failed to parse");
|
||||
let h: Aheader = raw.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "reset@example.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_non_critical() {
|
||||
fn test_from_str_non_critical() -> Result<()> {
|
||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", RAWKEY);
|
||||
let h: Aheader = raw.parse().expect("failed to parse");
|
||||
let h: Aheader = raw.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -216,7 +221,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_headers() {
|
||||
fn test_good_headers() -> Result<()> {
|
||||
let fixed_header = concat!(
|
||||
"addr=a@b.example.org; prefer-encrypt=mutual; ",
|
||||
"keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
|
||||
@@ -242,7 +247,7 @@ mod tests {
|
||||
" wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
|
||||
);
|
||||
|
||||
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
|
||||
let ah = Aheader::from_str(fixed_header)?;
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(format!("{}", ah), fixed_header);
|
||||
@@ -250,18 +255,17 @@ mod tests {
|
||||
let rendered = ah.to_string();
|
||||
assert_eq!(rendered, fixed_header);
|
||||
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY)).expect("failed to parse");
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY))?;
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Aheader::from_str(&format!(
|
||||
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
|
||||
RAWKEY
|
||||
))
|
||||
.expect("failed to parse");
|
||||
))?;
|
||||
|
||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))
|
||||
.expect("failed to parse");
|
||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
534
src/blob.rs
534
src/blob.rs
@@ -1,5 +1,6 @@
|
||||
//! # Blob directory management
|
||||
//! # Blob directory management.
|
||||
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
|
||||
@@ -7,8 +8,12 @@ use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use anyhow::format_err;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Error;
|
||||
use image::DynamicImage;
|
||||
use image::GenericImageView;
|
||||
use image::ImageFormat;
|
||||
use num_traits::FromPrimitive;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -19,6 +24,7 @@ use crate::constants::{
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::message;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
@@ -53,12 +59,12 @@ impl<'a> BlobObject<'a> {
|
||||
/// underlying error.
|
||||
pub async fn create(
|
||||
context: &'a Context,
|
||||
suggested_name: impl AsRef<str>,
|
||||
suggested_name: &str,
|
||||
data: &[u8],
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let blobdir = context.get_blobdir();
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
||||
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name);
|
||||
let (name, mut file) = BlobObject::create_new_file(context, blobdir, &stem, &ext).await?;
|
||||
file.write_all(data)
|
||||
.await
|
||||
.map_err(|err| BlobError::WriteFailure {
|
||||
@@ -69,7 +75,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
// workaround a bug in async-std
|
||||
// (the executor does not handle blocking operation in Drop correctly,
|
||||
// see https://github.com/async-rs/async-std/issues/900 )
|
||||
// see <https://github.com/async-rs/async-std/issues/900>)
|
||||
let _ = file.flush().await;
|
||||
|
||||
let blob = BlobObject {
|
||||
@@ -82,13 +88,16 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
// Creates a new file, returning a tuple of the name and the handle.
|
||||
async fn create_new_file(
|
||||
context: &Context,
|
||||
dir: &Path,
|
||||
stem: &str,
|
||||
ext: &str,
|
||||
) -> Result<(String, fs::File), BlobError> {
|
||||
let max_attempt = 15;
|
||||
const MAX_ATTEMPT: u32 = 16;
|
||||
let mut attempt = 0;
|
||||
let mut name = format!("{}{}", stem, ext);
|
||||
for attempt in 0..max_attempt {
|
||||
loop {
|
||||
attempt += 1;
|
||||
let path = dir.join(&name);
|
||||
match fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
@@ -98,24 +107,20 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
Ok(file) => return Ok((name, file)),
|
||||
Err(err) => {
|
||||
if attempt == max_attempt {
|
||||
if attempt >= MAX_ATTEMPT {
|
||||
return Err(BlobError::CreateFailure {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: err,
|
||||
});
|
||||
} else if attempt == 1 && !dir.exists().await {
|
||||
fs::create_dir_all(dir).await.ok_or_log(context);
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is supposed to be unreachable, but the compiler doesn't know.
|
||||
Err(BlobError::CreateFailure {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new blob object with unique name by copying an existing file.
|
||||
@@ -132,20 +137,19 @@ impl<'a> BlobObject<'a> {
|
||||
/// copied.
|
||||
pub async fn create_and_copy(
|
||||
context: &'a Context,
|
||||
src: impl AsRef<Path>,
|
||||
src: &Path,
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let mut src_file =
|
||||
fs::File::open(src.as_ref())
|
||||
.await
|
||||
.map_err(|err| BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: String::from(""),
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||
let mut src_file = fs::File::open(src)
|
||||
.await
|
||||
.map_err(|err| BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: String::from(""),
|
||||
src: src.to_path_buf(),
|
||||
cause: err,
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
|
||||
let (name, mut dst_file) =
|
||||
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
|
||||
BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?;
|
||||
let name_for_err = name.clone();
|
||||
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
|
||||
{
|
||||
@@ -156,7 +160,7 @@ impl<'a> BlobObject<'a> {
|
||||
return Err(BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: name_for_err,
|
||||
src: src.as_ref().to_path_buf(),
|
||||
src: src.to_path_buf(),
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
@@ -190,16 +194,13 @@ impl<'a> BlobObject<'a> {
|
||||
/// the [BlobObject::from_path] methods. See those for possible
|
||||
/// errors.
|
||||
pub async fn new_from_path(
|
||||
context: &Context,
|
||||
src: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject<'_>, BlobError> {
|
||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
||||
context: &'a Context,
|
||||
src: &Path,
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
if src.starts_with(context.get_blobdir()) {
|
||||
BlobObject::from_path(context, src)
|
||||
} else if src.as_ref().starts_with("$BLOBDIR/") {
|
||||
BlobObject::from_name(
|
||||
context,
|
||||
src.as_ref().to_str().unwrap_or_default().to_string(),
|
||||
)
|
||||
} else if src.starts_with("$BLOBDIR/") {
|
||||
BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string())
|
||||
} else {
|
||||
BlobObject::create_and_copy(context, src).await
|
||||
}
|
||||
@@ -220,23 +221,22 @@ impl<'a> BlobObject<'a> {
|
||||
/// [BlobError::WrongName] is used if the file name does not
|
||||
/// remain identical after sanitisation.
|
||||
pub fn from_path(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject, BlobError> {
|
||||
let rel_path = path
|
||||
.as_ref()
|
||||
.strip_prefix(context.get_blobdir())
|
||||
.map_err(|_| BlobError::WrongBlobdir {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
src: path.as_ref().to_path_buf(),
|
||||
})?;
|
||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||
context: &'a Context,
|
||||
path: &Path,
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let rel_path =
|
||||
path.strip_prefix(context.get_blobdir())
|
||||
.map_err(|_| BlobError::WrongBlobdir {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
src: path.to_path_buf(),
|
||||
})?;
|
||||
if !BlobObject::is_acceptible_blob_name(rel_path) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
blobname: path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
blobname: path.to_path_buf(),
|
||||
})?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
}
|
||||
@@ -380,7 +380,7 @@ impl<'a> BlobObject<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<(), BlobError> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
|
||||
let img_wh =
|
||||
@@ -391,7 +391,15 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => WORSE_AVATAR_SIZE,
|
||||
};
|
||||
|
||||
self.recode_to_size(context, blob_abs, img_wh).await
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
if let Some(new_name) = self
|
||||
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
|
||||
.await?
|
||||
{
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
|
||||
@@ -410,30 +418,69 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => WORSE_IMAGE_SIZE,
|
||||
};
|
||||
|
||||
self.recode_to_size(context, blob_abs, img_wh).await
|
||||
if self
|
||||
.recode_to_size(context, blob_abs, img_wh, None)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(format_err!(
|
||||
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recode_to_size(
|
||||
&self,
|
||||
context: &Context,
|
||||
blob_abs: PathBuf,
|
||||
img_wh: u32,
|
||||
) -> Result<(), BlobError> {
|
||||
mut blob_abs: PathBuf,
|
||||
mut img_wh: u32,
|
||||
max_bytes: Option<usize>,
|
||||
) -> Result<Option<String>, BlobError> {
|
||||
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err,
|
||||
})?;
|
||||
let orientation = self.get_exif_orientation(context);
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
|
||||
let do_scale = img.width() > img_wh || img.height() > img_wh;
|
||||
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
img.write_to(encoded, image::ImageFormat::Jpeg)?;
|
||||
Ok(())
|
||||
}
|
||||
fn encoded_img_exceeds_bytes(
|
||||
context: &Context,
|
||||
img: &DynamicImage,
|
||||
max_bytes: Option<usize>,
|
||||
encoded: &mut Vec<u8>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if let Some(max_bytes) = max_bytes {
|
||||
encode_img(img, encoded)?;
|
||||
if encoded.len() > max_bytes {
|
||||
info!(
|
||||
context,
|
||||
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
|
||||
encoded.len(),
|
||||
img.width(),
|
||||
img.height(),
|
||||
max_bytes,
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
|
||||
|
||||
let do_scale =
|
||||
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
|
||||
|
||||
if do_scale || do_rotate {
|
||||
if do_scale {
|
||||
img = img.thumbnail(img_wh, img_wh);
|
||||
}
|
||||
|
||||
if do_rotate {
|
||||
img = match orientation {
|
||||
Ok(90) => img.rotate90(),
|
||||
@@ -443,14 +490,64 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err.into(),
|
||||
})?;
|
||||
if do_scale {
|
||||
if !exceeds_width {
|
||||
// The image is already smaller than img_wh, but exceeds max_bytes
|
||||
// We can directly start with trying to scale down to 2/3 of its current width
|
||||
img_wh = max(img.width(), img.height()) * 2 / 3
|
||||
}
|
||||
|
||||
loop {
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B",
|
||||
max_bytes.unwrap_or_default()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
} else {
|
||||
if encoded.is_empty() {
|
||||
encode_img(&new_img, &mut encoded)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px)",
|
||||
encoded.len(),
|
||||
img_wh
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The file format is JPEG now, we may have to change the file extension
|
||||
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
|
||||
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
|
||||
changed_name = Some(format!("$BLOBDIR/{}", file_name));
|
||||
}
|
||||
|
||||
if encoded.is_empty() {
|
||||
encode_img(&img, &mut encoded)?;
|
||||
}
|
||||
|
||||
fs::write(&blob_abs, &encoded)
|
||||
.await
|
||||
.map_err(|err| BlobError::WriteFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err.into(),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(changed_name)
|
||||
}
|
||||
|
||||
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
|
||||
@@ -514,17 +611,24 @@ pub enum BlobError {
|
||||
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||
#[error("Blob has a badname {}", .blobname.display())]
|
||||
WrongName { blobname: PathBuf },
|
||||
#[error("Sql: {0}")]
|
||||
Sql(#[from] crate::sql::Error),
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::File;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::{
|
||||
chat,
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
@@ -626,13 +730,15 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
let src = t.dir.path().join("src");
|
||||
fs::write(&src, b"boo").await.unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t, &src).await.unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let whoops = t.dir.path().join("whoops");
|
||||
assert!(BlobObject::create_and_copy(&t, &whoops).await.is_err());
|
||||
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
||||
.await
|
||||
.is_err());
|
||||
let whoops = t.get_blobdir().join("whoops");
|
||||
assert!(!whoops.exists().await);
|
||||
}
|
||||
@@ -643,7 +749,9 @@ mod tests {
|
||||
|
||||
let src_ext = t.dir.path().join("external");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
@@ -660,7 +768,9 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
blob.as_name(),
|
||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||
@@ -715,4 +825,282 @@ mod tests {
|
||||
assert!(!stem.contains('*'));
|
||||
assert!(!stem.contains('?'));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join("avatar.jpg");
|
||||
assert!(!avatar_blob.exists().await);
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists().await);
|
||||
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
|
||||
let img = image::open(avatar_src).unwrap();
|
||||
assert_eq!(img.width(), 1000);
|
||||
assert_eq!(img.height(), 1000);
|
||||
|
||||
let img = image::open(&avatar_blob).unwrap();
|
||||
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
|
||||
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
|
||||
|
||||
async fn file_size(path_buf: &PathBuf) -> u64 {
|
||||
let file = File::open(path_buf).await.unwrap();
|
||||
file.metadata().await.unwrap().len()
|
||||
}
|
||||
|
||||
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(file_size(&avatar_blob).await <= 3000);
|
||||
assert!(file_size(&avatar_blob).await > 2000);
|
||||
let img = image::open(&avatar_blob).unwrap();
|
||||
assert!(img.width() > 130);
|
||||
assert_eq!(img.width(), img.height());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let img = image::open(&avatar_src).unwrap();
|
||||
assert_eq!(img.width(), 900);
|
||||
assert_eq!(img.height(), 900);
|
||||
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
avatar_cfg,
|
||||
avatar_src.with_extension("jpg").to_str().unwrap()
|
||||
);
|
||||
|
||||
let img = image::open(avatar_cfg).unwrap();
|
||||
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
|
||||
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_copy_without_recode() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join("avatar.png");
|
||||
assert!(!avatar_blob.exists().await);
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists().await);
|
||||
assert_eq!(
|
||||
std::fs::metadata(&avatar_blob).unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_recode_image() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
|
||||
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
bytes,
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE,
|
||||
WORSE_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
bytes,
|
||||
2000,
|
||||
1800,
|
||||
270,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut bytes = vec![];
|
||||
img_rotated
|
||||
.write_to(&mut bytes, image::ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE * 1800 / 2000,
|
||||
WORSE_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
fn assert_correct_rotation(img: &DynamicImage) {
|
||||
// The test images are black in the bottom left corner after correctly applying
|
||||
// the EXIF orientation
|
||||
|
||||
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img
|
||||
.get_pixel(img.width() - 10, img.height() - 10)
|
||||
.to_luma()
|
||||
.0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
async fn send_image_check_mediaquality(
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
original_width: u32,
|
||||
original_height: u32,
|
||||
orientation: i32,
|
||||
compressed_width: u32,
|
||||
compressed_height: u32,
|
||||
) -> anyhow::Result<DynamicImage> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, media_quality_config)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file.jpg");
|
||||
|
||||
File::create(&file).await?.write_all(bytes).await?;
|
||||
let img = image::open(&file)?;
|
||||
assert_eq!(img.width(), original_width);
|
||||
assert_eq!(img.height(), original_height);
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
|
||||
|
||||
let img = image::open(file)?;
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
assert_eq!(prepared_id, msg.id);
|
||||
assert!(msg.is_increation());
|
||||
|
||||
let msg = Message::load_from_db(&t, prepared_id).await?;
|
||||
assert!(msg.is_increation());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_not_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user