mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
816 Commits
1.45.0
...
export_cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b5c900cb9 | ||
|
|
b87ca6e747 | ||
|
|
850f7e1174 | ||
|
|
9f2b5feda2 | ||
|
|
b8cbcc6648 | ||
|
|
584d28f807 | ||
|
|
241111470f | ||
|
|
4bf07ccc71 | ||
|
|
897d2f4a08 | ||
|
|
a81096aa36 | ||
|
|
b1c9342631 | ||
|
|
0c8aad2102 | ||
|
|
82253e1e30 | ||
|
|
aa953687bf | ||
|
|
9f2f2ca1c0 | ||
|
|
54637004cd | ||
|
|
da9f45d9ff | ||
|
|
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 | ||
|
|
cc6b02f037 | ||
|
|
e13bb8fbd4 | ||
|
|
eaca4446aa | ||
|
|
f3fb26c066 | ||
|
|
1a1416e446 | ||
|
|
0afc07f6e7 | ||
|
|
e6d2b1052c | ||
|
|
19c1e6efc3 | ||
|
|
26d9addc5d | ||
|
|
6601015a09 | ||
|
|
36653928f7 | ||
|
|
dda3c605c6 | ||
|
|
2e015e685f | ||
|
|
d4a1858d41 | ||
|
|
d6a6ba01e4 | ||
|
|
27714f596e | ||
|
|
d4e065ee84 | ||
|
|
bc222af661 | ||
|
|
f6136f0ecc | ||
|
|
b2517d3060 | ||
|
|
244260a978 | ||
|
|
dc6fb7d481 | ||
|
|
f17320a9cb | ||
|
|
d1237c9f8d | ||
|
|
b9beaee7d4 | ||
|
|
258856c23a | ||
|
|
72ddd33adf | ||
|
|
1cd53aafff | ||
|
|
4d2ac5a3a2 | ||
|
|
146db48c35 | ||
|
|
9529d76d82 | ||
|
|
b5f2752e41 | ||
|
|
ce4675e9f7 | ||
|
|
f0bd129636 | ||
|
|
dfe3cabb14 | ||
|
|
09735b808e | ||
|
|
37f68459f6 | ||
|
|
3707471266 | ||
|
|
5394327bf6 | ||
|
|
df277b374d | ||
|
|
53dba3c1ba | ||
|
|
6540ee60e5 | ||
|
|
66b5084a1d | ||
|
|
f76aaf3205 | ||
|
|
179a2a50e6 | ||
|
|
720135a915 | ||
|
|
6bb5721f29 | ||
|
|
4dedc2d8ce | ||
|
|
ede9bdc018 | ||
|
|
11823d3b45 | ||
|
|
734ea8ab1b | ||
|
|
7017a050cb | ||
|
|
96e57e7ef3 | ||
|
|
02bc334af5 | ||
|
|
c8fea9c577 | ||
|
|
cdc1063d83 | ||
|
|
704a902cc5 | ||
|
|
36aef6499d | ||
|
|
4ba9c2fafa | ||
|
|
0de8b6a7e5 | ||
|
|
04f816be31 | ||
|
|
7bc919fad5 | ||
|
|
db3f87dd77 | ||
|
|
f43555b41c | ||
|
|
98fc559536 | ||
|
|
4ab90f7069 | ||
|
|
99b2d79312 | ||
|
|
6963fd877d | ||
|
|
045fbab7cd | ||
|
|
f69bcc71ed | ||
|
|
c1fddebc06 | ||
|
|
04891238d4 | ||
|
|
8703da83f5 | ||
|
|
4ae86b4e61 | ||
|
|
e418d89c79 | ||
|
|
6f4090fbf6 | ||
|
|
29cbbf9ce8 | ||
|
|
cd3c2a6c6c | ||
|
|
27a7fae9c6 | ||
|
|
1deaf87b24 | ||
|
|
75adbd2c8f | ||
|
|
165c57f0a4 | ||
|
|
476e613377 | ||
|
|
2a39dc06e9 | ||
|
|
a698a8dd84 | ||
|
|
5c2d6c22a0 | ||
|
|
40d7f3ff71 | ||
|
|
5535475cc9 | ||
|
|
c3dd47beba | ||
|
|
f9c5ad817b | ||
|
|
81cd577bf0 | ||
|
|
f789de7044 | ||
|
|
b035a721ef | ||
|
|
f8755b505e | ||
|
|
38169b2aad | ||
|
|
6dab25e5fb | ||
|
|
f973b75d6a | ||
|
|
a9aeea0ffc | ||
|
|
79e72418bb | ||
|
|
01af3b7547 | ||
|
|
6d93d7af63 | ||
|
|
6792523fcd | ||
|
|
0fa90a81e5 | ||
|
|
2c613b3837 | ||
|
|
036c9cd513 | ||
|
|
24cb6aa9a4 | ||
|
|
e1fec6a460 | ||
|
|
04a978687f | ||
|
|
f464e43ba9 | ||
|
|
21e67c79a1 | ||
|
|
6132cc2a42 | ||
|
|
57a6f27c87 | ||
|
|
374ee7c1fe | ||
|
|
88a9a13795 | ||
|
|
046a2a8eae | ||
|
|
dab91574f2 | ||
|
|
f77beaf4fc | ||
|
|
ffe68cadec | ||
|
|
d4e90c7fff | ||
|
|
0c2b3e838e | ||
|
|
e7c6667347 | ||
|
|
0b3fb9c0a3 | ||
|
|
2865ced3c0 | ||
|
|
309bea8e2a | ||
|
|
3ead349ccf | ||
|
|
cda2fc4fea | ||
|
|
95a0481b63 | ||
|
|
7d27c2bfea | ||
|
|
c0023cb54d | ||
|
|
e3f7b31501 | ||
|
|
35b0f00a88 | ||
|
|
b505d2666b | ||
|
|
1f59b5cd15 | ||
|
|
5c684eb3c1 | ||
|
|
57841cdcc0 | ||
|
|
7404e8c85f | ||
|
|
a24b607640 | ||
|
|
a88893f262 | ||
|
|
c620d3e215 | ||
|
|
ce09988ee5 | ||
|
|
a83293102e | ||
|
|
c3232e6d8f | ||
|
|
d0f0728245 | ||
|
|
5e4dde12e2 | ||
|
|
ced3a56da4 | ||
|
|
a2c3233c19 | ||
|
|
982dc53dc1 | ||
|
|
be7cee2c37 | ||
|
|
148ad31024 | ||
|
|
6fddcd83c1 | ||
|
|
46a3226e43 | ||
|
|
29f184b4c4 | ||
|
|
ad640e163c | ||
|
|
40d9a1ec22 | ||
|
|
0601b05cb7 | ||
|
|
59f9fc7cbf | ||
|
|
a5c8c977db | ||
|
|
10435a10e9 | ||
|
|
e82ec23024 | ||
|
|
c41f1b42df | ||
|
|
3f9242a610 | ||
|
|
6a4624be25 | ||
|
|
5922069b77 | ||
|
|
a2d64cbb4c | ||
|
|
65fb2d791b | ||
|
|
6aeda98c0a | ||
|
|
a8c389c3b4 | ||
|
|
3c2d698f4c | ||
|
|
61964707d3 | ||
|
|
d3b66cf724 | ||
|
|
24602ed8a8 | ||
|
|
e741cb3646 | ||
|
|
93ba6c1ce8 | ||
|
|
396ec131fc | ||
|
|
5acf8e1aac | ||
|
|
0cd8710289 | ||
|
|
fbec12393d | ||
|
|
a34cfd56b4 | ||
|
|
0ef6a3060a | ||
|
|
dc893bf5cd | ||
|
|
b0a3a0046c | ||
|
|
8ac2cdd929 | ||
|
|
e820d072f5 | ||
|
|
6bb0c164f9 | ||
|
|
5ee4bb58cd | ||
|
|
002ea8ed98 | ||
|
|
fe9c419e5d | ||
|
|
2407fbd1f0 | ||
|
|
b3fe74e0f0 | ||
|
|
93bd9422e7 | ||
|
|
b53415fed5 | ||
|
|
4a30cb6cd6 | ||
|
|
9ef0fefb75 | ||
|
|
1e2ec8e264 | ||
|
|
0c27e8ccaa | ||
|
|
6a834c9756 | ||
|
|
803452cbde | ||
|
|
11e3380f65 | ||
|
|
4508eced37 | ||
|
|
47a6e31047 | ||
|
|
785cc795e3 | ||
|
|
6b9b39b953 | ||
|
|
1ac44e5a77 | ||
|
|
1e6d8063c8 | ||
|
|
c8c2724c28 | ||
|
|
e6dd963ebb | ||
|
|
564d681bca | ||
|
|
c469798734 | ||
|
|
355e0145c0 | ||
|
|
7b5a3a8346 | ||
|
|
3d072a81b4 | ||
|
|
3fe5eb31d4 | ||
|
|
b938d5facd | ||
|
|
e9c582c4e4 | ||
|
|
2a8c418d54 | ||
|
|
b31becea2b | ||
|
|
c4ebb0a31e | ||
|
|
934dc420a8 | ||
|
|
b67fbedcef | ||
|
|
687db252b6 | ||
|
|
5561aada45 | ||
|
|
412e3c22df | ||
|
|
687c92d738 | ||
|
|
83dd1c6232 | ||
|
|
2435803fa3 | ||
|
|
6d5ccdf721 | ||
|
|
43e95ce68b | ||
|
|
dae20d90ed | ||
|
|
7154a47f85 | ||
|
|
9957bad83d | ||
|
|
93845c2a18 | ||
|
|
7b80801cb7 | ||
|
|
32cbdc630d | ||
|
|
1d62448903 | ||
|
|
f7ecf34ead | ||
|
|
ccee289a5c | ||
|
|
08c46af3aa | ||
|
|
4636785449 | ||
|
|
e077ee9238 | ||
|
|
662735c233 | ||
|
|
fef2a48054 | ||
|
|
8412affe37 | ||
|
|
ebccdbbcb9 | ||
|
|
3c387a3cb3 | ||
|
|
5e8e77dfb6 | ||
|
|
eeba70eb49 | ||
|
|
e2688f6355 | ||
|
|
bb9e6038c4 | ||
|
|
0e1ca4323c | ||
|
|
1b9cd18e33 | ||
|
|
f4c8ffca4c | ||
|
|
6edff503aa | ||
|
|
31ae099e19 | ||
|
|
0f90d50385 | ||
|
|
5bbbe4b79c | ||
|
|
c243b89f7d | ||
|
|
4690ba017f | ||
|
|
38ead5b72c | ||
|
|
30c334d887 | ||
|
|
78fd0c285b | ||
|
|
86a8767d94 | ||
|
|
4bc4aa0705 | ||
|
|
f98aa0d906 | ||
|
|
179dd0f3a1 | ||
|
|
8979232cfb | ||
|
|
d3aba4e817 | ||
|
|
53fed91a17 | ||
|
|
ff3dd878c5 | ||
|
|
03232eb79c | ||
|
|
9edc6702f1 | ||
|
|
2b207e1375 | ||
|
|
4d2c2130e8 | ||
|
|
af8a6d7722 | ||
|
|
bc67fa3204 | ||
|
|
29991f1caf | ||
|
|
e982549046 | ||
|
|
ec83fae314 | ||
|
|
518e87b0cf | ||
|
|
09113e2579 | ||
|
|
eb693a4a21 | ||
|
|
00a223b574 | ||
|
|
93e038e056 | ||
|
|
dea9630380 | ||
|
|
30e7f84770 | ||
|
|
fc1f44c6d6 | ||
|
|
f774665921 | ||
|
|
7b291c1416 | ||
|
|
8fcb8c3788 | ||
|
|
cdb7f1dd9f | ||
|
|
af045c245d | ||
|
|
0bdd1b7dc2 | ||
|
|
c6f6751c89 | ||
|
|
e77706f7d0 | ||
|
|
8de73c4566 | ||
|
|
56e6c2712b | ||
|
|
b510d74c4a | ||
|
|
966712019f | ||
|
|
f919e4962d | ||
|
|
412645e1ce | ||
|
|
7c9624e822 | ||
|
|
e79533ae54 | ||
|
|
d8babe2d0c | ||
|
|
83df69f43c | ||
|
|
2a9d06d817 | ||
|
|
4ef2a7c8d7 | ||
|
|
75d79dc79c | ||
|
|
2720d34594 | ||
|
|
7c15e4e948 | ||
|
|
4f836950bc | ||
|
|
1321a78f87 | ||
|
|
210d8bad04 | ||
|
|
e19d2cccfc | ||
|
|
a29dc514d3 | ||
|
|
07109e9b17 | ||
|
|
889f4673ad | ||
|
|
7fa794cd72 | ||
|
|
b21508fdb7 | ||
|
|
92175b27ab | ||
|
|
4d2a39febb | ||
|
|
4fa667d834 | ||
|
|
38ed94367c | ||
|
|
3a7bd8b49d | ||
|
|
c8d4eee794 | ||
|
|
d66174e55a | ||
|
|
8d2a5cd242 | ||
|
|
2fbef80df8 | ||
|
|
07768133d5 | ||
|
|
9df88745dc | ||
|
|
bd856d90db | ||
|
|
1f4403d149 | ||
|
|
6345e57720 | ||
|
|
332f32e799 | ||
|
|
deb506cb52 | ||
|
|
66907c17d3 | ||
|
|
bf82dd9c60 | ||
|
|
46c544a5ca | ||
|
|
c53c6cdf90 | ||
|
|
c6c2fb562e | ||
|
|
d30bedda96 | ||
|
|
f7a4f5debf | ||
|
|
ea759f17d0 | ||
|
|
236faafe0f | ||
|
|
769f9af861 | ||
|
|
bf83f6d4ad | ||
|
|
8232a148aa | ||
|
|
04a4424664 | ||
|
|
da729a8345 | ||
|
|
25513a6e42 | ||
|
|
9063725729 | ||
|
|
46833ca4f2 | ||
|
|
dbdea787a7 | ||
|
|
5c1bbc5d6a | ||
|
|
f30c319fbf | ||
|
|
b2b59852a7 | ||
|
|
4c4a9b52de | ||
|
|
c8242b12fe | ||
|
|
622d99a971 | ||
|
|
45ea41262c | ||
|
|
ed5167babc | ||
|
|
11d9fcad35 | ||
|
|
fa7b6c001e | ||
|
|
42bd1bc806 | ||
|
|
31bf34890a | ||
|
|
34af492afb | ||
|
|
ef245b5759 | ||
|
|
41b2dee4ca | ||
|
|
1ce1a01d49 | ||
|
|
1cd3ee6a05 | ||
|
|
75df8f762c | ||
|
|
e5da5c48f1 | ||
|
|
5b5c6a9c31 | ||
|
|
4ae1a17cc0 | ||
|
|
0781316c97 | ||
|
|
8eb73a5ade | ||
|
|
e6b7a7e292 | ||
|
|
c438691b73 | ||
|
|
1cacfb30ff | ||
|
|
39a00929c7 | ||
|
|
8156692e5a | ||
|
|
5a9a4dbbab | ||
|
|
df56b76182 | ||
|
|
0fc1134bab | ||
|
|
dfecd033a7 | ||
|
|
6c5eaaed2c | ||
|
|
dcc00075b0 | ||
|
|
a320fb9d6c | ||
|
|
3bef4909d5 | ||
|
|
26aeacc6be | ||
|
|
0b1288fc17 | ||
|
|
c005f756d6 | ||
|
|
3c6d52842e | ||
|
|
4d2542cee5 | ||
|
|
9bf8799484 | ||
|
|
e15372531e | ||
|
|
0ae9443e22 | ||
|
|
67cddedf7e | ||
|
|
bf72ae4ccc | ||
|
|
c7863c67bf | ||
|
|
3d108fedc4 | ||
|
|
546e8dedce | ||
|
|
64cd48a4e1 | ||
|
|
2e118b773e | ||
|
|
6206c82ee5 | ||
|
|
e7736138a8 | ||
|
|
78b44cb4d0 | ||
|
|
0a9a2394d8 | ||
|
|
6e37c1442e | ||
|
|
c323798386 | ||
|
|
b8e98c0b81 | ||
|
|
7a82fd4bbd | ||
|
|
0a300da347 | ||
|
|
aa26c52813 | ||
|
|
19697e255e | ||
|
|
07e4762f71 | ||
|
|
6ec743f8b1 | ||
|
|
010be693e1 | ||
|
|
d8a7a178c2 | ||
|
|
18e9073bfe | ||
|
|
0032468a87 | ||
|
|
7e793a518c | ||
|
|
e5b0194e8c | ||
|
|
13055b9c87 | ||
|
|
5661e0b8f1 | ||
|
|
1672905c71 | ||
|
|
d13d62105a | ||
|
|
0b80b81129 | ||
|
|
9b72aba8e3 | ||
|
|
1f24c5f8a4 | ||
|
|
7f882a6406 | ||
|
|
50f3af58f8 | ||
|
|
8425e23d82 | ||
|
|
9fc6bbf41f | ||
|
|
1e2e042244 | ||
|
|
03d86360d6 | ||
|
|
4eb8d3fef6 | ||
|
|
da727740ab | ||
|
|
3a993a4b77 | ||
|
|
45dae1ff0c | ||
|
|
f144426bf5 | ||
|
|
e447bdc0c3 | ||
|
|
c1768bb311 | ||
|
|
66cb3d4358 | ||
|
|
47f4f2bd08 | ||
|
|
12cf89735c | ||
|
|
d240bbcd07 | ||
|
|
5e07a36cd2 | ||
|
|
49b5962568 | ||
|
|
a7998c190c | ||
|
|
b8a55f3aa4 | ||
|
|
ab8bf3c2f3 | ||
|
|
d05dd977d9 | ||
|
|
8b3494b5c1 | ||
|
|
d9a45eb931 | ||
|
|
cb5bcebf75 | ||
|
|
69f159792e | ||
|
|
bb50b9abe4 | ||
|
|
48e1f53826 | ||
|
|
be88b946b6 | ||
|
|
c2b222e6a5 | ||
|
|
cf5342c367 | ||
|
|
990ab739cc | ||
|
|
eaec03142b | ||
|
|
ea731a3619 | ||
|
|
719cba68b3 | ||
|
|
20182b027e | ||
|
|
8c82a5cbfa | ||
|
|
25274f13c3 | ||
|
|
093839c2b0 | ||
|
|
4c8e6ef495 | ||
|
|
2fe600f885 | ||
|
|
9739c0305b | ||
|
|
893e4b91ba | ||
|
|
5cb1d10401 | ||
|
|
11107d5484 | ||
|
|
5405bfbc8d | ||
|
|
a0c92753a9 | ||
|
|
de97e0263f | ||
|
|
44558d0ce8 | ||
|
|
be40417a7f | ||
|
|
8301e27f86 | ||
|
|
21b18836ca | ||
|
|
2e3352afca | ||
|
|
9667859410 | ||
|
|
b437ab86d1 | ||
|
|
1fdb697c09 | ||
|
|
7200e62375 | ||
|
|
7ddf3ba754 | ||
|
|
7786a4ced4 | ||
|
|
c649db15b6 | ||
|
|
60a8b47ad0 | ||
|
|
0344bc387c | ||
|
|
0f1798ae50 | ||
|
|
02baf4b1f0 | ||
|
|
9fb2c59b6e | ||
|
|
9121e30600 | ||
|
|
39d8cffe18 | ||
|
|
9486c67904 | ||
|
|
29c4bbab2b | ||
|
|
9a80385278 | ||
|
|
f0fb1bfdcb | ||
|
|
ab90b6b390 | ||
|
|
e9733e7525 | ||
|
|
f3c7d2f9c6 | ||
|
|
b5e1b1a2d2 | ||
|
|
5c1b69c3c5 | ||
|
|
12bc364e42 | ||
|
|
879bd7e35e | ||
|
|
81b0b24114 | ||
|
|
2095962466 | ||
|
|
0c03024b97 | ||
|
|
cd990039bd | ||
|
|
184f303b54 | ||
|
|
637d2661e8 | ||
|
|
987eaae0c1 | ||
|
|
fc0e88539a | ||
|
|
c124eadf9d | ||
|
|
423c0dc808 | ||
|
|
97b1a1c392 | ||
|
|
fe1c99c5e8 | ||
|
|
332a387c98 | ||
|
|
92b304dee4 | ||
|
|
92abae0b5b | ||
|
|
81db6e3ee2 | ||
|
|
af67e798fb | ||
|
|
4090120041 | ||
|
|
49f07421ec | ||
|
|
7b38d6693d | ||
|
|
277bbfaead | ||
|
|
f8d7242079 | ||
|
|
498880d874 | ||
|
|
4573e6d18b | ||
|
|
a26c43e9fd | ||
|
|
238c4bb792 | ||
|
|
efcdb45301 | ||
|
|
0485c55718 | ||
|
|
5742360e3e | ||
|
|
99a36e8629 | ||
|
|
6253a2cef7 | ||
|
|
aee6eb2261 | ||
|
|
6d6ac66f4d | ||
|
|
4ed2638594 | ||
|
|
b892dafa49 | ||
|
|
e870b33e03 | ||
|
|
27e53ddbff | ||
|
|
396ccebb5c | ||
|
|
f9cc3cbef0 | ||
|
|
0a5d1e5551 | ||
|
|
49c8964aec | ||
|
|
ec5ca4464b | ||
|
|
c3f9f473ac | ||
|
|
b0d68ce09e | ||
|
|
191de6c445 | ||
|
|
6ebdbe7dd6 | ||
|
|
b42b1ad99b | ||
|
|
b28f5c8716 | ||
|
|
3ce244b048 | ||
|
|
53c47bd862 | ||
|
|
d6a0763b1d | ||
|
|
ecbc83390e | ||
|
|
f5b16cf086 | ||
|
|
cdba74a027 | ||
|
|
a065f654e8 | ||
|
|
2a254c51fa | ||
|
|
428dbfb537 | ||
|
|
b0bb0214c0 | ||
|
|
f7897d5f1a | ||
|
|
42c5bbcda3 | ||
|
|
f657b2950c | ||
|
|
6fcc589655 | ||
|
|
a7178f4f25 | ||
|
|
ce01e1652f | ||
|
|
c6339c4ae4 | ||
|
|
65f2a3b1c6 | ||
|
|
87c6f7d42b | ||
|
|
cd925624a7 | ||
|
|
cdd1ccb458 | ||
|
|
e388e4cc1e | ||
|
|
9b741825ef | ||
|
|
f4e0c6b5f1 | ||
|
|
a68528479f | ||
|
|
383c5ba7fd | ||
|
|
ee27c7d9d4 | ||
|
|
11b9a933b0 | ||
|
|
8d9fa233c5 | ||
|
|
edcad6f5d5 | ||
|
|
23eb3c40ca | ||
|
|
8727e0acf8 | ||
|
|
dd682e87db | ||
|
|
0b743c6bc3 | ||
|
|
8f290530fd | ||
|
|
0f164861c7 | ||
|
|
4481ab18f5 | ||
|
|
927c7eb59d | ||
|
|
6eef4066db | ||
|
|
df8e5f6088 | ||
|
|
1cb4e41883 | ||
|
|
cbfada3e4a | ||
|
|
c19e35b68d | ||
|
|
94e52b5598 | ||
|
|
f6854fd22f | ||
|
|
3fc03fb5ca | ||
|
|
42bd9f71f0 | ||
|
|
064d62e758 | ||
|
|
5a2f0d07a0 | ||
|
|
fd54b6b5b1 | ||
|
|
aae8163696 | ||
|
|
a01755b65b | ||
|
|
6763dd653e | ||
|
|
0fc57bdb35 | ||
|
|
4bd2a9084c | ||
|
|
0816e6d0f6 | ||
|
|
c7d72d64cc | ||
|
|
e33f6c1c85 | ||
|
|
b4c85c534d | ||
|
|
763334d0aa | ||
|
|
be922eef0f | ||
|
|
1325b2f7c6 | ||
|
|
b9ca7b8ace | ||
|
|
3faf968b7c | ||
|
|
1a736ca6c3 | ||
|
|
f1ec1a0765 | ||
|
|
fc2367894b | ||
|
|
a0293de397 |
@@ -7,116 +7,13 @@ executors:
|
||||
doxygen:
|
||||
docker:
|
||||
- image: hrektts/doxygen
|
||||
python:
|
||||
docker:
|
||||
- image: 3.7.7-stretch
|
||||
|
||||
|
||||
restore-workspace: &restore-workspace
|
||||
attach_workspace:
|
||||
at: /mnt
|
||||
|
||||
restore-cache: &restore-cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||
- repo-source-{{ .Branch }}-{{ .Revision }}
|
||||
|
||||
commands:
|
||||
test_target:
|
||||
parameters:
|
||||
target:
|
||||
type: string
|
||||
steps:
|
||||
- *restore-workspace
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Test (<< parameters.target >>)
|
||||
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
|
||||
no_output_timeout: 15m
|
||||
|
||||
jobs:
|
||||
cargo_fetch:
|
||||
executor: default
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||
- run: rustup install $(cat rust-toolchain)
|
||||
- run: rustup default $(cat rust-toolchain)
|
||||
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
|
||||
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
|
||||
- run: cargo fetch
|
||||
- run: rustc +stable --version
|
||||
- run: rustc +$(cat rust-toolchain) --version
|
||||
# make sure this git repo doesn't grow too big
|
||||
- run: git gc
|
||||
- persist_to_workspace:
|
||||
root: /mnt
|
||||
paths:
|
||||
- crate
|
||||
- save_cache:
|
||||
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||
paths:
|
||||
- "~/.cargo"
|
||||
- "~/.rustup"
|
||||
|
||||
rustfmt:
|
||||
executor: default
|
||||
steps:
|
||||
- *restore-workspace
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Run cargo fmt
|
||||
command: cargo fmt --all -- --check
|
||||
|
||||
test_macos:
|
||||
macos:
|
||||
xcode: "10.0.0"
|
||||
working_directory: ~/crate
|
||||
steps:
|
||||
- run:
|
||||
name: Configure environment variables
|
||||
command: |
|
||||
echo 'export PATH="${HOME}/.cargo/bin:${HOME}/.bin:${PATH}"' >> $BASH_ENV
|
||||
echo 'export CIRCLE_ARTIFACTS="/tmp"' >> $BASH_ENV
|
||||
- checkout
|
||||
- run:
|
||||
name: Install Rust
|
||||
command: |
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- run: rustup install $(cat rust-toolchain)
|
||||
- run: rustup default $(cat rust-toolchain)
|
||||
- run: cargo fetch
|
||||
- run:
|
||||
name: Test
|
||||
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
|
||||
|
||||
test_x86_64-unknown-linux-gnu:
|
||||
executor: default
|
||||
steps:
|
||||
- test_target:
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
|
||||
test_i686-unknown-linux-gnu:
|
||||
executor: default
|
||||
steps:
|
||||
- test_target:
|
||||
target: "i686-unknown-linux-gnu"
|
||||
|
||||
test_aarch64-linux-android:
|
||||
executor: default
|
||||
steps:
|
||||
- test_target:
|
||||
target: "aarch64-linux-android"
|
||||
|
||||
|
||||
build_doxygen:
|
||||
executor: doxygen
|
||||
steps:
|
||||
- checkout
|
||||
- run: bash ci_scripts/run-doxygen.sh
|
||||
- run: bash scripts/run-doxygen.sh
|
||||
- run: mkdir -p workspace/c-docs
|
||||
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
|
||||
- persist_to_workspace:
|
||||
@@ -130,7 +27,10 @@ jobs:
|
||||
- checkout
|
||||
# the following commands on success produces
|
||||
# workspace/{wheelhouse,py-docs} as artefact directories
|
||||
- run: bash ci_scripts/remote_python_packaging.sh
|
||||
- run:
|
||||
# building aarch64 packages under qemu is very slow
|
||||
no_output_timeout: 60m
|
||||
command: bash scripts/remote_python_packaging.sh
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
@@ -138,93 +38,40 @@ jobs:
|
||||
- py-docs
|
||||
- wheelhouse
|
||||
|
||||
remote_tests_python:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: ci_scripts/remote_tests_python.sh
|
||||
|
||||
upload_docs_wheels:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: workspace
|
||||
- run: pyenv versions
|
||||
- run: pyenv global 3.5.2
|
||||
- run: ls -laR workspace
|
||||
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||
|
||||
clippy:
|
||||
executor: default
|
||||
steps:
|
||||
- *restore-workspace
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Run cargo clippy
|
||||
command: cargo clippy
|
||||
|
||||
- run: scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||
|
||||
workflows:
|
||||
version: 2.1
|
||||
|
||||
test:
|
||||
jobs:
|
||||
# - cargo_fetch
|
||||
|
||||
- remote_tests_python:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- remote_python_packaging:
|
||||
requires:
|
||||
- remote_tests_python
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
only: /.+/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
- upload_docs_wheels:
|
||||
requires:
|
||||
- remote_python_packaging
|
||||
- build_doxygen
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
# - rustfmt:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
# - clippy:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
only: /.+/
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
# Linux Desktop 64bit
|
||||
# - test_x86_64-unknown-linux-gnu:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
# Linux Desktop 32bit
|
||||
# - test_i686-unknown-linux-gnu:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
# Android 64bit
|
||||
# - test_aarch64-linux-android:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
# Desktop Apple
|
||||
# - test_macos:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
ignore: /.*/
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -12,3 +12,6 @@ test-data/* text=false
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
|
||||
*.py diff=python
|
||||
*.rs diff=rust
|
||||
*.md diff=markdown
|
||||
|
||||
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
|
||||
86
.github/workflows/ci.yml
vendored
86
.github/workflows/ci.yml
vendored
@@ -14,11 +14,11 @@ jobs:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.45.0
|
||||
toolchain: 1.50.0
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
@@ -29,38 +29,52 @@ jobs:
|
||||
run_clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.45.0
|
||||
toolchain: 1.50.0
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all
|
||||
args: --workspace --tests --examples
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rust-docs
|
||||
override: true
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- name: Rustdoc
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --document-private-items --no-deps
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [1.45.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:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.50.0
|
||||
python: 3.6
|
||||
- os: windows-latest
|
||||
rust: 1.50.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
@@ -90,8 +104,10 @@ jobs:
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: check
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl
|
||||
|
||||
- name: tests
|
||||
@@ -99,3 +115,29 @@ jobs:
|
||||
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,doc,py3
|
||||
|
||||
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
@@ -1,5 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/build
|
||||
|
||||
# ignore vi temporaries
|
||||
*~
|
||||
@@ -25,3 +26,5 @@ deltachat-ffi/html
|
||||
deltachat-ffi/xml
|
||||
|
||||
.rsynclist
|
||||
|
||||
coverage/
|
||||
|
||||
385
CHANGELOG.md
385
CHANGELOG.md
@@ -1,5 +1,389 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
- database library changed from rusqlite to sqlx #2089 #2331 #2336 #2340
|
||||
|
||||
- add alias support: UIs should check for `dc_msg_get_override_sender_name()`
|
||||
also in single-chats now and display divergent names and avatars #2297
|
||||
|
||||
- parse blockquote-tags for better quote detection #2313
|
||||
|
||||
- ignore unknown classical emails from spam folder #2311
|
||||
|
||||
- support "Mixed Up” encryption repairing #2321
|
||||
|
||||
- fix single chat search #2344
|
||||
|
||||
- fix nightly clippy and rustc errors #2341
|
||||
|
||||
- update dependencies #2350
|
||||
|
||||
- improve ci #2342
|
||||
|
||||
- improve python bindings #2332 #2326
|
||||
|
||||
|
||||
## 1.51.0
|
||||
|
||||
- breaking change: You have to call `dc_stop_io()`/`dc_start_io()`
|
||||
before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`:
|
||||
fix race condition and db corruption
|
||||
when a message was received during backup #2253
|
||||
|
||||
- save subject for messages: new api `dc_msg_get_subject()`,
|
||||
when quoting, use the subject of the quoted message as the new subject,
|
||||
instead of the last subject in the chat #2274 #2283
|
||||
|
||||
- new apis to get full or html message,
|
||||
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151 #2264 #2279
|
||||
|
||||
- new chat type and apis for the new mailing list support,
|
||||
`DC_CHAT_TYPE_MAILINGLIST`, `dc_msg_get_real_chat_id()`,
|
||||
`dc_msg_get_override_sender_name()` #1964 #2181 #2185 #2195 #2211 #2210 #2240
|
||||
#2241 #2243 #2258 #2259 #2261 #2267 #2270 #2272 #2290
|
||||
|
||||
- new api `dc_decide_on_contact_request()`,
|
||||
deprecated `dc_create_chat_by_msg_id()` and `dc_marknoticed_contact()` #1964
|
||||
|
||||
- new flag `DC_GCM_INFO_ONLY` for api `dc_get_chat_msgs()` #2132
|
||||
|
||||
- new api `dc_get_chat_encrinfo()` #2186
|
||||
|
||||
- new api `dc_contact_get_status()`, returning the recent footer #2218 #2307
|
||||
|
||||
- improve contact name update rules,
|
||||
add api `dc_contact_get_auth_name()` #2206 #2212 #2225
|
||||
|
||||
- new api for bots: `dc_msg_set_html()` #2153
|
||||
|
||||
- new api for bots: `dc_msg_set_override_sender_name()` #2231
|
||||
|
||||
- api removed: `dc_is_io_running()` #2139
|
||||
|
||||
- api removed: `dc_contact_get_first_name()` #2165 #2171
|
||||
|
||||
- improve compatibility with providers changing the Message-ID
|
||||
(as Outlook.com) #2250 #2265
|
||||
|
||||
- correctly show emails that were sent to an alias and then bounced
|
||||
|
||||
- implement Consistent Color Generation (XEP-0392),
|
||||
that results in contact colors be be changed #2228 #2229 #2239
|
||||
|
||||
- fetch recent existing messages
|
||||
and create corresponding chats after configure #2106
|
||||
|
||||
- improve e-mail compatibility
|
||||
by scanning all folders from time to time #2067 #2152 #2158 #2184 #2215 #2224
|
||||
|
||||
- better support videochat-services not supporting random rooms #2191
|
||||
|
||||
- export backups as .tar files #2023
|
||||
|
||||
- scale avatars based on media_quality, fix avatar rotation #2063
|
||||
|
||||
- compare ephemeral timer to parent message to deal with reordering better #2100
|
||||
|
||||
- better ephemeral system messages #2183
|
||||
|
||||
- read quotes out of html messages #2104
|
||||
|
||||
- prepend subject to messages with attachments, if needed #2111
|
||||
|
||||
- run housekeeping at least once a day #2114
|
||||
|
||||
- resolve MX domain only once per OAuth2 provider #2122
|
||||
|
||||
- configure provider based on MX record #2123 #2134
|
||||
|
||||
- make transient bad destination address error permanent
|
||||
after n tries #2126 #2202
|
||||
|
||||
- enable strict TLS for known providers by default #2121
|
||||
|
||||
- improve and harden secure join #2154 #2161 #2251
|
||||
|
||||
- update `dc_get_info()` to return more information #2156
|
||||
|
||||
- prefer In-Reply-To/References
|
||||
over group-id stored in Message-ID #2164 #2172 #2173
|
||||
|
||||
- apply gossiped encryption preference to new peerstates #2174
|
||||
|
||||
- fix: do not return quoted messages from the trash chat #2221
|
||||
|
||||
- fix: allow emojis for location markers #2177
|
||||
|
||||
- fix encoding of Chat-Group-Name-Changed messages that could even lead to
|
||||
messages not being delivered #2141
|
||||
|
||||
- fix error when no temporary directory is available #1929
|
||||
|
||||
- fix marking read receipts as seen #2117
|
||||
|
||||
- fix read-notification for mixed-case addresses #2103
|
||||
|
||||
- fix decoding of attachment filenames #2080 #2094 #2102
|
||||
|
||||
- fix downloading ranges of message #2061
|
||||
|
||||
- fix parsing quoted encoded words in From: header #2193 #2204
|
||||
|
||||
- fix import/export race condition #2250
|
||||
|
||||
- fix: exclude muted chats from notified-list #2269 #2275
|
||||
|
||||
- fix: update uid_next if the server rewind it #2288
|
||||
|
||||
- fix: return error on fingerprint mismatch on qr-scan #2295
|
||||
|
||||
- fix ci #2217 #2226 #2244 #2245 #2249 #2277 #2286
|
||||
|
||||
- try harder on backup opening #2148
|
||||
|
||||
- trash messages more thoroughly #2273
|
||||
|
||||
- nicer logging #2284
|
||||
|
||||
- add CMakeLists.txt #2260
|
||||
|
||||
- switch to rust 1.50, update toolchains, deps #2150 #2155 #2165 #2107 #2262 #2271
|
||||
|
||||
- improve python bindings #2113 #2115 #2133 #2214
|
||||
|
||||
- improve documentation #2143 #2160 #2175 #2146
|
||||
|
||||
- refactorings #2110 #2136 #2135 #2168 #2178 #2189 #2190 #2198 #2197 #2201 #2196
|
||||
#2200 #2230 #2262 #2203
|
||||
|
||||
- update provider-database #2299
|
||||
|
||||
|
||||
## 1.50.0
|
||||
|
||||
- do not fetch emails in between inbox_watch disabled and enabled again #2087
|
||||
|
||||
- fix: do not fetch from INBOX if inbox_watch is disabled #2085
|
||||
|
||||
- fix: do not use STARTTLS when PLAIN connection is requested
|
||||
and do not allow downgrade if STARTTLS is not available #2071
|
||||
|
||||
|
||||
## 1.49.0
|
||||
|
||||
- add timestamps to image and video filenames #2068
|
||||
|
||||
- forbid quoting messages from another context #2069
|
||||
|
||||
- fix: preserve quotes in messages with attachments #2070
|
||||
|
||||
|
||||
## 1.48.0
|
||||
|
||||
- `fetch_existing` renamed to `fetch_existing_msgs` and disabled by default
|
||||
#2035 #2042
|
||||
|
||||
- skip fetch existing messages/contacts if config-option `bot` set #2017
|
||||
|
||||
- always log why a message is sorted to trash #2045
|
||||
|
||||
- display a quote if top posting is detected #2047
|
||||
|
||||
- add ephemeral task cancellation to `dc_stop_io()`;
|
||||
before, there was no way to quickly terminate pending ephemeral tasks #2051
|
||||
|
||||
- when saved-messages chat is deleted,
|
||||
a device-message about recreation is added #2050
|
||||
|
||||
- use `max_smtp_rcpt_to` from provider-db,
|
||||
sending messages to many recipients in configurable chunks #2056
|
||||
|
||||
- fix handling of empty autoconfigure files #2027
|
||||
|
||||
- fix adding saved messages to wrong chats on multi-device #2034 #2039
|
||||
|
||||
- fix hang on android4.4 and other systems
|
||||
by adding a workaround to executer-blocking-handling bug #2040
|
||||
|
||||
- fix secret key export/import roundtrip #2048
|
||||
|
||||
- fix mistakenly unarchived chats #2057
|
||||
|
||||
- fix outdated-reminder test that fails only 7 days a year,
|
||||
including halloween :) #2059
|
||||
|
||||
- improve python bindings #2021 #2036 #2038
|
||||
|
||||
- update provider-database #2037
|
||||
|
||||
|
||||
## 1.47.0
|
||||
|
||||
- breaking change: `dc_update_device_chats()` removed;
|
||||
this is now done automatically during configure
|
||||
unless the new config-option `bot` is set #1957
|
||||
|
||||
- breaking change: split `DC_EVENT_MSGS_NOTICED` off `DC_EVENT_MSGS_CHANGED`
|
||||
and remove `dc_marknoticed_all_chats()` #1942 #1981
|
||||
|
||||
- breaking change: remove unused starring options #1965
|
||||
|
||||
- breaking change: `DC_CHAT_TYPE_VERIFIED_GROUP` replaced by
|
||||
`dc_chat_is_protected()`; also single-chats may be protected now, this may
|
||||
happen over the wire even if the UI do not offer an option for that #1968
|
||||
|
||||
- breaking change: split quotes off message text,
|
||||
UIs should use at least `dc_msg_get_quoted_text()` to show quotes now #1975
|
||||
|
||||
- new api for quote handling: `dc_msg_set_quote()`, `dc_msg_get_quoted_text()`,
|
||||
`dc_msg_get_quoted_msg()` #1975 #1984 #1985 #1987 #1989 #2004
|
||||
|
||||
- require quorum to enable encryption #1946
|
||||
|
||||
- speed up and clean up account creation #1912 #1927 #1960 #1961
|
||||
|
||||
- configure now collects recent contacts and fetches last messages
|
||||
unless disabled by `fetch_existing` config-option #1913 #2003
|
||||
EDIT: `fetch_existing` renamed to `fetch_existing_msgs` in 1.48.0 #2042
|
||||
|
||||
- emit `DC_EVENT_CHAT_MODIFIED` on contact rename
|
||||
and set contact-id on `DC_EVENT_CONTACTS_CHANGED` #1935 #1936 #1937
|
||||
|
||||
- add `dc_set_chat_protection()`; the `protect` parameter in
|
||||
`dc_create_group_chat()` will be removed in an upcoming release;
|
||||
up to then, UIs using the "verified group" paradigm
|
||||
should not use `dc_set_chat_protection()` #1968 #2014 #2001 #2012 #2007
|
||||
|
||||
- remove unneeded `DC_STR_COUNT` #1991
|
||||
|
||||
- mark all failed messages as failed when receiving an NDN #1993
|
||||
|
||||
- check some easy cases for bad system clock and outdated app #1901
|
||||
|
||||
- fix import temporary directory usage #1929
|
||||
|
||||
- fix forcing encryption for reset peers #1998
|
||||
|
||||
- fix: do not allow to save drafts in non-writeable chats #1997
|
||||
|
||||
- fix: do not show HTML if there is no content and there is an attachment #1988
|
||||
|
||||
- fix recovering offline/lost connections, fixes background receive bug #1983
|
||||
|
||||
- fix ordering of accounts returned by `dc_accounts_get_all()` #1909
|
||||
|
||||
- fix whitespace for summaries #1938
|
||||
|
||||
- fix: improve sentbox name guessing #1941
|
||||
|
||||
- fix: avoid manual poll impl for accounts events #1944
|
||||
|
||||
- fix encoding newlines in param as a preparation for storing quotes #1945
|
||||
|
||||
- fix: internal and ffi error handling #1967 #1966 #1959 #1911 #1916 #1917 #1915
|
||||
|
||||
- fix ci #1928 #1931 #1932 #1933 #1934 #1943
|
||||
|
||||
- update provider-database #1940 #2005 #2006
|
||||
|
||||
- update dependencies #1919 #1908 #1950 #1963 #1996 #2010 #2013
|
||||
|
||||
|
||||
## 1.46.0
|
||||
|
||||
- breaking change: `dc_configure()` report errors in
|
||||
`DC_EVENT_CONFIGURE_PROGRESS`: capturing error events is no longer working
|
||||
#1886 #1905
|
||||
|
||||
- breaking change: removed `DC_LP_{IMAP|SMTP}_SOCKET*` from `server_flags`;
|
||||
added `mail_security` and `send_security` using `DC_SOCKET` enum #1835
|
||||
|
||||
- parse multiple servers in Mozilla autoconfig #1860
|
||||
|
||||
- try multiple servers for each protocol #1871
|
||||
|
||||
- do IMAP and SMTP configuration in parallel #1891
|
||||
|
||||
- configuration cleanup and speedup #1858 #1875 #1889 #1904 #1906
|
||||
|
||||
- secure-join cleanup, testing, fixing #1876 #1877 #1887 #1888 #1896 #1899 #1900
|
||||
|
||||
- do not reset peerstate on encrypted messages,
|
||||
ignore reordered autocrypt headers #1885 #1890
|
||||
|
||||
- always sort message replies after parent message #1852
|
||||
|
||||
- add an index to significantly speed up `get_fresh_msg_cnt()` #1881
|
||||
|
||||
- improve mimetype guessing for PDF and many other formats #1857 #1861
|
||||
|
||||
- improve accepting invalid html #1851
|
||||
|
||||
- improve tests, cleanup and ci #1850 #1856 #1859 #1861 #1884 #1894 #1895
|
||||
|
||||
- tweak HELO command #1908
|
||||
|
||||
- make `dc_accounts_get_all()` return accounts sorted #1909
|
||||
|
||||
- fix KML coordinates precision used for location streaming #1872
|
||||
|
||||
- fix cancelling import/export #1855
|
||||
|
||||
|
||||
## 1.45.0
|
||||
|
||||
- add `dc_accounts_t` account manager object and related api functions #1784
|
||||
@@ -704,4 +1088,3 @@
|
||||
For a full list of changes, please see our closed Pull Requests:
|
||||
|
||||
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
|
||||
|
||||
38
CMakeLists.txt
Normal file
38
CMakeLists.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat LANGUAGES C)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.so"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${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(
|
||||
lib_deltachat
|
||||
ALL
|
||||
DEPENDS
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.so"
|
||||
"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})
|
||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
2272
Cargo.lock
generated
2272
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
139
Cargo.toml
139
Cargo.toml
@@ -1,82 +1,90 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.45.0"
|
||||
version = "1.55.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
libc = "0.2.51"
|
||||
pgp = { version = "0.6.0", default-features = false }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1.0.40"
|
||||
async-imap = "0.5.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
async-std-resolver = "0.20.3"
|
||||
async-std = { version = "~1.9.0", features = ["unstable"] }
|
||||
async-tar = "0.3.0"
|
||||
async-trait = "0.1.50"
|
||||
backtrace = "0.3.59"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.1.0"
|
||||
byteorder = "1.3.1"
|
||||
charset = "0.1"
|
||||
chrono = "0.4.6"
|
||||
dirs = { version = "3.0.2", 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.1"
|
||||
futures = "0.3.15"
|
||||
hex = "0.4.0"
|
||||
sha2 = "0.9.0"
|
||||
rand = "0.7.0"
|
||||
smallvec = "1.0.0"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.3.0"
|
||||
itertools = "0.10.0"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2.95"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
mailparse = "0.13.4"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.13.0"
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
async-smtp = "0.3"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
async-imap = "0.4.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-std = { version = "1.6.1", features = ["unstable"] }
|
||||
base64 = "0.12"
|
||||
charset = "0.1"
|
||||
once_cell = "1.4.1"
|
||||
percent-encoding = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4.6"
|
||||
indexmap = "1.3.0"
|
||||
kamadak-exif = "0.5"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.1.6"
|
||||
rusqlite = { version = "0.23", features = ["bundled"] }
|
||||
r2d2_sqlite = "0.16.0"
|
||||
r2d2 = "0.8.5"
|
||||
strum = "0.18.0"
|
||||
strum_macros = "0.18.0"
|
||||
backtrace = "0.3.33"
|
||||
byteorder = "1.3.1"
|
||||
itertools = "0.9.0"
|
||||
quick-xml = "0.18.1"
|
||||
escaper = "0.1.0"
|
||||
bitflags = "1.1.0"
|
||||
sanitize-filename = "0.2.1"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = "0.12.1"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
native-tls = "0.2.3"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
futures = "0.3.4"
|
||||
thiserror = "1.0.14"
|
||||
anyhow = "1.0.28"
|
||||
async-trait = "0.1.31"
|
||||
url = "2.1.1"
|
||||
async-std-resolver = "0.19.5"
|
||||
async-tar = "0.3.0"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
log = {version = "0.4.8", optional = true }
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
dirs = { version = "3.0.1", optional=true }
|
||||
quick-xml = "0.22.0"
|
||||
r2d2 = "0.8.9"
|
||||
r2d2_sqlite = "0.18.0"
|
||||
rand = "0.7.0"
|
||||
regex = "1.4.6"
|
||||
rusqlite = "0.25"
|
||||
rust-hsluv = "0.1.4"
|
||||
rustyline = { version = "8.2.0", optional = true }
|
||||
sanitize-filename = "0.3.0"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9.6"
|
||||
sha2 = "0.9.5"
|
||||
smallvec = "1.0.0"
|
||||
stop-token = "0.2.0"
|
||||
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.25"
|
||||
toml = "0.5.6"
|
||||
|
||||
url = "2.2.2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
zip = "0.5.12"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
pretty_assertions = "0.6.1"
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.7.0"
|
||||
log = "0.4.11"
|
||||
pretty_assertions = "0.7.2"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "0.10"
|
||||
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
|
||||
smol = "0.1.10"
|
||||
proptest = "1.0"
|
||||
tempfile = "3.0"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -95,10 +103,21 @@ path = "examples/repl/main.rs"
|
||||
required-features = ["repl"]
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "create_account"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "contacts"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "search_msgs"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
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"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -2,7 +2,8 @@
|
||||
|
||||
> 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)
|
||||
[](https://circleci.com/gh/deltachat/deltachat-core-rust/)
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
@@ -17,7 +18,7 @@ $ curl https://sh.rustup.rs -sSf | sh
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
|
||||
```
|
||||
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
|
||||
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
|
||||
```
|
||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||
|
||||
@@ -95,7 +96,7 @@ $ cargo build -p deltachat_ffi --release
|
||||
|
||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||
|
||||
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||
SMTP tracing in addition to info messages.
|
||||
|
||||
### Expensive tests
|
||||
@@ -111,11 +112,6 @@ $ 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:
|
||||
@@ -123,7 +119,7 @@ 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/hugot/go-deltachat/)
|
||||
- [Go](https://github.com/deltachat/go-deltachat/)
|
||||
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
|
||||
39
benches/contacts.rs
Normal file
39
benches/contacts.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
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("FakeOS".into(), 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);
|
||||
26
benches/create_account.rs
Normal file
26
benches/create_account.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::task::block_on;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::accounts::Accounts;
|
||||
use tempfile::tempdir;
|
||||
|
||||
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();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("create 1 account", |b| {
|
||||
b.iter(|| block_on(async { create_accounts(black_box(1)).await }))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
29
benches/search_msgs.rs
Normal file
29
benches/search_msgs.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
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("FakeOS".into(), 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,57 +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}
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
||||
|
||||
|
||||
# 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
|
||||
pushd $WHEELHOUSEDIR
|
||||
|
||||
pip3 install -U pip setuptools
|
||||
pip3 install devpi-client
|
||||
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 deltachat*
|
||||
|
||||
popd
|
||||
|
||||
# remove devpi non-master dc indices if thy are too old
|
||||
python ci_scripts/cleanup_devpi_indices.py
|
||||
@@ -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.45.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.45.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,61 +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_PY_LIVECONFIG
|
||||
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,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
# we construct the BUILDDIR such that we can easily share the
|
||||
# CARGO_TARGET_DIR between runs ("..")
|
||||
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
|
||||
cd $BUILDDIR
|
||||
# let's share the target dir with our last run on this branch/job-type
|
||||
# cargo will make sure to block/unblock us properly
|
||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||
export TARGET=x86_64-unknown-linux-gnu
|
||||
export RUSTC_WRAPPER=sccache
|
||||
|
||||
bash ci_scripts/run-rust-test.sh
|
||||
_HERE
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
cd deltachat-ffi
|
||||
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.45.0"
|
||||
version = "1.55.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -20,9 +20,9 @@ libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
serde_json = "1.0"
|
||||
async-std = "1.6.0"
|
||||
anyhow = "1.0.28"
|
||||
thiserror = "1.0.14"
|
||||
async-std = "1.9.0"
|
||||
anyhow = "1.0.40"
|
||||
thiserror = "1.0.25"
|
||||
rand = "0.7.3"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -236,12 +236,6 @@ TAB_SIZE = 4
|
||||
|
||||
ALIASES =
|
||||
|
||||
# This tag can be used to specify a number of word-keyword mappings (TCL only).
|
||||
# A mapping has the form "name=value". For example adding "class=itcl::class"
|
||||
# will allow you to use the command class in the itcl::class meaning.
|
||||
|
||||
TCL_SUBST =
|
||||
|
||||
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
|
||||
# only. Doxygen will then generate output that is more tailored for C. For
|
||||
# instance, some of the names that are used will be different. The list of all
|
||||
|
||||
38
deltachat-ffi/DoxygenLayout.xml
Normal file
38
deltachat-ffi/DoxygenLayout.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.8.20 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title=""/>
|
||||
<tab type="classes" visible="yes" title="">
|
||||
<tab type="classlist" visible="no" title="" intro=""/>
|
||||
<tab type="classindex" visible="no" title=""/>
|
||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="namespaces" visible="yes" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="interfaces" visible="yes" title="">
|
||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="structs" visible="yes" title="">
|
||||
<tab type="structlist" visible="yes" title="" intro=""/>
|
||||
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
</tab>
|
||||
<tab type="exceptions" visible="yes" title="">
|
||||
<tab type="exceptionlist" visible="yes" title="" intro=""/>
|
||||
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="files" visible="yes" title="">
|
||||
<tab type="filelist" visible="yes" title="" intro=""/>
|
||||
<tab type="globals" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro=""/>
|
||||
</navindex>
|
||||
</doxygenlayout>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -241,7 +241,7 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
|
||||
/// requires that the pointer contains valid UTF-8 on Windows.
|
||||
///
|
||||
/// Because this returns a reference the [Path] silce can not outlive
|
||||
/// Because this returns a reference the [Path] slice can not outlive
|
||||
/// the original pointer.
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
|
||||
@@ -9,5 +9,5 @@ license = "MPL-2.0"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.13"
|
||||
syn = "1.0.72"
|
||||
quote = "1.0.2"
|
||||
|
||||
@@ -35,7 +35,11 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
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)?;
|
||||
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
||||
if let Some(value) = num_traits::FromPrimitive::from_i64(inner) {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,69 +5,95 @@ Problem: missing eventual group consistency
|
||||
If group members are concurrently adding new members,
|
||||
the new members will miss each other's additions, example:
|
||||
|
||||
- Alice and Bob are in a two-member group
|
||||
1. Alice and Bob are in a two-member group
|
||||
|
||||
- Alice adds Carol, concurrently Bob adds Doris
|
||||
2. Then Alice adds Carol, while concurrently Bob adds Doris
|
||||
|
||||
- Carol will see a three-member group (Alice, Bob, Carol),
|
||||
Doris will see a different three-member group (Alice, Bob, Doris),
|
||||
and only Alice and Bob will have all four members.
|
||||
Right now, the group has inconsistent memberships:
|
||||
|
||||
Note that for verified groups any mitigation mechanism likely
|
||||
needs to make all clients to know who originally added a member.
|
||||
- Alice and Carol see a (Alice, Carol, Bob) group
|
||||
|
||||
- Bob and Doris see a (Bob, Doris, Alice)
|
||||
|
||||
This then leads to "sender is unknown" messages in the chat,
|
||||
for example when Alice receives a message from Doris,
|
||||
or when Bob receives a message from Carol.
|
||||
|
||||
There are also other sources for group membership inconsistency:
|
||||
|
||||
- leaving/deleting/adding in larger groups, while being offline,
|
||||
increases chances for inconsistent group membership
|
||||
|
||||
- dropped group-membership messages
|
||||
|
||||
- group-membership messages landing in "Spam"
|
||||
|
||||
|
||||
solution: memorize+attach (possible encrypted) chat-meta mime messages
|
||||
----------------------------------------------------------------------
|
||||
Note that all these problems (can) also happen with verified groups,
|
||||
then raising "false alarms" which could lure people to ignore such issues.
|
||||
|
||||
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
|
||||
IOW, it's clear we need to do something about it to improve overall
|
||||
reliability in group-settings.
|
||||
|
||||
|
||||
- All Chat-Group-Member-Added/Removed messages are recorded in their
|
||||
full raw (signed and encrypted) mime-format in the DB
|
||||
|
||||
- If an incoming member-add/member-delete messages has a member list
|
||||
which is, apart from the added/removed member, not consistent
|
||||
with our own view, broadcast a "Chat-Group-Member-Correction" message to
|
||||
all members, attaching the original added/removed mime-message for all mismatching
|
||||
contacts. If we have no relevant add/del information, don't send a
|
||||
correction message out.
|
||||
Solution: replay group modification messages on inconsistencies
|
||||
------------------------------------------------------------------
|
||||
|
||||
- Upong receiving added/removed attachments we don't do the
|
||||
check_consistency+correction message dance.
|
||||
This avoids recursion problems and hard-to-reason-about chatter.
|
||||
For brevity let's abbreviate "group membership modification" as **GMM**.
|
||||
|
||||
Notes:
|
||||
Delta chat has explicit GMM messages, typically encrypted to the group members
|
||||
as seen by the device that sends the GMM. The `Spec <https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members>`_ details the Mime headers and format.
|
||||
|
||||
- mechanism works for both encrypted and unencrypted add/del messages
|
||||
If we detect membership inconsistencies we can resend relevant GMM messages
|
||||
to the respective chat. The receiving devices can process those GMM messages
|
||||
as if it would be an incoming message. If for example they have already seen
|
||||
the Message-ID of the GMM message, they will ignore the message. It's
|
||||
probably useful to record GMM message in their original MIME-format and
|
||||
not invent a new recording format. Few notes on three aspects:
|
||||
|
||||
- we already have a "mime_headers" column in the DB for each incoming message.
|
||||
We could extend it to also include the payload and store mime unconditionally
|
||||
for member-added/removed messages.
|
||||
- **group-membership-tracking**: All valid GMM messages are persisted in
|
||||
their full raw (signed/encrypted?) MIME-format in the DB. Note that GMM messages
|
||||
already are in the msgs table, and there is a mime_header column which we could
|
||||
extend to contain the raw Mime GMM message.
|
||||
|
||||
- multiple member-added/removed messages can be attached in a single
|
||||
correction message
|
||||
- **consistency_checking**: If an incoming GMM has a member list which is
|
||||
not consistent with our own view, broadcast a "Group-Member-Correction"
|
||||
message to all members containing a multipart list of GMMs.
|
||||
|
||||
- it is minimal on the number of overall messages to reach group consistency
|
||||
(best-case: no extra messages, the ABCD case above: max two extra messages)
|
||||
- **correcting_memberships**: Upon receiving a Group-Member-Correction
|
||||
message we pass the contained GMMs to the "incoming mail pipeline"
|
||||
(without **consistency_checking** them, to avoid recursion issues)
|
||||
|
||||
- somewhat backward compatible: older clients will probably ignore
|
||||
messages which are signed by someone who is not the outer From-address.
|
||||
|
||||
- the correction-protocol also helps with dropped messages. If a member
|
||||
did not see a member-added/removed message, the next member add/removed
|
||||
message in the group will likely heal group consistency for this member.
|
||||
Alice/Carol and Bob/Doris getting on the same page
|
||||
++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
Recall that Alice/Carol and Bob/Doris had a differening view of
|
||||
group membership. With the proposed solution, when Bob receives
|
||||
Alice's "Carol added" message, he will notice that Alice (and thus
|
||||
also carol) did not know about Doris. Bob's device sends a
|
||||
"Chat-Group-Member-Correction" message containing his own GMM
|
||||
when adding Doris. Therefore, the group's membership is healed
|
||||
for everyone in a single broadcast message.
|
||||
|
||||
Alice might also send a Group-member-Correction message,
|
||||
so there is a second chance that the group gets to know all GMMs.
|
||||
|
||||
Note, for example, that if for some reason Bobs and Carols provider
|
||||
drop GMM messages between them (spam) that Alice and Doris can heal
|
||||
it by resending GMM messages whenever they detect them to be out of sync.
|
||||
|
||||
- we can quite easily extend the mechanism to also provide the group-avatar or
|
||||
other meta-information.
|
||||
|
||||
Discussions of variants
|
||||
++++++++++++++++++++++++
|
||||
|
||||
- instead of acting on MemberAdded/Removed message we could send
|
||||
corrections for any received message that addresses inconsistent group members but
|
||||
a) this would delay group-membership healing
|
||||
- instead of acting on GMM messages we could send corrections
|
||||
for any received message that addresses inconsistent group members but
|
||||
a) this could delay group-membership healing
|
||||
b) could lead to a lot of members sending corrections
|
||||
c) means we might rely on "To-Addresses" which we also like to strike
|
||||
at least for protected chats.
|
||||
|
||||
- instead of broadcasting correction messages we could only send it to
|
||||
the sender of the inconsistent member-added/removed message.
|
||||
@@ -83,44 +109,3 @@ Discussions of variants
|
||||
while both being in an offline or bad-connection situation).
|
||||
|
||||
|
||||
solution2: repeat member-added/removed messages
|
||||
---------------------------------------------------
|
||||
|
||||
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
|
||||
but keep sending out the old headers until the new protocol is sufficiently deployed.
|
||||
|
||||
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
|
||||
which controls repetition of the signed "add/del e-mail address" payload.
|
||||
|
||||
Example::
|
||||
|
||||
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
|
||||
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
|
||||
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
|
||||
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
|
||||
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
|
||||
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
|
||||
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
|
||||
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
|
||||
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
|
||||
=P6GG
|
||||
|
||||
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
|
||||
Receivers will apply the add/del change to the group-membership,
|
||||
decrease the TTL by 1, and if TTL>0 re-sent the header.
|
||||
|
||||
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
|
||||
This allows to propagate, in a cryptographically secured way,
|
||||
who added a member. This is particularly important for allowing
|
||||
to show in verified groups who added a member (planned).
|
||||
|
||||
Disadvantage to solution 1:
|
||||
|
||||
- requires to specify encoding and precise rules for what/how is signed.
|
||||
|
||||
- causes O(N^2) extra messages
|
||||
|
||||
- Not easily extendable for other things (without introducing a new
|
||||
header / encoding)
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ extern crate dirs;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use anyhow::{bail, ensure, Error};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||
};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -12,15 +14,19 @@ use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::error::Error;
|
||||
use deltachat::export_chat::export_chat_to_zip;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::lot::LotState;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::EventType;
|
||||
use deltachat::{config, provider};
|
||||
use std::fs;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
@@ -116,11 +122,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
real_spec = spec.to_string();
|
||||
context
|
||||
.sql()
|
||||
.set_raw_config(context, "import_spec", Some(&real_spec))
|
||||
.set_raw_config("import_spec", Some(&real_spec))
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config(context, "import_spec").await;
|
||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
return false;
|
||||
@@ -171,8 +177,11 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
let contact = Contact::get_by_id(context, msg.get_from_id())
|
||||
.await
|
||||
.expect("invalid contact");
|
||||
|
||||
let contact_name = contact.get_name();
|
||||
let contact_name = if let Some(name) = msg.get_override_sender_name() {
|
||||
format!("~{}", name)
|
||||
} else {
|
||||
contact.get_display_name().to_string()
|
||||
};
|
||||
let contact_id = contact.get_id();
|
||||
|
||||
let statestr = match msg.get_state() {
|
||||
@@ -193,8 +202,8 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
if msg.is_starred() { "★" } else { "" },
|
||||
if msg.get_from_id() == 1 as libc::c_uint {
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
if msg.get_from_id() == 1 {
|
||||
""
|
||||
} else if msg.get_state() == MessageState::InSeen {
|
||||
"[SEEN]"
|
||||
@@ -252,15 +261,11 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
|
||||
}
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
let mut contacts = contacts.to_vec();
|
||||
if !contacts.contains(&1) {
|
||||
contacts.push(1);
|
||||
}
|
||||
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_name();
|
||||
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 {
|
||||
@@ -289,14 +294,14 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
);
|
||||
}
|
||||
|
||||
println!("Contact#{}: {}{}", contact_id, line, line2);
|
||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,9 +361,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
listarchived\n\
|
||||
chat [<chat-id>|0]\n\
|
||||
createchat <contact-id>\n\
|
||||
createchatbymsg <msg-id>\n\
|
||||
creategroup <name>\n\
|
||||
createverified <name>\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
@@ -369,9 +373,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
dellocations\n\
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
send-garbage\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
@@ -380,15 +384,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
unarchive <chat-id>\n\
|
||||
pin <chat-id>\n\
|
||||
unpin <chat-id>\n\
|
||||
mute <chat-id> [<seconds>]\n\
|
||||
unmute <chat-id>\n\
|
||||
protect <chat-id>\n\
|
||||
unprotect <chat-id>\n\
|
||||
delchat <chat-id>\n\
|
||||
export-chat <chat-id> <destination-file>\n\
|
||||
===========================Contact requests==\n\
|
||||
decidestartchat <msg-id>\n\
|
||||
decideblock <msg-id>\n\
|
||||
decidenotnow <msg-id>\n\
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
html <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
markseen <msg-id>\n\
|
||||
star <msg-id>\n\
|
||||
unstar <msg-id>\n\
|
||||
delmsg <msg-id>\n\
|
||||
===========================Contact commands==\n\
|
||||
listcontacts [<query>]\n\
|
||||
@@ -397,15 +409,18 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
contactinfo <contact-id>\n\
|
||||
delcontact <contact-id>\n\
|
||||
cleanupcontacts\n\
|
||||
block <contact-id>\n\
|
||||
unblock <contact-id>\n\
|
||||
listblocked\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
|
||||
clear -- clear screen\n\
|
||||
exit or quit\n\
|
||||
============================================="
|
||||
@@ -445,20 +460,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"export-backup" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportBackup, Some(&dir)).await?;
|
||||
imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
|
||||
imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?;
|
||||
}
|
||||
"export-keys" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportSelfKeys, Some(&dir)).await?;
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, Some(arg1)).await?;
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?;
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
@@ -502,7 +517,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
context.maybe_network().await;
|
||||
}
|
||||
"housekeeping" => {
|
||||
sql::housekeeping(&context).await;
|
||||
sql::housekeeping(&context).await.ok_or_log(&context);
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
|
||||
@@ -514,9 +529,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 {
|
||||
@@ -527,16 +540,18 @@ 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?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}",
|
||||
"{}#{}: {} [{} fresh] {}{}{}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
chat.get_id().get_fresh_msg_cnt(&context).await,
|
||||
chat.get_id().get_fresh_msg_cnt(&context).await?,
|
||||
if chat.is_muted() { "🔇" } else { "" },
|
||||
match chat.visibility {
|
||||
ChatVisibility::Normal => "",
|
||||
ChatVisibility::Archived => "📦",
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
if chat.is_protected() { "🛡️" } else { "" },
|
||||
);
|
||||
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
|
||||
let statestr = if chat.visibility == ChatVisibility::Archived {
|
||||
@@ -571,7 +586,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await {
|
||||
if location::is_sending_locations_to_chat(&context, None).await {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{} chats", cnt);
|
||||
@@ -591,7 +606,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 {
|
||||
@@ -601,33 +620,41 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
})
|
||||
.collect();
|
||||
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
let contact = Contact::get_by_id(&context, members[0]).await?;
|
||||
contact.get_addr().to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Mailinglist && !members.is_empty() {
|
||||
"mailinglist".to_string()
|
||||
} else {
|
||||
format!("{} member(s)", members.len())
|
||||
};
|
||||
println!(
|
||||
"{}#{}: {} [{}]{}{}",
|
||||
"{}#{}: {} [{}]{}{}{} {}",
|
||||
chat_prefix(sel_chat),
|
||||
sel_chat.get_id(),
|
||||
sel_chat.get_name(),
|
||||
subtitle,
|
||||
if sel_chat.is_muted() { "🔇" } else { "" },
|
||||
if sel_chat.is_sending_locations() {
|
||||
"📍"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(&context).await {
|
||||
match sel_chat.get_profile_image(&context).await? {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => format!(" Icon: {}", icon),
|
||||
_ => " Icon: Err".to_string(),
|
||||
},
|
||||
_ => "".to_string(),
|
||||
},
|
||||
if sel_chat.is_protected() {
|
||||
"🛡️"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
log_msglist(&context, &msglist).await?;
|
||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||
@@ -636,47 +663,77 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!(
|
||||
"{} messages.",
|
||||
sel_chat.get_id().get_msg_cnt(&context).await
|
||||
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: libc::c_int = arg1.parse()?;
|
||||
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
}
|
||||
"createchatbymsg" => {
|
||||
"decidestartchat" | "createchatbymsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
let chat_id = chat::create_by_msg_id(&context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(&context, chat_id).await?;
|
||||
|
||||
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,);
|
||||
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 =
|
||||
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
|
||||
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
||||
|
||||
println!("Group#{} created successfully.", chat_id);
|
||||
}
|
||||
"createverified" => {
|
||||
"createprotected" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
|
||||
let chat_id =
|
||||
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
|
||||
|
||||
println!("VerifiedGroup#{} created successfully.", chat_id);
|
||||
println!("Group#{} created and protected successfully.", chat_id);
|
||||
}
|
||||
"addmember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id_0: libc::c_int = arg1.parse()?;
|
||||
let contact_id_0: u32 = arg1.parse()?;
|
||||
if chat::add_contact_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_0 as u32,
|
||||
contact_id_0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -688,11 +745,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"removemember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id_1: libc::c_int = arg1.parse()?;
|
||||
let contact_id_1: u32 = arg1.parse()?;
|
||||
chat::remove_contact_from_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_1 as u32,
|
||||
contact_id_1,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -718,7 +775,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await;
|
||||
@@ -727,7 +784,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
contacts.len(),
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id()
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -735,15 +792,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"getlocations" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let contact_id = arg1.parse().unwrap_or_default();
|
||||
let contact_id: Option<u32> = arg1.parse().ok();
|
||||
let locations = location::get_range(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
Some(sel_chat.as_ref().unwrap().get_id()),
|
||||
contact_id,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
let default_marker = "-".to_string();
|
||||
for location in &locations {
|
||||
let marker = location.marker.as_ref().unwrap_or(&default_marker);
|
||||
@@ -826,6 +883,22 @@ 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?;
|
||||
}
|
||||
"sendhtml" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No html-file given.");
|
||||
let path: &Path = arg1.as_ref();
|
||||
let html = &*fs::read(&path)?;
|
||||
let html = String::from_utf8_lossy(html);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_html(Some(html.to_string()));
|
||||
msg.set_text(Some(if arg2.is_empty() {
|
||||
path.file_name().unwrap().to_string_lossy().to_string()
|
||||
} else {
|
||||
arg2.to_string()
|
||||
}));
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
@@ -834,15 +907,18 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
let chat = if let Some(ref sel_chat) = sel_chat {
|
||||
sel_chat.get_id()
|
||||
Some(sel_chat.get_id())
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
None
|
||||
};
|
||||
|
||||
let msglist = context.search_msgs(chat, arg1).await;
|
||||
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.");
|
||||
@@ -855,7 +931,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(&context, Some(&mut draft))
|
||||
.await;
|
||||
.await?;
|
||||
println!("Draft saved.");
|
||||
} else {
|
||||
sel_chat
|
||||
@@ -863,7 +939,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(&context, None)
|
||||
.await;
|
||||
.await?;
|
||||
println!("Draft deleted.");
|
||||
}
|
||||
}
|
||||
@@ -876,9 +952,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
msg.set_text(Some(arg1.to_string()));
|
||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
"updatedevicechats" => {
|
||||
context.update_device_chats().await?;
|
||||
}
|
||||
"listmedia" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
@@ -889,7 +962,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Viewtype::Gif,
|
||||
Viewtype::Video,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
println!("{} images or videos: ", images.len());
|
||||
for (i, data) in images.iter().enumerate() {
|
||||
if 0 == i {
|
||||
@@ -910,7 +983,39 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"archive" => ChatVisibility::Archived,
|
||||
"unarchive" | "unpin" => ChatVisibility::Normal,
|
||||
"pin" => ChatVisibility::Pinned,
|
||||
_ => panic!("Unexpected command (This should never happen)"),
|
||||
_ => unreachable!("arg0={:?}", arg0),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"mute" | "unmute" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
let duration = match arg0 {
|
||||
"mute" => {
|
||||
if arg2.is_empty() {
|
||||
MuteDuration::Forever
|
||||
} else {
|
||||
SystemTime::now()
|
||||
.checked_add(Duration::from_secs(arg2.parse()?))
|
||||
.map_or(MuteDuration::Forever, MuteDuration::Until)
|
||||
}
|
||||
}
|
||||
"unmute" => MuteDuration::NotMuted,
|
||||
_ => unreachable!("arg0={:?}", arg0),
|
||||
};
|
||||
chat::set_muted(&context, chat_id, duration).await?;
|
||||
}
|
||||
"protect" | "unprotect" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id
|
||||
.set_protection(
|
||||
&context,
|
||||
match arg0 {
|
||||
"protect" => ProtectionStatus::Protected,
|
||||
"unprotect" => ProtectionStatus::Unprotected,
|
||||
_ => unreachable!("arg0={:?}", arg0),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -920,14 +1025,31 @@ 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?;
|
||||
}
|
||||
"export-chat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
ensure!(!arg2.is_empty(), "Argument <destination file> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
// todo check if path is valid (dest dir exists) and ends in .zip
|
||||
export_chat_to_zip(&context, chat_id, arg2).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;
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
println!("{}", res);
|
||||
}
|
||||
"html" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let file = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(format!("msg-{}.html", id.to_u32()));
|
||||
let html = id.get_html(&context).await?.unwrap_or_default();
|
||||
fs::write(&file, html)?;
|
||||
println!("HTML written to: {:#?}", file);
|
||||
}
|
||||
"listfresh" => {
|
||||
let msglist = context.get_fresh_msgs().await;
|
||||
let msglist = context.get_fresh_msgs().await?;
|
||||
|
||||
log_msglist(&context, &msglist).await?;
|
||||
print!("{} fresh messages.", msglist.len());
|
||||
@@ -947,13 +1069,7 @@ 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;
|
||||
}
|
||||
"star" | "unstar" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut msg_ids = vec![MsgId::new(0); 1];
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::star_msgs(&context, msg_ids, arg0 == "star").await;
|
||||
message::markseen_msgs(&context, msg_ids).await?;
|
||||
}
|
||||
"delmsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
@@ -965,9 +1081,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let contacts = Contact::get_all(
|
||||
&context,
|
||||
if arg0 == "listverified" {
|
||||
0x1 | 0x2
|
||||
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
|
||||
} else {
|
||||
0x2
|
||||
DC_GCL_ADD_SELF
|
||||
},
|
||||
Some(arg1),
|
||||
)
|
||||
@@ -980,7 +1096,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?;
|
||||
}
|
||||
@@ -988,14 +1104,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"contactinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
match contact.get_profile_image(&context).await {
|
||||
match contact.get_profile_image(&context).await? {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
}
|
||||
@@ -1025,6 +1141,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
Contact::delete(&context, arg1.parse()?).await?;
|
||||
}
|
||||
"block" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
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;
|
||||
}
|
||||
"listblocked" => {
|
||||
let contacts = Contact::get_all_blocked(&context).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;
|
||||
@@ -1045,7 +1176,7 @@ 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) {
|
||||
match provider::get_provider_info(arg1).await {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {}:", arg1);
|
||||
println!("status: {}", info.status as u32);
|
||||
@@ -1069,7 +1200,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// println!(
|
||||
// "Sending event {:?}({}), received value {}.",
|
||||
// event, event as usize, r as libc::c_int,
|
||||
// event, event as usize, r,
|
||||
// );
|
||||
// }
|
||||
"fileinfo" => {
|
||||
@@ -1092,11 +1223,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
seconds, device_cnt, server_cnt
|
||||
);
|
||||
}
|
||||
"emptyserver" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <flags> missing");
|
||||
|
||||
message::dc_empty_server(&context, arg1.parse()?).await;
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ 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;
|
||||
@@ -80,11 +81,21 @@ fn receive_event(event: EventType) {
|
||||
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
||||
);
|
||||
}
|
||||
EventType::ConfigureProgress(progress) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||
);
|
||||
EventType::ConfigureProgress { progress, comment } => {
|
||||
if let Some(comment) = comment {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!(
|
||||
"Received CONFIGURE_PROGRESS({} ‰, {})",
|
||||
progress, comment
|
||||
))
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||
);
|
||||
}
|
||||
}
|
||||
EventType::ImexProgress(progress) => {
|
||||
info!(
|
||||
@@ -158,12 +169,14 @@ const DB_COMMANDS: [&str; 9] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 27] = [
|
||||
const CHAT_COMMANDS: [&str; 35] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
"createchat",
|
||||
"createchatbymsg",
|
||||
"decidestartchat",
|
||||
"decideblock",
|
||||
"decidenotnow",
|
||||
"creategroup",
|
||||
"createverified",
|
||||
"addmember",
|
||||
@@ -178,6 +191,7 @@ const CHAT_COMMANDS: [&str; 27] = [
|
||||
"send",
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -185,25 +199,31 @@ const CHAT_COMMANDS: [&str; 27] = [
|
||||
"unarchive",
|
||||
"pin",
|
||||
"unpin",
|
||||
"mute",
|
||||
"unmute",
|
||||
"protect",
|
||||
"unprotect",
|
||||
"delchat",
|
||||
"export-chat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
const MESSAGE_COMMANDS: [&str; 6] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"markseen",
|
||||
"star",
|
||||
"unstar",
|
||||
"delmsg",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 6] = [
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
"listverified",
|
||||
"addcontact",
|
||||
"contactinfo",
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
"block",
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
"getqr",
|
||||
@@ -219,7 +239,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[..],
|
||||
@@ -241,11 +263,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)
|
||||
@@ -266,6 +287,7 @@ impl Highlighter for DcHelper {
|
||||
}
|
||||
|
||||
impl Helper for DcHelper {}
|
||||
impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
@@ -290,7 +312,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
.output_stream(OutputStreamType::Stdout)
|
||||
.build();
|
||||
let mut selected_chat = ChatId::default();
|
||||
let (reader_s, reader_r) = async_std::sync::channel(100);
|
||||
let (reader_s, reader_r) = async_std::channel::bounded(100);
|
||||
let input_loop = async_std::task::spawn_blocking(move || {
|
||||
let h = DcHelper {
|
||||
completer: FilenameCompleter::new(),
|
||||
@@ -299,8 +321,8 @@ 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.");
|
||||
}
|
||||
@@ -313,7 +335,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok(line) => {
|
||||
// TODO: ignore "set mail_pw"
|
||||
rl.add_history_entry(line.as_str());
|
||||
async_std::task::block_on(reader_s.send(line));
|
||||
async_std::task::block_on(reader_s.send(line)).unwrap();
|
||||
}
|
||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||
println!("Exiting...");
|
||||
@@ -372,7 +394,7 @@ async fn handle_cmd(
|
||||
ctx.configure().await?;
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
|
||||
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;
|
||||
if oauth2_url.is_none() {
|
||||
@@ -390,9 +412,8 @@ async fn handle_cmd(
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
if let Some(mut qr) =
|
||||
dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).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")
|
||||
@@ -410,7 +431,7 @@ async fn handle_cmd(
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
dc_join_securejoin(&ctx, arg1).await;
|
||||
dc_join_securejoin(&ctx, arg1).await?;
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tempfile::tempdir;
|
||||
|
||||
use deltachat::chat;
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
@@ -10,7 +10,7 @@ use deltachat::EventType;
|
||||
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
EventType::ConfigureProgress(progress) => {
|
||||
EventType::ConfigureProgress { progress, .. } => {
|
||||
log::info!("progress: {}", progress);
|
||||
}
|
||||
EventType::Info(msg) => {
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
1.51.0
|
||||
------
|
||||
|
||||
- adapt python bindings and APIs to core51 release
|
||||
(see CHANGELOG of https://github.com/deltachat/deltachat-core-rust/blob/1.51.0/CHANGELOG.md#1510
|
||||
for more details on all core changes)
|
||||
|
||||
1.44.0
|
||||
------
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ Installing pre-built packages (Linux-only)
|
||||
========================================================
|
||||
|
||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
|
||||
yourself <sourceinstall>`_.
|
||||
without any "build-from-source" steps.
|
||||
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
|
||||
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
|
||||
then create a fresh Python virtual environment and activate it in your shell::
|
||||
|
||||
virtualenv venv # or: python -m venv
|
||||
@@ -142,7 +142,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 +151,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::
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
setup a python binding development in-place install with cargo debug symbols.
|
||||
@@ -19,8 +19,7 @@ if __name__ == "__main__":
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
|
||||
if target == 'release':
|
||||
extra = " -C lto=on -C embed-bitcode=yes"
|
||||
os.environ["RUSTFLAGS"] = os.environ.get("RUSTFLAGS", "") + extra
|
||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
||||
cmd.append("--release")
|
||||
|
||||
print("running:", " ".join(cmd))
|
||||
|
||||
@@ -18,7 +18,7 @@ def main():
|
||||
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'],
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -77,6 +77,7 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
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()
|
||||
configtracker.wait_finish()
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -145,9 +144,12 @@ def extract_defines(flags):
|
||||
| DC_STR
|
||||
| DC_CONTACT_ID
|
||||
| DC_GCL
|
||||
| DC_GCM
|
||||
| DC_SOCKET
|
||||
| DC_CHAT
|
||||
| DC_PROVIDER
|
||||
| DC_KEY_GEN
|
||||
| DC_IMEX
|
||||
) # End of prefix matching
|
||||
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
||||
) # Close the capturing group, this contains
|
||||
@@ -165,11 +167,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()
|
||||
|
||||
@@ -89,6 +89,22 @@ class Account(object):
|
||||
d[key.lower()] = value
|
||||
return d
|
||||
|
||||
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, string):
|
||||
""" set stock translation string.
|
||||
|
||||
@@ -179,17 +195,6 @@ class Account(object):
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def empty_server_folders(self, inbox=False, mvbox=False):
|
||||
""" empty server folders. """
|
||||
flags = 0
|
||||
if inbox:
|
||||
flags |= const.DC_EMPTY_INBOX
|
||||
if mvbox:
|
||||
flags |= const.DC_EMPTY_MVBOX
|
||||
if not flags:
|
||||
raise ValueError("no flags set")
|
||||
lib.dc_empty_server(self._dc_context, flags)
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
@@ -216,7 +221,7 @@ class Account(object):
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
|
||||
Calling this method will always resulut in the same
|
||||
Calling this method will always result in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
with that e-mail address, it is unblocked and its display
|
||||
`name` is updated if specified.
|
||||
@@ -225,6 +230,19 @@ class Account(object):
|
||||
:param name: (optional) display name for this contact
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
(name, addr) = self.get_contact_addr_and_name(obj, name)
|
||||
name = as_dc_charpointer(name)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj):
|
||||
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):
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("can only add addresses from configured accounts")
|
||||
@@ -240,13 +258,7 @@ class Account(object):
|
||||
|
||||
if name is None and displayname:
|
||||
name = displayname
|
||||
return self._create_contact(addr, name)
|
||||
|
||||
def _create_contact(self, addr, name):
|
||||
addr = as_dc_charpointer(addr)
|
||||
name = as_dc_charpointer(name)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
return Contact(self, contact_id)
|
||||
return (name, addr)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
""" delete a Contact.
|
||||
@@ -274,6 +286,17 @@ class Account(object):
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self):
|
||||
""" return a list of all blocked contacts.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
"""
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_blocked_contacts(self._dc_context),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
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.
|
||||
|
||||
@@ -347,6 +370,9 @@ class Account(object):
|
||||
def get_deaddrop_chat(self):
|
||||
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
||||
|
||||
def get_device_chat(self):
|
||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
||||
|
||||
def get_message_by_id(self, msg_id):
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
@@ -402,23 +428,23 @@ class Account(object):
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
return self._export(path, imex_cmd=1)
|
||||
return self._export(path, imex_cmd=const.DC_IMEX_EXPORT_SELF_KEYS)
|
||||
|
||||
def export_all(self, path):
|
||||
"""return new file containing a backup of all database state
|
||||
(chats, contacts, keys, media, ...). The file is created in the
|
||||
the `path` directory.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
Note that the account has to be stopped; call stop_io() if necessary.
|
||||
"""
|
||||
export_files = self._export(path, 11)
|
||||
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP)
|
||||
if len(export_files) != 1:
|
||||
raise RuntimeError("found more than one new file")
|
||||
return export_files[0]
|
||||
|
||||
def _export(self, path, imex_cmd):
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
self.imex(path, imex_cmd)
|
||||
return imex_tracker.wait_finish()
|
||||
|
||||
def import_self_keys(self, path):
|
||||
@@ -428,7 +454,7 @@ class Account(object):
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
self._import(path, imex_cmd=2)
|
||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_SELF_KEYS)
|
||||
|
||||
def import_all(self, path):
|
||||
"""import delta chat state from the specified backup `path` (a file).
|
||||
@@ -436,21 +462,22 @@ class Account(object):
|
||||
The account must be in unconfigured state for import to attempted.
|
||||
"""
|
||||
assert not self.is_configured(), "cannot import into configured account"
|
||||
self._import(path, imex_cmd=12)
|
||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_BACKUP)
|
||||
|
||||
def _import(self, path, imex_cmd):
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
self.imex(path, imex_cmd)
|
||||
imex_tracker.wait_finish()
|
||||
|
||||
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):
|
||||
"""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.
|
||||
"""
|
||||
self.check_is_configured()
|
||||
if not self.is_started():
|
||||
raise RuntimeError("IO not running, can not send out")
|
||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
||||
if res == ffi.NULL:
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
@@ -557,6 +584,9 @@ class Account(object):
|
||||
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
|
||||
account is started.
|
||||
|
||||
If you are using this from a test, you may want to call
|
||||
wait_all_initial_fetches() afterwards.
|
||||
|
||||
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
|
||||
:raises ConfigureFailed: if the account could not be configured.
|
||||
|
||||
@@ -566,12 +596,34 @@ class Account(object):
|
||||
raise ValueError("account not configured, cannot start io")
|
||||
lib.dc_start_io(self._dc_context)
|
||||
|
||||
def configure(self):
|
||||
def maybe_network(self):
|
||||
"""This function should be called when there is a hint
|
||||
that the network is available again,
|
||||
e.g. as a response to system event reporting network availability.
|
||||
The library will try to send pending messages out immediately.
|
||||
|
||||
Moreover, to have a reliable state
|
||||
when the app comes to foreground with network available,
|
||||
it may be reasonable to call the function also at that moment.
|
||||
|
||||
It is okay to call the function unconditionally when there is
|
||||
network available, however, calling the function
|
||||
_without_ having network may interfere with the backoff algorithm
|
||||
and will led to let the jobs fail faster, with fewer retries
|
||||
and may avoid messages being sent out.
|
||||
|
||||
Finally, if the context was created by the dc_accounts_t account manager
|
||||
(currently not implemented in the Python bindings),
|
||||
use dc_accounts_maybe_network() instead of this function
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure=False):
|
||||
""" 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.
|
||||
"""
|
||||
assert not self.is_configured()
|
||||
assert self.is_configured() == reconfigure
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
configtracker = ConfigureTracker(self)
|
||||
@@ -579,9 +631,6 @@ class Account(object):
|
||||
lib.dc_configure(self._dc_context)
|
||||
return configtracker
|
||||
|
||||
def is_started(self):
|
||||
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
|
||||
|
||||
def wait_shutdown(self):
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
@@ -591,11 +640,8 @@ class Account(object):
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
|
||||
if bool(lib.dc_is_io_running(self._dc_context)):
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
else:
|
||||
self.log("stop_scheduler called on non-running context")
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
|
||||
def shutdown(self):
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
|
||||
@@ -57,10 +57,7 @@ class Chat(object):
|
||||
|
||||
: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) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_VERIFIED_GROUP
|
||||
)
|
||||
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.
|
||||
@@ -85,12 +82,20 @@ class Chat(object):
|
||||
"""
|
||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||
|
||||
def is_verified(self):
|
||||
""" return True if this chat is a verified group.
|
||||
def can_send(self):
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the deaddrop or for the device-talk
|
||||
|
||||
:returns: True if chat is verified, False otherwise.
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
return lib.dc_chat_is_verified(self._dc_chat)
|
||||
return lib.dc_chat_can_send(self._dc_chat)
|
||||
|
||||
def is_protected(self):
|
||||
""" 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):
|
||||
""" return name of this chat.
|
||||
@@ -162,6 +167,13 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat)
|
||||
|
||||
def get_encryption_info(self):
|
||||
"""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):
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
@@ -242,17 +254,19 @@ class Chat(object):
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def prepare_message(self, msg):
|
||||
""" create a new prepared message.
|
||||
""" prepare a message for sending.
|
||||
|
||||
:param msg: the message to be prepared.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
:returns: a :class:`deltachat.message.Message` instance.
|
||||
This is the same object that was passed in, which
|
||||
has been modified with the new state of the core.
|
||||
"""
|
||||
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be prepared")
|
||||
# invalidate passed in message which is not safe to use anymore
|
||||
msg._dc_msg = msg.id = None
|
||||
return Message.from_db(self.account, msg_id)
|
||||
# modify message in place to avoid bad state for the caller
|
||||
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
|
||||
return msg
|
||||
|
||||
def prepare_message_file(self, path, mime_type=None, view_type="file"):
|
||||
""" prepare a message for sending and return the resulting Message instance.
|
||||
@@ -366,7 +380,7 @@ class Chat(object):
|
||||
:raises ValueError: if contact could not be removed
|
||||
:returns: None
|
||||
"""
|
||||
contact = self.account.create_contact(obj)
|
||||
contact = self.account.get_contact(obj)
|
||||
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
|
||||
if ret != 1:
|
||||
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
||||
@@ -490,18 +504,23 @@ 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.utcfromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i)
|
||||
),
|
||||
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
for i in range(lib.dc_array_get_cnt(dc_array))
|
||||
]
|
||||
|
||||
|
||||
class Location:
|
||||
def __init__(self, latitude, longitude, accuracy, timestamp):
|
||||
def __init__(self, latitude, longitude, accuracy, timestamp, marker):
|
||||
assert isinstance(timestamp, datetime)
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.accuracy = accuracy
|
||||
self.timestamp = timestamp
|
||||
self.marker = marker
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
@@ -51,6 +51,18 @@ class Contact(object):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
|
||||
def set_blocked(self, block=True):
|
||||
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, block)
|
||||
|
||||
def block(self):
|
||||
""" Block this contact. Message will not be seen/retrieved from this contact. """
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, True)
|
||||
|
||||
def unblock(self):
|
||||
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||
|
||||
def is_verified(self):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
@@ -65,6 +77,14 @@ class Contact(object):
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get contact status.
|
||||
|
||||
:returns: contact status, empty string if it doesn't exist.
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
|
||||
|
||||
def create_chat(self):
|
||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ def iter_array(dc_array_t, constructor):
|
||||
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
|
||||
|
||||
class DCLot:
|
||||
|
||||
@@ -9,7 +9,9 @@ import ssl
|
||||
import pathlib
|
||||
from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
@@ -24,12 +26,30 @@ def dc_account_extra_configure(account):
|
||||
""" Reset the account (we reuse accounts across tests)
|
||||
and make 'account.direct_imap' available for direct IMAP ops.
|
||||
"""
|
||||
imap = DirectImap(account)
|
||||
if imap.select_config_folder("mvbox"):
|
||||
imap.delete(ALL, expunge=True)
|
||||
assert imap.select_config_folder("inbox")
|
||||
imap.delete(ALL, expunge=True)
|
||||
setattr(account, "direct_imap", imap)
|
||||
try:
|
||||
|
||||
if not hasattr(account, "direct_imap"):
|
||||
imap = DirectImap(account)
|
||||
|
||||
for folder in imap.list_folders():
|
||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
||||
assert imap.select_folder(folder)
|
||||
imap.delete(ALL, expunge=True)
|
||||
else:
|
||||
imap.conn.delete_folder(folder)
|
||||
# We just deleted the folder, so we have to make DC forget about it, too
|
||||
if account.get_config("configured_sentbox_folder") == folder:
|
||||
account.set_config("configured_sentbox_folder", None)
|
||||
if account.get_config("configured_spam_folder") == folder:
|
||||
account.set_config("configured_spam_folder", None)
|
||||
|
||||
setattr(account, "direct_imap", imap)
|
||||
|
||||
except Exception as e:
|
||||
# Uncaught exceptions here would lead to a timeout without any note written to the log
|
||||
# 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
|
||||
@@ -49,18 +69,31 @@ class DirectImap:
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
pw = self.account.get_config("mail_pw")
|
||||
self.conn = IMAPClient(host, ssl_context=ssl_context)
|
||||
|
||||
if security == const.DC_SOCKET_PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.DC_SOCKET_STARTTLS:
|
||||
self.conn = IMAPClient(host, port, ssl=False)
|
||||
self.conn.starttls(ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN:
|
||||
self.conn = IMAPClient(host, port, ssl=False)
|
||||
elif security == const.DC_SOCKET_SSL:
|
||||
self.conn = IMAPClient(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
@@ -75,6 +108,12 @@ class DirectImap:
|
||||
except (OSError, IMAPClientError):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.create_folder(foldername)
|
||||
except imaplib.IMAP4.error as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
|
||||
def select_folder(self, foldername):
|
||||
assert not self._idling
|
||||
return self.conn.select_folder(foldername)
|
||||
@@ -134,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()
|
||||
@@ -225,3 +249,9 @@ class DirectImap:
|
||||
res = self.conn.idle_done()
|
||||
self._idling = False
|
||||
return res
|
||||
|
||||
def append(self, folder, msg):
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(folder, msg)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
from queue import Queue, Empty
|
||||
|
||||
import deltachat
|
||||
@@ -48,6 +49,15 @@ class FFIEventLogger:
|
||||
if self.logid:
|
||||
locname += "-" + self.logid
|
||||
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
||||
|
||||
if os.name == "posix":
|
||||
WARN = '\033[93m'
|
||||
ERROR = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
if message.startswith("DC_EVENT_WARNING"):
|
||||
s = WARN + s + ENDC
|
||||
if message.startswith("DC_EVENT_ERROR"):
|
||||
s = ERROR + s + ENDC
|
||||
with self._loglock:
|
||||
print(s, flush=True)
|
||||
|
||||
@@ -93,6 +103,14 @@ class FFIEventTracker:
|
||||
if rex.search(ev.data2):
|
||||
return ev
|
||||
|
||||
def get_info_regex_groups(self, regex, check_error=True):
|
||||
rex = re.compile(regex)
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO", check_error=check_error)
|
||||
m = rex.match(ev.data2)
|
||||
if m is not None:
|
||||
return m.groups()
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
@@ -111,6 +129,15 @@ class FFIEventTracker:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
|
||||
break
|
||||
|
||||
def wait_all_initial_fetches(self):
|
||||
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
|
||||
so that new messages are not mistaken for old ones:
|
||||
- ac1 and ac2 are created
|
||||
- ac1 sends a message to ac2
|
||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent"""
|
||||
self.get_info_contains("Done fetching existing messages")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
""" wait for and return next incoming message. """
|
||||
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
|
||||
@@ -21,8 +21,8 @@ class Message(object):
|
||||
assert isinstance(dc_msg, ffi.CData)
|
||||
assert dc_msg != ffi.NULL
|
||||
self._dc_msg = dc_msg
|
||||
self.id = lib.dc_msg_get_id(dc_msg)
|
||||
assert self.id is not None and self.id >= 0, repr(self.id)
|
||||
msg_id = self.id
|
||||
assert msg_id is not None and msg_id >= 0, repr(msg_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.account == other.account and self.id == other.id
|
||||
@@ -46,9 +46,13 @@ class Message(object):
|
||||
def new_empty(cls, account, view_type):
|
||||
""" create a non-persistent message.
|
||||
|
||||
:param: view_type is "text", "audio", "video", "file"
|
||||
:param: view_type is the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker"
|
||||
"""
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
if isinstance(view_type, int):
|
||||
view_type_code = view_type
|
||||
else:
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
return Message(account, ffi.gc(
|
||||
lib.dc_msg_new(account._dc_context, view_type_code),
|
||||
lib.dc_msg_unref
|
||||
@@ -68,6 +72,11 @@ class Message(object):
|
||||
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def id(self):
|
||||
"""id of this message. """
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
@@ -77,6 +86,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):
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_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. """
|
||||
@@ -175,6 +201,27 @@ class Message(object):
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
"""Text inside the quote
|
||||
|
||||
:returns: Quoted text"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
"""Quote getter
|
||||
|
||||
:returns: Quoted message, if found in the database"""
|
||||
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
|
||||
if msg:
|
||||
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
|
||||
|
||||
@quote.setter
|
||||
def quote(self, quoted_message):
|
||||
"""Quote setter"""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -191,6 +238,11 @@ class Message(object):
|
||||
return email.message_from_bytes(s)
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
"""Error message"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
def chat(self):
|
||||
"""chat this message was posted in.
|
||||
@@ -201,6 +253,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):
|
||||
"""the name that should be shown over the message instead of the contact display name.
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_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.
|
||||
|
||||
@@ -313,6 +379,10 @@ class Message(object):
|
||||
""" return True if it's a gif message. """
|
||||
return self._view_type == const.DC_MSG_GIF
|
||||
|
||||
def is_sticker(self):
|
||||
""" return True if it's a sticker message. """
|
||||
return self._view_type == const.DC_MSG_STICKER
|
||||
|
||||
def is_audio(self):
|
||||
""" return True if it's an audio message. """
|
||||
return self._view_type == const.DC_MSG_AUDIO
|
||||
@@ -333,21 +403,22 @@ class Message(object):
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
_view_type_mapping = {
|
||||
const.DC_MSG_TEXT: 'text',
|
||||
const.DC_MSG_IMAGE: 'image',
|
||||
const.DC_MSG_GIF: 'gif',
|
||||
const.DC_MSG_AUDIO: 'audio',
|
||||
const.DC_MSG_VIDEO: 'video',
|
||||
const.DC_MSG_FILE: 'file'
|
||||
'text': const.DC_MSG_TEXT,
|
||||
'image': const.DC_MSG_IMAGE,
|
||||
'gif': const.DC_MSG_GIF,
|
||||
'audio': const.DC_MSG_AUDIO,
|
||||
'video': const.DC_MSG_VIDEO,
|
||||
'file': const.DC_MSG_FILE,
|
||||
'sticker': const.DC_MSG_STICKER,
|
||||
}
|
||||
|
||||
|
||||
def get_viewtype_code_from_name(view_type_name):
|
||||
for code, value in _view_type_mapping.items():
|
||||
if value == view_type_name:
|
||||
return code
|
||||
code = _view_type_mapping.get(view_type_name)
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError("message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())))
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -312,24 +312,31 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
self.wait_configure_and_start_io()
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self, move=False, quiet=False):
|
||||
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
|
||||
ac2 = self.get_online_configuring_account(quiet=quiet)
|
||||
self.wait_configure_and_start_io()
|
||||
self.wait_configure_and_start_io([ac1, ac2])
|
||||
return ac1, ac2
|
||||
|
||||
def get_many_online_accounts(self, num, move=True):
|
||||
accounts = [self.get_online_configuring_account(move=move, quiet=True)
|
||||
for i in range(num)]
|
||||
self.wait_configure_and_start_io()
|
||||
self.wait_configure_and_start_io(accounts)
|
||||
for acc in accounts:
|
||||
acc.add_account_plugin(FFIEventLogger(acc))
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_watch, 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.
|
||||
|
||||
`pre_generated_key` only means that a key from python/tests/data/key is
|
||||
used in order to speed things up.
|
||||
"""
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
@@ -342,19 +349,35 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
if hasattr(account, "direct_imap"):
|
||||
# Attach the existing direct_imap. If we did not do this, a new one would be created and
|
||||
# delete existing messages (see dc_account_extra_configure(configure))
|
||||
ac.direct_imap = account.direct_imap
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def wait_configure_and_start_io(self):
|
||||
for acc in self._accounts:
|
||||
if hasattr(acc, "_configtracker"):
|
||||
acc._configtracker.wait_finish()
|
||||
del acc._configtracker
|
||||
acc.set_config("bcc_self", "0")
|
||||
if acc.is_configured() and not acc.is_started():
|
||||
acc.start_io()
|
||||
print("{}: {} account was successfully setup".format(
|
||||
acc.get_config("displayname"), acc.get_config("addr")))
|
||||
def wait_configure_and_start_io(self, accounts=None):
|
||||
if accounts is None:
|
||||
accounts = self._accounts[:]
|
||||
started_accounts = []
|
||||
for acc in accounts:
|
||||
if acc not in started_accounts:
|
||||
self.wait_configure(acc)
|
||||
acc.set_config("bcc_self", "0")
|
||||
if acc.is_configured():
|
||||
acc.start_io()
|
||||
started_accounts.append(acc)
|
||||
print("{}: {} account was started".format(
|
||||
acc.get_config("displayname"), acc.get_config("addr")))
|
||||
for acc in started_accounts:
|
||||
acc._evtracker.wait_all_initial_fetches()
|
||||
|
||||
def wait_configure(self, acc):
|
||||
if hasattr(acc, "_configtracker"):
|
||||
acc._configtracker.wait_finish()
|
||||
acc._evtracker.consume_events()
|
||||
acc.get_device_chat().mark_noticed()
|
||||
del acc._configtracker
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
@@ -391,13 +414,13 @@ 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):
|
||||
@@ -493,6 +516,9 @@ def lp():
|
||||
def step(self, msg):
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg):
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,16 @@ class ImexTracker:
|
||||
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(ffi_event.data2)
|
||||
|
||||
def wait_progress(self, target_progress, progress_upper_limit=1000, progress_timeout=60):
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, int) and ev >= target_progress:
|
||||
assert ev <= progress_upper_limit, \
|
||||
str(ev) + " exceeded upper progress limit " + str(progress_upper_limit)
|
||||
return ev
|
||||
if ev == 0:
|
||||
return None
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
|
||||
@@ -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])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
[tox]
|
||||
# make sure to update environment list in travis.yml and appveyor.yml
|
||||
envlist =
|
||||
py37
|
||||
py3
|
||||
lint
|
||||
auditwheels
|
||||
|
||||
@@ -13,7 +12,6 @@ passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
@@ -47,8 +45,9 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
sphinx
|
||||
breathe
|
||||
# Pin dependencies to the versions which actually work with Python 3.5.
|
||||
sphinx==3.4.3
|
||||
breathe==4.28.0
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.45.0
|
||||
1.50.0
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# Continuous Integration Scripts for Delta Chat
|
||||
|
||||
Continuous Integration, run through CircleCI and an own build machine.
|
||||
Continuous Integration, run through [GitHub
|
||||
Actions](https://docs.github.com/actions),
|
||||
[CircleCI](https://app.circleci.com/) 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 Circle-CI
|
||||
by CircleCI.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
`run-python-test.sh` remotely on the build machine.
|
||||
@@ -26,8 +30,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.
|
||||
@@ -41,6 +45,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
|
||||
75
scripts/ci_upload.sh
Executable file
75
scripts/ci_upload.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/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 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
|
||||
26
scripts/coverage.sh
Executable file
26
scripts/coverage.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if ! which grcov 2>/dev/null 1>&2; 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
|
||||
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.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
16
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
16
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/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.
|
||||
curl "https://static.rust-lang.org/dist/rust-1.52.1-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-1.52.1-$(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-1.52.1-$(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.30.0
|
||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||
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.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
11
scripts/docker-coredeps/deps/build_rust.sh
Executable file
11
scripts/docker-coredeps/deps/build_rust.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain "1.50.0-$(uname -m)-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-$(uname -m)-unknown-linux-gnu/share"
|
||||
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,11 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
# we construct the BUILDDIR such that we can easily share the
|
||||
# CARGO_TARGET_DIR between runs ("..")
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
@@ -17,6 +15,7 @@ git ls-files >.rsynclist
|
||||
# we seem to need .git for setuptools_scm versioning
|
||||
find .git >>.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR-arm64"
|
||||
|
||||
set +x
|
||||
|
||||
@@ -28,24 +27,41 @@ set +x
|
||||
# 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
|
||||
for arch in "" "-arm64"; do
|
||||
|
||||
ssh $SSHTARGET bash -c "cat >${BUILDDIR}${arch}/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
cd $BUILDDIR
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
shopt -s huponexit
|
||||
cd ${BUILDDIR}${arch}
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
|
||||
docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
deltachat/coredeps${arch} scripts/run_all.sh
|
||||
|
||||
_HERE
|
||||
|
||||
done
|
||||
|
||||
echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||
echo "--- Building aarch64 wheels"
|
||||
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR-arm64/exec_docker_run"
|
||||
|
||||
echo "--- Building x86_64 wheels"
|
||||
ssh -o ServerAliveInterval=30 -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/
|
||||
|
||||
# Wheels
|
||||
for arch in "" "-arm64"; do
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||
done
|
||||
|
||||
# Source packages
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||
|
||||
# Documentation
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||
@@ -1,12 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
# we construct the BUILDDIR such that we can easily share the
|
||||
# CARGO_TARGET_DIR between runs ("..")
|
||||
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"
|
||||
|
||||
@@ -20,16 +17,17 @@ 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
|
||||
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
# let's share the target dir with our last run on this branch/job-type
|
||||
# cargo will make sure to block/unblock us properly
|
||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||
export TARGET=release
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
@@ -43,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
|
||||
|
||||
6
scripts/run-doxygen.sh
Executable file
6
scripts/run-doxygen.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
cd deltachat-ffi
|
||||
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen
|
||||
@@ -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}
|
||||
@@ -23,9 +23,6 @@ if [ $? != 0 ]; then
|
||||
fi
|
||||
|
||||
pushd python
|
||||
if [ -e "./liveconfig" -a -z "$DCC_PY_LIVECONFIG" ]; then
|
||||
export DCC_PY_LIVECONFIG=liveconfig
|
||||
fi
|
||||
tox "$@"
|
||||
ret=$?
|
||||
popd
|
||||
@@ -4,6 +4,7 @@
|
||||
# and tox/pytest.
|
||||
|
||||
set -e -x
|
||||
shopt -s huponexit
|
||||
|
||||
# for core-building and python install step
|
||||
export DCC_RS_TARGET=debug
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
shopt -s huponexit
|
||||
|
||||
#export RUST_TEST_THREADS=1
|
||||
export RUST_BACKTRACE=1
|
||||
@@ -19,14 +19,17 @@ 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/cp36-cp36m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.5
|
||||
ln -s /opt/python/cp35-cp35m/bin/python3.5
|
||||
rm -f python3.6
|
||||
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
|
||||
@@ -39,9 +42,8 @@ mkdir -p $TOXWORKDIR
|
||||
# XXX we may switch on some live-tests on for better ensurances
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -63,13 +63,14 @@ def main():
|
||||
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
|
||||
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
||||
|
||||
for line in open("CHANGELOG.md"):
|
||||
## 1.25.0
|
||||
if line.startswith("## "):
|
||||
if line[2:].strip().startswith(newversion):
|
||||
break
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
if "alpha" not in newversion:
|
||||
for line in open("CHANGELOG.md"):
|
||||
## 1.25.0
|
||||
if line.startswith("## "):
|
||||
if line[2:].strip().startswith(newversion):
|
||||
break
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
@@ -81,9 +82,10 @@ 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("")
|
||||
|
||||
|
||||
2
spec.md
2
spec.md
@@ -32,7 +32,7 @@ Messages SHOULD be encrypted by the
|
||||
`prefer-encrypt=mutual` MAY be set by default.
|
||||
|
||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
||||
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
119
src/accounts.rs
119
src/accounts.rs
@@ -1,18 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::task::{Context as TaskContext, Poll};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use async_std::fs;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
use anyhow::{ensure, Context as _};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::Result;
|
||||
use crate::events::Event;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
@@ -20,7 +17,7 @@ use crate::events::Event;
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
accounts: Arc<RwLock<HashMap<u32, Context>>>,
|
||||
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -138,15 +135,25 @@ impl Accounts {
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
// create new account
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
let account_config = self
|
||||
.config
|
||||
.new_account(&self.dir)
|
||||
.await
|
||||
.context("failed to create new account")?;
|
||||
|
||||
let new_dbfile = account_config.dbfile().into();
|
||||
let new_blobdir = Context::derive_blobdir(&new_dbfile);
|
||||
|
||||
let res = {
|
||||
fs::create_dir_all(&account_config.dir).await?;
|
||||
fs::rename(&dbfile, &new_dbfile).await?;
|
||||
fs::rename(&blobdir, &new_blobdir).await?;
|
||||
fs::create_dir_all(&account_config.dir)
|
||||
.await
|
||||
.context("failed to create dir")?;
|
||||
fs::rename(&dbfile, &new_dbfile)
|
||||
.await
|
||||
.context("failed to rename dbfile")?;
|
||||
fs::rename(&blobdir, &new_blobdir)
|
||||
.await
|
||||
.context("failed to rename blobdir")?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
@@ -190,7 +197,7 @@ impl Accounts {
|
||||
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, Some(file)).await {
|
||||
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
|
||||
Ok(_) => Ok(id),
|
||||
Err(err) => {
|
||||
// remove temp account
|
||||
@@ -225,61 +232,42 @@ impl Accounts {
|
||||
|
||||
/// Unified event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
let emitters = self
|
||||
let emitters: Vec<_> = self
|
||||
.accounts
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(id, a)| EmitterWrapper {
|
||||
id: *id,
|
||||
emitter: a.get_event_emitter(),
|
||||
done: AtomicBool::new(false),
|
||||
})
|
||||
.map(|(_id, a)| a.get_event_emitter())
|
||||
.collect();
|
||||
|
||||
EventEmitter(emitters)
|
||||
EventEmitter(futures::stream::select_all(emitters))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
|
||||
|
||||
impl EventEmitter {
|
||||
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub fn recv_sync(&self) -> Option<Event> {
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// Async recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub async fn recv(&self) -> Option<Event> {
|
||||
futures::future::poll_fn(|cx| Pin::new(self).recv_poll(cx)).await
|
||||
}
|
||||
|
||||
fn recv_poll(self: Pin<&Self>, _cx: &mut TaskContext<'_>) -> Poll<Option<Event>> {
|
||||
for e in &*self.0 {
|
||||
if e.done.load(Ordering::Acquire) {
|
||||
// skip emitters that are already done
|
||||
continue;
|
||||
}
|
||||
|
||||
match e.emitter.try_recv() {
|
||||
Ok(event) => return Poll::Ready(Some(event)),
|
||||
Err(async_std::sync::TryRecvError::Disconnected) => {
|
||||
e.done.store(true, Ordering::Release);
|
||||
}
|
||||
Err(async_std::sync::TryRecvError::Empty) => {}
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventEmitter(Vec<EmitterWrapper>);
|
||||
impl async_std::stream::Stream for EventEmitter {
|
||||
type Item = Event;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EmitterWrapper {
|
||||
id: u32,
|
||||
emitter: crate::events::EventEmitter,
|
||||
done: AtomicBool,
|
||||
fn poll_next(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
@@ -342,9 +330,9 @@ impl Config {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self) -> Result<HashMap<u32, Context>> {
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let cfg = &*self.inner.read().await;
|
||||
let mut accounts = HashMap::with_capacity(cfg.accounts.len());
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &cfg.accounts {
|
||||
let ctx = Context::new(
|
||||
cfg.os_name.clone(),
|
||||
@@ -368,7 +356,6 @@ impl Config {
|
||||
|
||||
inner.accounts.push(AccountConfig {
|
||||
id,
|
||||
name: String::new(),
|
||||
dir: target_dir.into(),
|
||||
uuid,
|
||||
});
|
||||
@@ -435,8 +422,6 @@ impl Config {
|
||||
pub struct AccountConfig {
|
||||
/// Unique id.
|
||||
pub id: u32,
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Root directory for all data for this account.
|
||||
pub dir: std::path::PathBuf,
|
||||
pub uuid: Uuid,
|
||||
@@ -527,7 +512,29 @@ mod tests {
|
||||
let ctx = accounts.get_selected_account().await;
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr).await.unwrap()
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that accounts are sorted by ID.
|
||||
#[async_std::test]
|
||||
async fn test_accounts_sorted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
for (i, expected_id) in (1..10).enumerate() {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use crate::contact::*;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
461
src/blob.rs
461
src/blob.rs
@@ -1,5 +1,6 @@
|
||||
//! # Blob directory management
|
||||
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
|
||||
@@ -7,14 +8,21 @@ 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;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::constants::{
|
||||
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
|
||||
WORSE_IMAGE_SIZE,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::events::EventType;
|
||||
use crate::message;
|
||||
|
||||
@@ -50,12 +58,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(blobdir, &stem, &ext).await?;
|
||||
file.write_all(data)
|
||||
.await
|
||||
.map_err(|err| BlobError::WriteFailure {
|
||||
@@ -63,6 +71,12 @@ impl<'a> BlobObject<'a> {
|
||||
blobname: name.clone(),
|
||||
cause: err.into(),
|
||||
})?;
|
||||
|
||||
// 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 )
|
||||
let _ = file.flush().await;
|
||||
|
||||
let blob = BlobObject {
|
||||
blobdir,
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
@@ -123,18 +137,17 @@ 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?;
|
||||
let name_for_err = name.clone();
|
||||
@@ -147,10 +160,14 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
// workaround, see create() for details
|
||||
let _ = dst_file.flush().await;
|
||||
|
||||
let blob = BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
@@ -177,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
|
||||
}
|
||||
@@ -207,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())
|
||||
}
|
||||
@@ -367,26 +380,25 @@ impl<'a> BlobObject<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
pub 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 = 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,
|
||||
})?;
|
||||
|
||||
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
|
||||
return Ok(());
|
||||
let img_wh =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
|
||||
MediaQuality::Worse => WORSE_AVATAR_SIZE,
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
|
||||
|
||||
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(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -398,40 +410,140 @@ impl<'a> BlobObject<'a> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
|
||||
let img_wh =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
|
||||
MediaQuality::Worse => WORSE_IMAGE_SIZE,
|
||||
};
|
||||
|
||||
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,
|
||||
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 img_wh = if MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
|
||||
.unwrap_or_default()
|
||||
== MediaQuality::Balanced
|
||||
{
|
||||
BALANCED_IMAGE_SIZE
|
||||
} else {
|
||||
WORSE_IMAGE_SIZE
|
||||
};
|
||||
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
img.write_to(encoded, image::ImageFormat::Jpeg)?;
|
||||
Ok(())
|
||||
}
|
||||
fn encode_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;
|
||||
|
||||
if img.width() <= img_wh && img.height() <= img_wh {
|
||||
return Ok(());
|
||||
let do_scale =
|
||||
exceeds_width || encode_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_rotate {
|
||||
img = match orientation {
|
||||
Ok(90) => img.rotate90(),
|
||||
Ok(180) => img.rotate180(),
|
||||
Ok(270) => img.rotate270(),
|
||||
_ => img,
|
||||
}
|
||||
}
|
||||
|
||||
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 encode_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 {
|
||||
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(),
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut img = img.thumbnail(img_wh, img_wh);
|
||||
match self.get_exif_orientation(context) {
|
||||
Ok(90) => img = img.rotate90(),
|
||||
Ok(180) => img = img.rotate180(),
|
||||
Ok(270) => img = img.rotate270(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
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(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
Ok(changed_name)
|
||||
}
|
||||
|
||||
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
|
||||
@@ -495,75 +607,67 @@ pub enum BlobError {
|
||||
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||
#[error("Blob has a badname {}", .blobname.display())]
|
||||
WrongName { blobname: PathBuf },
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::File;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
|
||||
let fname = t.ctx.get_blobdir().join("foo");
|
||||
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
|
||||
let fname = t.get_blobdir().join("foo");
|
||||
let data = fs::read(fname).await.unwrap();
|
||||
assert_eq!(data, b"hello");
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
|
||||
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
|
||||
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_lowercase_ext() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let blob = BlobObject::create(&t, "foo.TXT", b"hello").await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_as_file_name() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||
assert_eq!(blob.as_file_name(), "foo.txt");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_as_rel_path() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_suffix() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||
assert_eq!(blob.suffix(), Some("txt"));
|
||||
let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
|
||||
let blob = BlobObject::create(&t, "bar", b"world").await.unwrap();
|
||||
assert_eq!(blob.suffix(), None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_dup() {
|
||||
let t = TestContext::new().await;
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let foo_path = t.ctx.get_blobdir().join("foo.txt");
|
||||
BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||
let foo_path = t.get_blobdir().join("foo.txt");
|
||||
assert!(foo_path.exists().await);
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"world")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
|
||||
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
|
||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||
while let Some(dirent) = dir.next().await {
|
||||
let fname = dirent.unwrap().file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
@@ -579,15 +683,15 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_double_ext_preserved() {
|
||||
let t = TestContext::new().await;
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
|
||||
BlobObject::create(&t, "foo.tar.gz", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
|
||||
let foo_path = t.get_blobdir().join("foo.tar.gz");
|
||||
assert!(foo_path.exists().await);
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
|
||||
BlobObject::create(&t, "foo.tar.gz", b"world")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
|
||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||
while let Some(dirent) = dir.next().await {
|
||||
let fname = dirent.unwrap().file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
@@ -605,7 +709,7 @@ mod tests {
|
||||
async fn test_create_long_names() {
|
||||
let t = TestContext::new().await;
|
||||
let s = "1".repeat(150);
|
||||
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
|
||||
let blob = BlobObject::create(&t, &s, b"data").await.unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
assert!(blobname.len() < 128);
|
||||
}
|
||||
@@ -615,14 +719,16 @@ 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.ctx, &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.ctx, &whoops).await.is_err());
|
||||
let whoops = t.ctx.get_blobdir().join("whoops");
|
||||
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
||||
.await
|
||||
.is_err());
|
||||
let whoops = t.get_blobdir().join("whoops");
|
||||
assert!(!whoops.exists().await);
|
||||
}
|
||||
|
||||
@@ -632,14 +738,16 @@ 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.ctx, &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");
|
||||
|
||||
let src_int = t.ctx.get_blobdir().join("internal");
|
||||
let src_int = t.get_blobdir().join("internal");
|
||||
fs::write(&src_int, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
@@ -649,7 +757,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.ctx, &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"
|
||||
@@ -685,7 +795,7 @@ mod tests {
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||
assert!(stem.contains("foo"));
|
||||
assert!(!stem.contains("?"));
|
||||
assert!(!stem.contains('?'));
|
||||
assert_eq!(ext, ".bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||
@@ -698,10 +808,111 @@ mod tests {
|
||||
assert!(!stem.contains("ignored"));
|
||||
assert!(stem.contains("this"));
|
||||
assert!(stem.contains("forbidden"));
|
||||
assert!(!stem.contains("/"));
|
||||
assert!(!stem.contains("\\"));
|
||||
assert!(!stem.contains(":"));
|
||||
assert!(!stem.contains("*"));
|
||||
assert!(!stem.contains("?"));
|
||||
assert!(!stem.contains('/'));
|
||||
assert!(!stem.contains('\\'));
|
||||
assert!(!stem.contains(':'));
|
||||
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");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_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()));
|
||||
}
|
||||
}
|
||||
|
||||
2683
src/chat.rs
2683
src/chat.rs
File diff suppressed because it is too large
Load Diff
299
src/chatlist.rs
299
src/chatlist.rs
@@ -1,15 +1,19 @@
|
||||
//! # Chat list module
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use anyhow::{bail, ensure, Result};
|
||||
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::constants::{
|
||||
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
|
||||
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT,
|
||||
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
|
||||
};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::stock::StockMessage;
|
||||
use crate::stock_str;
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
///
|
||||
@@ -58,9 +62,8 @@ impl Chatlist {
|
||||
/// messages from addresses that have no relationship to the configured account.
|
||||
/// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
|
||||
/// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
|
||||
/// and offers the options "Yes" (call dc_create_chat_by_msg_id()), "Never" (call dc_block_contact())
|
||||
/// or "Not now".
|
||||
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
||||
/// and offers the options "Start chat", "Block" and "Not now";
|
||||
/// The decision should be passed to dc_decide_on_contact_request().
|
||||
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
||||
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
|
||||
/// "Show archived chats", if the user clicks this item, the UI should show a
|
||||
@@ -118,10 +121,9 @@ impl Chatlist {
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
@@ -228,10 +230,9 @@ impl Chatlist {
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let sort_id_up = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
@@ -256,13 +257,11 @@ impl Chatlist {
|
||||
process_rows,
|
||||
).await?;
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
||||
if let Some(last_deaddrop_fresh_msg_id) =
|
||||
get_last_deaddrop_fresh_msg(context).await?
|
||||
{
|
||||
if !flag_for_forwarding {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
|
||||
}
|
||||
}
|
||||
add_archived_link_item = true;
|
||||
@@ -270,11 +269,11 @@ impl Chatlist {
|
||||
ids
|
||||
};
|
||||
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
|
||||
}
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
|
||||
}
|
||||
|
||||
Ok(Chatlist { ids })
|
||||
@@ -359,30 +358,29 @@ impl Chatlist {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut lastcontact = None;
|
||||
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
|
||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||
}
|
||||
|
||||
Some(lastmsg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (lastmsg, lastcontact) =
|
||||
if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
|
||||
if lastmsg.from_id == DC_CONTACT_ID_SELF {
|
||||
(Some(lastmsg), None)
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
let lastcontact =
|
||||
Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||
(Some(lastmsg), lastcontact)
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if chat.id.is_archived_link() {
|
||||
ret.text2 = None;
|
||||
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
||||
{
|
||||
ret.text2 = Some(
|
||||
context
|
||||
.stock_str(StockMessage::NoMessages)
|
||||
.await
|
||||
.to_string(),
|
||||
);
|
||||
ret.text2 = Some(stock_str::no_messages(context).await);
|
||||
} else {
|
||||
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
|
||||
.await;
|
||||
@@ -397,25 +395,23 @@ impl Chatlist {
|
||||
}
|
||||
|
||||
/// Returns the number of archived chats
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||
context
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
|
||||
// We have an index over the state-column, this should be
|
||||
// sufficient as there are typically only few fresh messages.
|
||||
context
|
||||
let id = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
@@ -428,30 +424,37 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
),
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::*;
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message;
|
||||
use crate::message::ContactRequestDecision;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_try_load() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
|
||||
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
|
||||
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id3);
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
@@ -460,26 +463,24 @@ mod tests {
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
// check chatlist query and archive functionality
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.set_visibility(&t, ChatVisibility::Archived)
|
||||
.await
|
||||
.ok();
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
@@ -488,23 +489,23 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
t.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert!(chats.len() == 3);
|
||||
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None)
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
assert!(Chat::load_from_db(&t, chats.get_chat_id(0))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
@@ -513,49 +514,177 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
t.update_device_chats().await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
|
||||
t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
|
||||
t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_single_chat() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// receive a one-to-one-message, accept contact request
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.get_chat_id(), DC_CHAT_ID_DEADDROP);
|
||||
|
||||
let chat_id =
|
||||
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
|
||||
.await
|
||||
.unwrap();
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = *contacts.first().unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Authname");
|
||||
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Nickname");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
// revert contact to authname, this again changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Authname");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// receive a one-to-one-message without authname set, accept contact request
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg5678@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat_id =
|
||||
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
|
||||
.await
|
||||
.unwrap();
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = *contacts.first().unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "bob@example.org");
|
||||
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Nickname");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
|
||||
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
|
||||
// revert name change, this again changes the name of the one-to-one-chat to the email-address
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "bob@example.org");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// finally, also check that a simple substring-search is working with email-addresses
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summary_unwrap() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
|
||||
chat_id1.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
let summary = chats.get_summary(&t.ctx, 0, None).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
let summary = chats.get_summary(&t, 0, None).await;
|
||||
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
}
|
||||
|
||||
62
src/color.rs
Normal file
62
src/color.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! Implementation of Consistent Color Generation
|
||||
//!
|
||||
//! Consistent Color Generation is defined in XEP-0392.
|
||||
//!
|
||||
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
|
||||
//! corresponding settings.
|
||||
use hsluv::hsluv_to_rgb;
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Converts an identifier to Hue angle.
|
||||
fn str_to_angle(s: &str) -> f64 {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha1::digest(bytes);
|
||||
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
|
||||
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
|
||||
f64::from(checksum) / 65536.0 * 360.0
|
||||
}
|
||||
|
||||
/// Converts RGB tuple to a 24-bit number.
|
||||
///
|
||||
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
|
||||
/// most significant bits corresponding to the red color.
|
||||
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
|
||||
let r = ((r * 256.0) as u32).min(255);
|
||||
let g = ((g * 256.0) as u32).min(255);
|
||||
let b = ((b * 256.0) as u32).min(255);
|
||||
65536 * r + 256 * g + b
|
||||
}
|
||||
|
||||
/// Converts an identifier to RGB color.
|
||||
///
|
||||
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
|
||||
/// half (50.0) to make colors suitable both for light and dark theme.
|
||||
pub(crate) fn str_to_color(s: &str) -> u32 {
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_str_to_angle() {
|
||||
// Test against test vectors from
|
||||
// https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd
|
||||
assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
|
||||
assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
|
||||
assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);
|
||||
assert!((str_to_angle("council") - 359.994507).abs() < 1e-6);
|
||||
assert!((str_to_angle("Board") - 171.430664).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_u32() {
|
||||
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
|
||||
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
|
||||
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
|
||||
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
|
||||
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
|
||||
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
|
||||
}
|
||||
}
|
||||
256
src/config.rs
256
src/config.rs
@@ -1,5 +1,6 @@
|
||||
//! # Key-value configuration management
|
||||
|
||||
use anyhow::Result;
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
@@ -7,11 +8,13 @@ use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::stock_str;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -24,17 +27,16 @@ pub enum Config {
|
||||
MailUser,
|
||||
MailPw,
|
||||
MailPort,
|
||||
MailSecurity,
|
||||
ImapCertificateChecks,
|
||||
SendServer,
|
||||
SendUser,
|
||||
SendPw,
|
||||
SendPort,
|
||||
SendSecurity,
|
||||
SmtpCertificateChecks,
|
||||
ServerFlags,
|
||||
|
||||
#[strum(props(default = "INBOX"))]
|
||||
ImapFolder,
|
||||
|
||||
Displayname,
|
||||
Selfstatus,
|
||||
Selfavatar,
|
||||
@@ -60,12 +62,21 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxMove,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
|
||||
|
||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// If set to "1", on the first time `start_io()` is called after configuring,
|
||||
/// the newest existing messages are fetched.
|
||||
/// Existing recipients are added to the contact database regardless of this setting.
|
||||
#[strum(props(default = "1"))]
|
||||
FetchExistingMsgs,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
@@ -107,6 +118,9 @@ pub enum Config {
|
||||
ConfiguredInboxFolder,
|
||||
ConfiguredMvboxFolder,
|
||||
ConfiguredSentboxFolder,
|
||||
ConfiguredSpamFolder,
|
||||
ConfiguredTimestamp,
|
||||
ConfiguredProvider,
|
||||
Configured,
|
||||
|
||||
#[strum(serialize = "sys.version")]
|
||||
@@ -118,113 +132,151 @@ pub enum Config {
|
||||
#[strum(serialize = "sys.config_keys")]
|
||||
SysConfigKeys,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
Bot,
|
||||
|
||||
/// Whether we send a warning if the password is wrong (set to false when we send a warning
|
||||
/// because we do not want to send a second warning)
|
||||
#[strum(props(default = "0"))]
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn config_exists(&self, key: Config) -> bool {
|
||||
self.sql.get_raw_config(self, key).await.is_some()
|
||||
pub async fn config_exists(&self, key: Config) -> Result<bool> {
|
||||
Ok(self.sql.get_raw_config(key).await?.is_some())
|
||||
}
|
||||
|
||||
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
|
||||
pub async fn get_config(&self, key: Config) -> Option<String> {
|
||||
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
|
||||
let value = match key {
|
||||
Config::Selfavatar => {
|
||||
let rel_path = self.sql.get_raw_config(self, key).await;
|
||||
let rel_path = self.sql.get_raw_config(key).await?;
|
||||
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(self, key).await,
|
||||
_ => self.sql.get_raw_config(key).await?,
|
||||
};
|
||||
|
||||
if value.is_some() {
|
||||
return value;
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
// Default values
|
||||
match key {
|
||||
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()),
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
|
||||
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
|
||||
_ => Ok(key.get_str("default").map(|s| s.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_config_int(&self, key: Config) -> i32 {
|
||||
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default()
|
||||
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_config_bool(&self, key: Config) -> bool {
|
||||
self.get_config_int(key).await != 0
|
||||
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
|
||||
Ok(self.get_config_int(key).await? != 0)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub async fn get_config_delete_server_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteServerAfter).await {
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x as i64),
|
||||
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
|
||||
match self.get_config_int(Config::DeleteServerAfter).await? {
|
||||
0 => Ok(None),
|
||||
1 => Ok(Some(0)),
|
||||
x => Ok(Some(x as i64)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the configured provider, as saved in the `configured_provider` value.
|
||||
///
|
||||
/// The provider is determined by `get_provider_info()` during configuration and then saved
|
||||
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
|
||||
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
|
||||
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
|
||||
return Ok(get_provider_by_id(&cfg));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(x)` means delete
|
||||
/// after `x` seconds.
|
||||
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter).await {
|
||||
0 => None,
|
||||
x => Some(x as i64),
|
||||
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter).await? {
|
||||
0 => Ok(None),
|
||||
x => Ok(Some(x as i64)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||
.await?;
|
||||
self.sql
|
||||
.set_raw_config_bool(self, "attach_selfavatar", true)
|
||||
.set_raw_config_bool("attach_selfavatar", true)
|
||||
.await?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let blob = BlobObject::new_from_path(&self, value).await?;
|
||||
blob.recode_to_avatar_size(self)?;
|
||||
self.sql
|
||||
.set_raw_config(self, key, Some(blob.as_name()))
|
||||
.await
|
||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => self.sql.set_raw_config(self, key, None).await,
|
||||
}
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
let def = self.stock_str(StockMessage::StatusLine).await;
|
||||
let def = stock_str::status_line(self).await;
|
||||
let val = if value.is_none() || value.unwrap() == def {
|
||||
None
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
self.sql.set_raw_config(self, key, val).await
|
||||
self.sql.set_raw_config(key, val).await?;
|
||||
Ok(())
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
let ret = self
|
||||
.sql
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
@@ -232,9 +284,32 @@ impl Context {
|
||||
});
|
||||
ret
|
||||
}
|
||||
_ => self.sql.set_raw_config(self, key, value).await,
|
||||
Config::Displayname => {
|
||||
let value = value.map(improve_single_line_input);
|
||||
self.sql.set_raw_config(key, value.as_deref()).await?;
|
||||
Ok(())
|
||||
}
|
||||
Config::DeleteServerAfter => {
|
||||
let ret = self
|
||||
.sql
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
job::schedule_resync(self).await;
|
||||
ret
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, if value { Some("1") } else { None })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all available configuration keys concated together.
|
||||
@@ -256,12 +331,8 @@ mod tests {
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::constants;
|
||||
use crate::constants::AVATAR_SIZE;
|
||||
use crate::test_utils::*;
|
||||
use image::GenericImageView;
|
||||
use crate::test_utils::TestContext;
|
||||
use num_traits::FromPrimitive;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
@@ -275,104 +346,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_prop() {
|
||||
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
|
||||
}
|
||||
|
||||
#[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)
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.unwrap();
|
||||
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
|
||||
assert!(!avatar_blob.exists().await);
|
||||
t.ctx
|
||||
.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.ctx.get_config(Config::Selfavatar).await;
|
||||
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(), AVATAR_SIZE);
|
||||
assert_eq!(img.height(), AVATAR_SIZE);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
File::create(&avatar_src)
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.unwrap();
|
||||
|
||||
let img = image::open(&avatar_src).unwrap();
|
||||
assert_eq!(img.width(), 900);
|
||||
assert_eq!(img.height(), 900);
|
||||
|
||||
t.ctx
|
||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
|
||||
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
|
||||
|
||||
let img = image::open(avatar_src).unwrap();
|
||||
assert_eq!(img.width(), AVATAR_SIZE);
|
||||
assert_eq!(img.height(), 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)
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.unwrap();
|
||||
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
|
||||
assert!(!avatar_blob.exists().await);
|
||||
t.ctx
|
||||
.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.ctx.get_config(Config::Selfavatar).await;
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_media_quality_config_option() {
|
||||
let t = TestContext::new().await;
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
|
||||
assert_eq!(media_quality, 0);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Balanced);
|
||||
|
||||
t.ctx
|
||||
.set_config(Config::MediaQuality, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
|
||||
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
|
||||
assert_eq!(media_quality, 1);
|
||||
assert_eq!(constants::MediaQuality::Worse as i32, 1);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
|
||||
641
src/configure.rs
Normal file
641
src/configure.rs
Normal file
@@ -0,0 +1,641 @@
|
||||
//! Email accounts autoconfiguration process module
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod read_url;
|
||||
mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use itertools::Itertools;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::imap::Imap;
|
||||
use crate::login_param::{LoginParam, ServerLoginParam};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::dc_get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock_str;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{config::Config, dc_tools::time};
|
||||
use crate::{
|
||||
constants::{Viewtype, DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2},
|
||||
job,
|
||||
};
|
||||
use crate::{context::Context, param::Params};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
$progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress {
|
||||
progress: $progress,
|
||||
comment: $comment,
|
||||
});
|
||||
};
|
||||
($context:tt, $progress:expr) => {
|
||||
progress!($context, $progress, None);
|
||||
};
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Checks if the context is already configured.
|
||||
pub async fn is_configured(&self) -> Result<bool> {
|
||||
self.sql
|
||||
.get_raw_config_bool("configured")
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
ensure!(
|
||||
!self.scheduler.read().await.is_running(),
|
||||
"cannot configure, already running"
|
||||
);
|
||||
ensure!(
|
||||
self.sql.is_open().await,
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
let res = self
|
||||
.inner_configure()
|
||||
.race(cancel_channel.recv().map(|_| {
|
||||
progress!(self, 0);
|
||||
Ok(())
|
||||
}))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::from_database(self, "").await?;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !self.config_exists(def.key).await? {
|
||||
info!(self, "apply config_defaults {}={}", def.key, def.value);
|
||||
self.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
info!(
|
||||
self,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(self, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!(self, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match success {
|
||||
Ok(_) => {
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
progress!(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
progress!(
|
||||
self,
|
||||
0,
|
||||
Some(
|
||||
stock_str::configuration_failed(
|
||||
self,
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
format!("{:#}", err),
|
||||
)
|
||||
.await
|
||||
)
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
// Check basic settings.
|
||||
ensure!(!param.addr.is_empty(), "Please enter an email address.");
|
||||
|
||||
// Only check for IMAP password, SMTP password is an "advanced" setting.
|
||||
ensure!(!param.imap.password.is_empty(), "Please enter a password.");
|
||||
if param.smtp.password.is_empty() {
|
||||
param.smtp.password = param.imap.password.clone()
|
||||
}
|
||||
|
||||
// Normalize authentication flags.
|
||||
let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 {
|
||||
DC_LP_AUTH_OAUTH2 => true,
|
||||
DC_LP_AUTH_NORMAL => false,
|
||||
_ => false,
|
||||
};
|
||||
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
|
||||
param.server_flags |= if oauth2 {
|
||||
DC_LP_AUTH_OAUTH2 as i32
|
||||
} else {
|
||||
DC_LP_AUTH_NORMAL as i32
|
||||
};
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
// Step 1: Load the parameters and check email-address and password
|
||||
|
||||
if oauth2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
.await?
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
info!(ctx, "Authorized address is {}", oauth2_addr);
|
||||
param.addr = oauth2_addr;
|
||||
ctx.sql
|
||||
.set_raw_config("addr", Some(param.addr.as_str()))
|
||||
.await?;
|
||||
}
|
||||
progress!(ctx, 20);
|
||||
}
|
||||
// no oauth? - just continue it's no error
|
||||
|
||||
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// Step 2: Autoconfig
|
||||
progress!(ctx, 200);
|
||||
|
||||
let param_autoconfig;
|
||||
if param.imap.server.is_empty()
|
||||
&& param.imap.port == 0
|
||||
&& param.imap.security == Socket::Automatic
|
||||
&& param.imap.user.is_empty()
|
||||
&& param.smtp.server.is_empty()
|
||||
&& param.smtp.port == 0
|
||||
&& param.smtp.security == Socket::Automatic
|
||||
&& param.smtp.user.is_empty()
|
||||
{
|
||||
// no advanced parameters entered by the user: query provider-database or do Autoconfig
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m_domain).await {
|
||||
param.provider = Some(provider);
|
||||
match provider.status {
|
||||
provider::Status::Ok | provider::Status::Preparation => {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "offline autoconfig found, but no servers defined");
|
||||
param_autoconfig = None;
|
||||
} else {
|
||||
info!(ctx, "offline autoconfig found");
|
||||
let servers = provider
|
||||
.server
|
||||
.iter()
|
||||
.map(|s| ServerParams {
|
||||
protocol: s.protocol,
|
||||
socket: s.socket,
|
||||
hostname: s.hostname.to_string(),
|
||||
port: s.port,
|
||||
username: match s.username_pattern {
|
||||
UsernamePattern::Email => param.addr.to_string(),
|
||||
UsernamePattern::Emaillocalpart => {
|
||||
if let Some(at) = param.addr.find('@') {
|
||||
param.addr.split_at(at).0.to_string()
|
||||
} else {
|
||||
param.addr.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
param_autoconfig = Some(servers)
|
||||
}
|
||||
}
|
||||
provider::Status::Broken => {
|
||||
info!(ctx, "offline autoconfig found, provider is broken");
|
||||
param_autoconfig = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!(ctx, "no offline autoconfig found");
|
||||
param_autoconfig =
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await;
|
||||
}
|
||||
} else {
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
progress!(ctx, 500);
|
||||
|
||||
let mut servers = param_autoconfig.unwrap_or_default();
|
||||
if !servers
|
||||
.iter()
|
||||
.any(|server| server.protocol == Protocol::Imap)
|
||||
{
|
||||
servers.push(ServerParams {
|
||||
protocol: Protocol::Imap,
|
||||
hostname: param.imap.server.clone(),
|
||||
port: param.imap.port,
|
||||
socket: param.imap.security,
|
||||
username: param.imap.user.clone(),
|
||||
})
|
||||
}
|
||||
if !servers
|
||||
.iter()
|
||||
.any(|server| server.protocol == Protocol::Smtp)
|
||||
{
|
||||
servers.push(ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: param.smtp.server.clone(),
|
||||
port: param.smtp.port,
|
||||
socket: param.smtp.security,
|
||||
username: param.smtp.user.clone(),
|
||||
})
|
||||
}
|
||||
let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
|
||||
|
||||
progress!(ctx, 550);
|
||||
|
||||
// Spawn SMTP configuration task
|
||||
let mut smtp = Smtp::new();
|
||||
|
||||
let context_smtp = ctx.clone();
|
||||
let mut smtp_param = param.smtp.clone();
|
||||
let smtp_addr = param.addr.clone();
|
||||
let smtp_servers: Vec<ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::Smtp)
|
||||
.cloned()
|
||||
.collect();
|
||||
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp_configured = false;
|
||||
let mut errors = Vec::new();
|
||||
for smtp_server in smtp_servers {
|
||||
smtp_param.user = smtp_server.username.clone();
|
||||
smtp_param.server = smtp_server.hostname.clone();
|
||||
smtp_param.port = smtp_server.port;
|
||||
smtp_param.security = smtp_server.socket;
|
||||
|
||||
match try_smtp_one_param(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
&mut smtp,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
smtp_configured = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
|
||||
if smtp_configured {
|
||||
Ok(smtp_param)
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
});
|
||||
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
let (_s, r) = async_std::channel::bounded(1);
|
||||
let mut imap = Imap::new(r);
|
||||
|
||||
let mut imap_configured = false;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::Imap)
|
||||
.collect();
|
||||
let imap_servers_count = imap_servers.len();
|
||||
let mut errors = Vec::new();
|
||||
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
|
||||
param.imap.user = imap_server.username.clone();
|
||||
param.imap.server = imap_server.hostname.clone();
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
|
||||
match try_imap_one_param(
|
||||
ctx,
|
||||
¶m.imap,
|
||||
¶m.addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
&mut imap,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
imap_configured = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
progress!(
|
||||
ctx,
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
if !imap_configured {
|
||||
bail!(nicer_configuration_error(ctx, errors).await);
|
||||
}
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
match smtp_config_task.await {
|
||||
Ok(smtp_param) => {
|
||||
param.smtp = smtp_param;
|
||||
}
|
||||
Err(errors) => {
|
||||
bail!(nicer_configuration_error(ctx, errors).await);
|
||||
}
|
||||
}
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
imap.select_with_uidvalidity(ctx, "INBOX")
|
||||
.await
|
||||
.context("could not read INBOX status")?;
|
||||
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
// configuration success - write back the configured parameters with the
|
||||
// "configured_" prefix; also write the "configured"-flag */
|
||||
// the trailing underscore is correct
|
||||
param.save_to_database(ctx, "configured_").await?;
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
|
||||
job::add(
|
||||
ctx,
|
||||
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve available autoconfigurations.
|
||||
///
|
||||
/// A Search configurations from the domain used in the email-address, prefer encrypted
|
||||
/// B. If we have no configuration yet, search configuration in Thunderbird's centeral database
|
||||
async fn get_autoconfig(
|
||||
ctx: &Context,
|
||||
param: &LoginParam,
|
||||
param_domain: &str,
|
||||
param_addr_urlencoded: &str,
|
||||
) -> Option<Vec<ServerParams>> {
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
&format!(
|
||||
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
|
||||
param_domain, param_addr_urlencoded
|
||||
),
|
||||
param,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 300);
|
||||
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
|
||||
&format!(
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
¶m_domain, ¶m_addr_urlencoded
|
||||
),
|
||||
param,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 310);
|
||||
|
||||
// Outlook uses always SSL but different domains (this comment describes the next two steps)
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
format!("https://{}/autodiscover/autodiscover.xml", ¶m_domain),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 320);
|
||||
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
format!(
|
||||
"https://autodiscover.{}/autodiscover/autodiscover.xml",
|
||||
¶m_domain
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 330);
|
||||
|
||||
// always SSL for Thunderbird's database
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
|
||||
param,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn try_imap_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
imap: &mut Imap,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = imap
|
||||
.connect(context, param, addr, oauth2, provider_strict_tls)
|
||||
.await
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_smtp_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp
|
||||
.connect(context, param, addr, oauth2, provider_strict_tls)
|
||||
.await
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Trying {config}…\nError: {msg}")]
|
||||
pub struct ConfigurationError {
|
||||
config: String,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
|
||||
let first_err = if let Some(f) = errors.first() {
|
||||
f
|
||||
} else {
|
||||
// This means configuration failed but no errors have been captured. This should never
|
||||
// happen, but if it does, the user will see classic "Error: no error".
|
||||
return "no error".to_string();
|
||||
};
|
||||
|
||||
if errors
|
||||
.iter()
|
||||
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
|
||||
{
|
||||
return stock_str::error_no_network(context).await;
|
||||
}
|
||||
|
||||
if errors.iter().all(|e| e.msg == first_err.msg) {
|
||||
return first_err.msg.to_string();
|
||||
}
|
||||
|
||||
errors.iter().map(|e| e.to_string()).join("\n\n")
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid email address: {0:?}")]
|
||||
InvalidEmailAddress(String),
|
||||
|
||||
#[error("XML error at position {position}: {error}")]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[source]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[error("Failed to get URL: {0}")]
|
||||
ReadUrlError(#[from] self::read_url::Error),
|
||||
|
||||
#[error("Number of redirection is exceeded")]
|
||||
RedirectionError,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_no_panic_on_bad_credentials() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::Addr, Some("probably@unexistant.addr"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.set_config(Config::MailPw, Some("123456")).await.unwrap();
|
||||
assert!(t.configure().await.is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,31 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::Error;
|
||||
use super::{Error, ServerParams};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MozAutoconfigure<'a> {
|
||||
pub in_emailaddr: &'a str,
|
||||
pub in_emaildomain: &'a str,
|
||||
pub in_emaillocalpart: &'a str,
|
||||
pub out: LoginParam,
|
||||
pub out_imap_set: bool,
|
||||
pub out_smtp_set: bool,
|
||||
pub tag_server: MozServer,
|
||||
pub tag_config: MozConfigTag,
|
||||
struct Server {
|
||||
pub typ: String,
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub sockettype: Socket,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum MozServer {
|
||||
Undefined,
|
||||
Imap,
|
||||
Smtp,
|
||||
#[derive(Debug)]
|
||||
struct MozAutoconfigure {
|
||||
pub incoming_servers: Vec<Server>,
|
||||
pub outgoing_servers: Vec<Server>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -38,10 +37,147 @@ enum MozConfigTag {
|
||||
Username,
|
||||
}
|
||||
|
||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
impl Default for MozConfigTag {
|
||||
fn default() -> Self {
|
||||
Self::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MozConfigTag {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim().to_lowercase().as_ref() {
|
||||
"hostname" => Ok(MozConfigTag::Hostname),
|
||||
"port" => Ok(MozConfigTag::Port),
|
||||
"sockettype" => Ok(MozConfigTag::Sockettype),
|
||||
"username" => Ok(MozConfigTag::Username),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a single IncomingServer or OutgoingServer section.
|
||||
fn parse_server<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
server_event: &BytesStart,
|
||||
) -> Result<Option<Server>, quick_xml::Error> {
|
||||
let end_tag = String::from_utf8_lossy(server_event.name())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
let typ = server_event
|
||||
.attributes()
|
||||
.find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.unwrap()
|
||||
.unescape_and_decode_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut hostname = None;
|
||||
let mut port = None;
|
||||
let mut sockettype = Socket::Automatic;
|
||||
let mut username = None;
|
||||
|
||||
let mut tag_config = MozConfigTag::Undefined;
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
tag_config = String::from_utf8_lossy(event.name())
|
||||
.parse()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Event::End(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == end_tag {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event
|
||||
.unescape_and_decode(reader)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
MozConfigTag::Port => port = Some(val.parse().unwrap_or_default()),
|
||||
MozConfigTag::Username => username = Some(val),
|
||||
MozConfigTag::Sockettype => {
|
||||
sockettype = match val.to_lowercase().as_ref() {
|
||||
"ssl" => Socket::Ssl,
|
||||
"starttls" => Socket::Starttls,
|
||||
"plain" => Socket::Plain,
|
||||
_ => Socket::Automatic,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(hostname), Some(port), Some(username)) = (hostname, port, username) {
|
||||
Ok(Some(Server {
|
||||
typ,
|
||||
hostname,
|
||||
port,
|
||||
sockettype,
|
||||
username,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_xml_reader<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<MozAutoconfigure, quick_xml::Error> {
|
||||
let mut incoming_servers = Vec::new();
|
||||
let mut outgoing_servers = Vec::new();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
if let Some(incoming_server) = parse_server(reader, event)? {
|
||||
incoming_servers.push(incoming_server);
|
||||
}
|
||||
} else if tag == "outgoingserver" {
|
||||
if let Some(outgoing_server) = parse_server(reader, event)? {
|
||||
outgoing_servers.push(outgoing_server);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
Ok(MozAutoconfigure {
|
||||
incoming_servers,
|
||||
outgoing_servers,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses XML and fills in address and domain placeholders.
|
||||
fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoconfigure, Error> {
|
||||
// Split address into local part and domain part.
|
||||
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
|
||||
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
|
||||
@@ -49,59 +185,78 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
|
||||
};
|
||||
|
||||
let mut moz_ac = MozAutoconfigure {
|
||||
in_emailaddr,
|
||||
in_emaildomain,
|
||||
in_emaillocalpart,
|
||||
out: LoginParam::new(),
|
||||
out_imap_set: false,
|
||||
out_smtp_set: false,
|
||||
tag_server: MozServer::Undefined,
|
||||
tag_config: MozConfigTag::Undefined,
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
|
||||
let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
let fill_placeholders = |val: &str| -> String {
|
||||
val.replace("%EMAILADDRESS%", in_emailaddr)
|
||||
.replace("%EMAILLOCALPART%", in_emaillocalpart)
|
||||
.replace("%EMAILDOMAIN%", in_emaildomain)
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
let event = reader
|
||||
.read_event(&mut buf)
|
||||
.map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
match event {
|
||||
quick_xml::events::Event::Start(ref e) => {
|
||||
moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
|
||||
}
|
||||
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
|
||||
quick_xml::events::Event::Text(ref e) => {
|
||||
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
|
||||
}
|
||||
quick_xml::events::Event::Eof => break,
|
||||
_ => (),
|
||||
let fill_server_placeholders = |server: Server| -> Server {
|
||||
Server {
|
||||
typ: server.typ,
|
||||
hostname: fill_placeholders(&server.hostname),
|
||||
port: server.port,
|
||||
sockettype: server.sockettype,
|
||||
username: fill_placeholders(&server.username),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
};
|
||||
|
||||
if moz_ac.out.mail_server.is_empty()
|
||||
|| moz_ac.out.mail_port == 0
|
||||
|| moz_ac.out.send_server.is_empty()
|
||||
|| moz_ac.out.send_port == 0
|
||||
{
|
||||
Err(Error::IncompleteAutoconfig(moz_ac.out))
|
||||
} else {
|
||||
Ok(moz_ac.out)
|
||||
}
|
||||
Ok(MozAutoconfigure {
|
||||
incoming_servers: moz_ac
|
||||
.incoming_servers
|
||||
.into_iter()
|
||||
.map(fill_server_placeholders)
|
||||
.collect(),
|
||||
outgoing_servers: moz_ac
|
||||
.outgoing_servers
|
||||
.into_iter()
|
||||
.map(fill_server_placeholders)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn moz_autoconfigure(
|
||||
/// Parses XML into `ServerParams` vector.
|
||||
fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerParams>, Error> {
|
||||
let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?;
|
||||
|
||||
let res = moz_ac
|
||||
.incoming_servers
|
||||
.into_iter()
|
||||
.chain(moz_ac.outgoing_servers.into_iter())
|
||||
.filter_map(|server| {
|
||||
let protocol = match server.typ.as_ref() {
|
||||
"imap" => Some(Protocol::Imap),
|
||||
"smtp" => Some(Protocol::Smtp),
|
||||
_ => None,
|
||||
};
|
||||
Some(ServerParams {
|
||||
protocol: protocol?,
|
||||
socket: server.sockettype,
|
||||
hostname: server.hostname,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn moz_autoconfigure(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
param_in: &LoginParam,
|
||||
) -> Result<LoginParam, Error> {
|
||||
) -> Result<Vec<ServerParams>, Error> {
|
||||
let xml_raw = read_url(context, url).await?;
|
||||
|
||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
||||
let res = parse_serverparams(¶m_in.addr, &xml_raw);
|
||||
if let Err(err) = &res {
|
||||
warn!(
|
||||
context,
|
||||
@@ -111,212 +266,62 @@ pub async fn moz_autoconfigure(
|
||||
res
|
||||
}
|
||||
|
||||
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
|
||||
event: &BytesText,
|
||||
moz_ac: &mut MozAutoconfigure,
|
||||
reader: &quick_xml::Reader<B>,
|
||||
) {
|
||||
let val = event.unescape_and_decode(reader).unwrap_or_default();
|
||||
|
||||
let addr = moz_ac.in_emailaddr;
|
||||
let email_local = moz_ac.in_emaillocalpart;
|
||||
let email_domain = moz_ac.in_emaildomain;
|
||||
|
||||
let val = val
|
||||
.trim()
|
||||
.replace("%EMAILADDRESS%", addr)
|
||||
.replace("%EMAILLOCALPART%", email_local)
|
||||
.replace("%EMAILDOMAIN%", email_domain);
|
||||
|
||||
match moz_ac.tag_server {
|
||||
MozServer::Imap => match moz_ac.tag_config {
|
||||
MozConfigTag::Hostname => moz_ac.out.mail_server = val,
|
||||
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
|
||||
MozConfigTag::Username => moz_ac.out.mail_user = val,
|
||||
MozConfigTag::Sockettype => {
|
||||
let val_lower = val.to_lowercase();
|
||||
if val_lower == "ssl" {
|
||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
|
||||
}
|
||||
if val_lower == "starttls" {
|
||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32
|
||||
}
|
||||
if val_lower == "plain" {
|
||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
MozServer::Smtp => match moz_ac.tag_config {
|
||||
MozConfigTag::Hostname => moz_ac.out.send_server = val,
|
||||
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(),
|
||||
MozConfigTag::Username => moz_ac.out.send_user = val,
|
||||
MozConfigTag::Sockettype => {
|
||||
let val_lower = val.to_lowercase();
|
||||
if val_lower == "ssl" {
|
||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
|
||||
}
|
||||
if val_lower == "starttls" {
|
||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32
|
||||
}
|
||||
if val_lower == "plain" {
|
||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
MozServer::Undefined => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
if moz_ac.tag_server == MozServer::Imap {
|
||||
moz_ac.out_imap_set = true;
|
||||
}
|
||||
moz_ac.tag_server = MozServer::Undefined;
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else if tag == "outgoingserver" {
|
||||
if moz_ac.tag_server == MozServer::Smtp {
|
||||
moz_ac.out_smtp_set = true;
|
||||
}
|
||||
moz_ac.tag_server = MozServer::Undefined;
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else {
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
|
||||
event: &BytesStart,
|
||||
moz_ac: &mut MozAutoconfigure,
|
||||
reader: &quick_xml::Reader<B>,
|
||||
) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
moz_ac.tag_server = if let Some(typ) = event.attributes().find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
let typ = typ
|
||||
.unwrap()
|
||||
.unescape_and_decode_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
if typ == "imap" && !moz_ac.out_imap_set {
|
||||
MozServer::Imap
|
||||
} else {
|
||||
MozServer::Undefined
|
||||
}
|
||||
} else {
|
||||
MozServer::Undefined
|
||||
};
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else if tag == "outgoingserver" {
|
||||
moz_ac.tag_server = if !moz_ac.out_smtp_set {
|
||||
MozServer::Smtp
|
||||
} else {
|
||||
MozServer::Undefined
|
||||
};
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else if tag == "hostname" {
|
||||
moz_ac.tag_config = MozConfigTag::Hostname;
|
||||
} else if tag == "port" {
|
||||
moz_ac.tag_config = MozConfigTag::Port;
|
||||
} else if tag == "sockettype" {
|
||||
moz_ac.tag_config = MozConfigTag::Sockettype;
|
||||
} else if tag == "username" {
|
||||
moz_ac.tag_config = MozConfigTag::Username;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_outlook_autoconfig() {
|
||||
// Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11
|
||||
let xml_raw =
|
||||
"<clientConfig version=\"1.1\">
|
||||
<emailProvider id=\"outlook.com\">
|
||||
<domain>hotmail.com</domain>
|
||||
<domain>hotmail.co.uk</domain>
|
||||
<domain>hotmail.co.jp</domain>
|
||||
<domain>hotmail.com.br</domain>
|
||||
<domain>hotmail.de</domain>
|
||||
<domain>hotmail.fr</domain>
|
||||
<domain>hotmail.it</domain>
|
||||
<domain>hotmail.es</domain>
|
||||
<domain>live.com</domain>
|
||||
<domain>live.co.uk</domain>
|
||||
<domain>live.co.jp</domain>
|
||||
<domain>live.de</domain>
|
||||
<domain>live.fr</domain>
|
||||
<domain>live.it</domain>
|
||||
<domain>live.jp</domain>
|
||||
<domain>msn.com</domain>
|
||||
<domain>outlook.com</domain>
|
||||
<displayName>Outlook.com (Microsoft)</displayName>
|
||||
<displayShortName>Outlook</displayShortName>
|
||||
<incomingServer type=\"exchange\">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>443</port>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>OAuth2</authentication>
|
||||
<owaURL>https://outlook.office365.com/owa/</owaURL>
|
||||
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
|
||||
<useGlobalPreferredServer>true</useGlobalPreferredServer>
|
||||
</incomingServer>
|
||||
<incomingServer type=\"imap\">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type=\"pop3\">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>995</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<pop3>
|
||||
<leaveMessagesOnServer>true</leaveMessagesOnServer>
|
||||
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
|
||||
</pop3>
|
||||
</incomingServer>
|
||||
<outgoingServer type=\"smtp\">
|
||||
<hostname>smtp.office365.com</hostname>
|
||||
<port>587</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
<documentation url=\"http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app\">
|
||||
<descr lang=\"en\">Set up an email app with Outlook.com</descr>
|
||||
</documentation>
|
||||
</emailProvider>
|
||||
<webMail>
|
||||
<loginPage url=\"https://www.outlook.com/\"/>
|
||||
<loginPageInfo url=\"https://www.outlook.com/\">
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<usernameField id=\"i0116\" name=\"login\"/>
|
||||
<passwordField id=\"i0118\" name=\"passwd\"/>
|
||||
<loginButton id=\"idSIButton9\" name=\"SI\"/>
|
||||
</loginPageInfo>
|
||||
</webMail>
|
||||
</clientConfig>";
|
||||
let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed");
|
||||
assert_eq!(res.mail_server, "outlook.office365.com");
|
||||
assert_eq!(res.mail_port, 993);
|
||||
assert_eq!(res.send_server, "smtp.office365.com");
|
||||
assert_eq!(res.send_port, 587);
|
||||
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
|
||||
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
|
||||
assert_eq!(res[0].protocol, Protocol::Imap);
|
||||
assert_eq!(res[0].hostname, "outlook.office365.com");
|
||||
assert_eq!(res[0].port, 993);
|
||||
assert_eq!(res[1].protocol, Protocol::Smtp);
|
||||
assert_eq!(res[1].hostname, "smtp.office365.com");
|
||||
assert_eq!(res[1].port, 587);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_lakenet_autoconfig() {
|
||||
let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml");
|
||||
let res =
|
||||
parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed");
|
||||
|
||||
assert_eq!(res.incoming_servers.len(), 4);
|
||||
|
||||
assert_eq!(res.incoming_servers[0].typ, "imap");
|
||||
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[0].port, 993);
|
||||
assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
|
||||
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[1].typ, "imap");
|
||||
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[1].port, 143);
|
||||
assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
|
||||
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[2].typ, "pop3");
|
||||
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[2].port, 995);
|
||||
assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
|
||||
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[3].typ, "pop3");
|
||||
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[3].port, 110);
|
||||
assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
|
||||
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.outgoing_servers.len(), 1);
|
||||
|
||||
assert_eq!(res.outgoing_servers[0].typ, "smtp");
|
||||
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.outgoing_servers[0].port, 587);
|
||||
assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
|
||||
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,194 @@
|
||||
//! Outlook's Autodiscover
|
||||
//! # Outlook's Autodiscover
|
||||
//!
|
||||
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
|
||||
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
|
||||
|
||||
use quick_xml::events::BytesEnd;
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::Error;
|
||||
use super::{Error, ServerParams};
|
||||
|
||||
struct OutlookAutodiscover {
|
||||
pub out: LoginParam,
|
||||
pub out_imap_set: bool,
|
||||
pub out_smtp_set: bool,
|
||||
pub config_type: Option<String>,
|
||||
pub config_server: String,
|
||||
pub config_port: i32,
|
||||
pub config_ssl: String,
|
||||
pub config_redirecturl: Option<String>,
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
|
||||
#[derive(Debug)]
|
||||
struct ProtocolTag {
|
||||
/// Server type, such as "IMAP", "SMTP" or "POP3".
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
|
||||
pub typ: String,
|
||||
|
||||
/// Server identifier, hostname or IP address for IMAP and SMTP.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
|
||||
pub server: String,
|
||||
|
||||
/// Network port.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
|
||||
pub port: u16,
|
||||
|
||||
/// Whether connection should be secure, "on" or "off", default is "on".
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
|
||||
pub ssl: bool,
|
||||
}
|
||||
|
||||
enum ParsingResult {
|
||||
LoginParam(LoginParam),
|
||||
Protocols(Vec<ProtocolTag>),
|
||||
|
||||
/// XML redirect via `RedirectUrl` tag.
|
||||
RedirectUrl(String),
|
||||
}
|
||||
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
let mut outlk_ad = OutlookAutodiscover {
|
||||
out: LoginParam::new(),
|
||||
out_imap_set: false,
|
||||
out_smtp_set: false,
|
||||
config_type: None,
|
||||
config_server: String::new(),
|
||||
config_port: 0,
|
||||
config_ssl: String::new(),
|
||||
config_redirecturl: None,
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(&xml_raw);
|
||||
reader.trim_text(true);
|
||||
/// Parses a single Protocol section.
|
||||
fn parse_protocol<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<Option<ProtocolTag>, quick_xml::Error> {
|
||||
let mut protocol_type = None;
|
||||
let mut protocol_server = None;
|
||||
let mut protocol_port = None;
|
||||
let mut protocol_ssl = true;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut current_tag: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let event = reader
|
||||
.read_event(&mut buf)
|
||||
.map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
match event {
|
||||
quick_xml::events::Event::Start(ref e) => {
|
||||
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
|
||||
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase());
|
||||
}
|
||||
Event::End(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
if tag == "protocol" {
|
||||
outlk_ad.config_type = None;
|
||||
outlk_ad.config_server = String::new();
|
||||
outlk_ad.config_port = 0;
|
||||
outlk_ad.config_ssl = String::new();
|
||||
outlk_ad.config_redirecturl = None;
|
||||
|
||||
break;
|
||||
}
|
||||
if Some(tag) == current_tag {
|
||||
current_tag = None;
|
||||
} else {
|
||||
current_tag = Some(tag);
|
||||
}
|
||||
}
|
||||
quick_xml::events::Event::End(ref e) => {
|
||||
outlk_autodiscover_endtag_cb(e, &mut outlk_ad);
|
||||
current_tag = None;
|
||||
}
|
||||
quick_xml::events::Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(&reader).unwrap_or_default();
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(reader).unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
"type" => {
|
||||
outlk_ad.config_type = Some(val.trim().to_lowercase().to_string())
|
||||
"type" => protocol_type = Some(val.trim().to_string()),
|
||||
"server" => protocol_server = Some(val.trim().to_string()),
|
||||
"port" => protocol_port = Some(val.trim().parse().unwrap_or_default()),
|
||||
"ssl" => {
|
||||
protocol_ssl = match val.trim() {
|
||||
"on" => true,
|
||||
"off" => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
"server" => outlk_ad.config_server = val.trim().to_string(),
|
||||
"port" => outlk_ad.config_port = val.trim().parse().unwrap_or_default(),
|
||||
"ssl" => outlk_ad.config_ssl = val.trim().to_string(),
|
||||
"redirecturl" => outlk_ad.config_redirecturl = Some(val.trim().to_string()),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
quick_xml::events::Event::Eof => break,
|
||||
Event::Eof => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(protocol_type), Some(protocol_server), Some(protocol_port)) =
|
||||
(protocol_type, protocol_server, protocol_port)
|
||||
{
|
||||
Ok(Some(ProtocolTag {
|
||||
typ: protocol_type,
|
||||
server: protocol_server,
|
||||
port: protocol_port,
|
||||
ssl: protocol_ssl,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `RedirectUrl` tag.
|
||||
fn parse_redirecturl<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<String, quick_xml::Error> {
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(reader).unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_xml_reader<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<ParsingResult, quick_xml::Error> {
|
||||
let mut protocols = Vec::new();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref e) => {
|
||||
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "protocol" {
|
||||
if let Some(protocol) = parse_protocol(reader)? {
|
||||
protocols.push(protocol);
|
||||
}
|
||||
} else if tag == "redirecturl" {
|
||||
let redirecturl = parse_redirecturl(reader)?;
|
||||
return Ok(ParsingResult::RedirectUrl(redirecturl));
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
// XML redirect via redirecturl
|
||||
let res = if outlk_ad.config_redirecturl.is_none()
|
||||
|| outlk_ad.config_redirecturl.as_ref().unwrap().is_empty()
|
||||
{
|
||||
if outlk_ad.out.mail_server.is_empty()
|
||||
|| outlk_ad.out.mail_port == 0
|
||||
|| outlk_ad.out.send_server.is_empty()
|
||||
|| outlk_ad.out.send_port == 0
|
||||
{
|
||||
return Err(Error::IncompleteAutoconfig(outlk_ad.out));
|
||||
}
|
||||
ParsingResult::LoginParam(outlk_ad.out)
|
||||
} else {
|
||||
ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap())
|
||||
};
|
||||
Ok(res)
|
||||
Ok(ParsingResult::Protocols(protocols))
|
||||
}
|
||||
|
||||
pub async fn outlk_autodiscover(
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
|
||||
parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
|
||||
protocols
|
||||
.into_iter()
|
||||
.filter_map(|protocol| {
|
||||
Some(ServerParams {
|
||||
protocol: match protocol.typ.to_lowercase().as_ref() {
|
||||
"imap" => Some(Protocol::Imap),
|
||||
"smtp" => Some(Protocol::Smtp),
|
||||
_ => None,
|
||||
}?,
|
||||
socket: match protocol.ssl {
|
||||
true => Socket::Automatic,
|
||||
false => Socket::Plain,
|
||||
},
|
||||
hostname: protocol.server,
|
||||
port: protocol.port,
|
||||
username: String::new(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn outlk_autodiscover(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
_param_in: &LoginParam,
|
||||
) -> Result<LoginParam, Error> {
|
||||
let mut url = url.to_string();
|
||||
mut url: String,
|
||||
) -> Result<Vec<ServerParams>, Error> {
|
||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||
for _i in 0..10 {
|
||||
let xml_raw = read_url(context, &url).await?;
|
||||
@@ -127,47 +198,18 @@ pub async fn outlk_autodiscover(
|
||||
}
|
||||
match res? {
|
||||
ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,
|
||||
ParsingResult::LoginParam(login_param) => return Ok(login_param),
|
||||
ParsingResult::Protocols(protocols) => {
|
||||
return Ok(protocols_to_serverparams(protocols));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Error::RedirectionError)
|
||||
}
|
||||
|
||||
fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "protocol" {
|
||||
if let Some(type_) = &outlk_ad.config_type {
|
||||
let port = outlk_ad.config_port;
|
||||
let ssl_on = outlk_ad.config_ssl == "on";
|
||||
let ssl_off = outlk_ad.config_ssl == "off";
|
||||
if type_ == "imap" && !outlk_ad.out_imap_set {
|
||||
outlk_ad.out.mail_server =
|
||||
std::mem::replace(&mut outlk_ad.config_server, String::new());
|
||||
outlk_ad.out.mail_port = port;
|
||||
if ssl_on {
|
||||
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
|
||||
} else if ssl_off {
|
||||
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
|
||||
}
|
||||
outlk_ad.out_imap_set = true
|
||||
} else if type_ == "smtp" && !outlk_ad.out_smtp_set {
|
||||
outlk_ad.out.send_server =
|
||||
std::mem::replace(&mut outlk_ad.config_server, String::new());
|
||||
outlk_ad.out.send_port = outlk_ad.config_port;
|
||||
if ssl_on {
|
||||
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
|
||||
} else if ssl_off {
|
||||
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
|
||||
}
|
||||
outlk_ad.out_smtp_set = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -184,16 +226,13 @@ mod tests {
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
").expect("XML is not parsed successfully");
|
||||
match res {
|
||||
ParsingResult::LoginParam(_lp) => {
|
||||
panic!("redirecturl is not found");
|
||||
}
|
||||
ParsingResult::RedirectUrl(url) => {
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://mail.example.com/autodiscover/autodiscover.xml"
|
||||
);
|
||||
}
|
||||
if let ParsingResult::RedirectUrl(url) = res {
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://mail.example.com/autodiscover/autodiscover.xml"
|
||||
);
|
||||
} else {
|
||||
panic!("redirecturl is not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,11 +267,16 @@ mod tests {
|
||||
.expect("XML is not parsed successfully");
|
||||
|
||||
match res {
|
||||
ParsingResult::LoginParam(lp) => {
|
||||
assert_eq!(lp.mail_server, "example.com");
|
||||
assert_eq!(lp.mail_port, 993);
|
||||
assert_eq!(lp.send_server, "smtp.example.com");
|
||||
assert_eq!(lp.send_port, 25);
|
||||
ParsingResult::Protocols(protocols) => {
|
||||
assert_eq!(protocols[0].typ, "IMAP");
|
||||
assert_eq!(protocols[0].server, "example.com");
|
||||
assert_eq!(protocols[0].port, 993);
|
||||
assert_eq!(protocols[0].ssl, true);
|
||||
|
||||
assert_eq!(protocols[1].typ, "SMTP");
|
||||
assert_eq!(protocols[1].server, "smtp.example.com");
|
||||
assert_eq!(protocols[1].port, 25);
|
||||
assert_eq!(protocols[1].ssl, false);
|
||||
}
|
||||
ParsingResult::RedirectUrl(_) => {
|
||||
panic!("RedirectUrl is not expected");
|
||||
|
||||
@@ -1,719 +0,0 @@
|
||||
//! Email accounts autoconfiguration process module
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod read_url;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::imap::Imap;
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::*;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr) => {
|
||||
assert!(
|
||||
$progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress($progress));
|
||||
};
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Checks if the context is already configured.
|
||||
pub async fn is_configured(&self) -> bool {
|
||||
self.sql.get_raw_config_bool(self, "configured").await
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
ensure!(
|
||||
!self.scheduler.read().await.is_running(),
|
||||
"cannot configure, already running"
|
||||
);
|
||||
ensure!(
|
||||
self.sql.is_open().await,
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
let res = self
|
||||
.inner_configure()
|
||||
.race(cancel_channel.recv().map(|_| {
|
||||
progress!(self, 0);
|
||||
Ok(())
|
||||
}))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::from_database(self, "").await;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !self.config_exists(def.key).await {
|
||||
info!(self, "apply config_defaults {}={}", def.key, def.value);
|
||||
self.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
info!(
|
||||
self,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(self, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!(self, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match success {
|
||||
Ok(_) => {
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
progress!(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
progress!(self, 0);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let mut param_autoconfig: Option<LoginParam> = None;
|
||||
let mut keep_flags = 0;
|
||||
|
||||
// Read login parameters from the database
|
||||
progress!(ctx, 1);
|
||||
ensure!(!param.addr.is_empty(), "Please enter an email address.");
|
||||
|
||||
// Step 1: Load the parameters and check email-address and password
|
||||
|
||||
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.mail_pw)
|
||||
.await
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
info!(ctx, "Authorized address is {}", oauth2_addr);
|
||||
param.addr = oauth2_addr;
|
||||
ctx.sql
|
||||
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
|
||||
.await?;
|
||||
}
|
||||
progress!(ctx, 20);
|
||||
}
|
||||
// no oauth? - just continue it's no error
|
||||
|
||||
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// Step 2: Autoconfig
|
||||
progress!(ctx, 200);
|
||||
|
||||
// param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then
|
||||
// param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for
|
||||
// autoconfig or not
|
||||
if param.mail_server.is_empty()
|
||||
&& param.mail_port == 0
|
||||
&& param.send_server.is_empty()
|
||||
&& param.send_port == 0
|
||||
&& param.send_user.is_empty()
|
||||
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
|
||||
{
|
||||
// no advanced parameters entered by the user: query provider-database or do Autoconfig
|
||||
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
|
||||
if let Some(new_param) = get_offline_autoconfig(ctx, ¶m) {
|
||||
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
|
||||
param_autoconfig = Some(new_param);
|
||||
}
|
||||
|
||||
if param_autoconfig.is_none() {
|
||||
param_autoconfig =
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await;
|
||||
}
|
||||
}
|
||||
|
||||
// C. Do we have any autoconfig result?
|
||||
progress!(ctx, 500);
|
||||
if let Some(ref cfg) = param_autoconfig {
|
||||
info!(ctx, "Got autoconfig: {}", &cfg);
|
||||
if !cfg.mail_user.is_empty() {
|
||||
param.mail_user = cfg.mail_user.clone();
|
||||
}
|
||||
// all other values are always NULL when entering autoconfig
|
||||
param.mail_server = cfg.mail_server.clone();
|
||||
param.mail_port = cfg.mail_port;
|
||||
param.send_server = cfg.send_server.clone();
|
||||
param.send_port = cfg.send_port;
|
||||
param.send_user = cfg.send_user.clone();
|
||||
param.server_flags = cfg.server_flags;
|
||||
// although param_autoconfig's data are no longer needed from,
|
||||
// it is used to later to prevent trying variations of port/server/logins
|
||||
}
|
||||
param.server_flags |= keep_flags;
|
||||
|
||||
// Step 3: Fill missing fields with defaults
|
||||
if param.send_user.is_empty() {
|
||||
param.send_user = param.mail_user.clone();
|
||||
}
|
||||
if param.send_pw.is_empty() {
|
||||
param.send_pw = param.mail_pw.clone()
|
||||
}
|
||||
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
||||
}
|
||||
if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) {
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
}
|
||||
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) {
|
||||
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_AUTH_NORMAL as i32
|
||||
}
|
||||
|
||||
// do we have a complete configuration?
|
||||
ensure!(
|
||||
!param.mail_pw.is_empty() && !param.send_pw.is_empty(),
|
||||
"Account settings incomplete."
|
||||
);
|
||||
|
||||
progress!(ctx, 600);
|
||||
// try to connect to IMAP - if we did not got an autoconfig,
|
||||
// do some further tries with different settings and username variations
|
||||
let (_s, r) = async_std::sync::channel(1);
|
||||
let mut imap = Imap::new(r);
|
||||
|
||||
if param_autoconfig.is_some() {
|
||||
if try_imap_one_param(ctx, ¶m, &mut imap).await.is_err() {
|
||||
bail!("IMAP autoconfig did not succeed");
|
||||
}
|
||||
} else {
|
||||
*param = try_imap_hostnames(ctx, param.clone(), &mut imap).await?;
|
||||
}
|
||||
progress!(ctx, 750);
|
||||
|
||||
let mut smtp = Smtp::new();
|
||||
if param_autoconfig.is_some() {
|
||||
if try_smtp_one_param(ctx, ¶m, &mut smtp).await.is_err() {
|
||||
bail!("SMTP autoconfig did not succeed");
|
||||
}
|
||||
} else {
|
||||
*param = try_smtp_hostnames(ctx, param.clone(), &mut smtp).await?;
|
||||
}
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
imap.select_with_uidvalidity(ctx, "INBOX")
|
||||
.await
|
||||
.context("could not read INBOX status")?;
|
||||
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
// configuration success - write back the configured parameters with the
|
||||
// "configured_" prefix; also write the "configured"-flag */
|
||||
// the trailing underscore is correct
|
||||
param.save_to_database(ctx, "configured_").await?;
|
||||
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
|
||||
progress!(ctx, 940);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum AutoconfigProvider {
|
||||
Mozilla,
|
||||
Outlook,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct AutoconfigSource {
|
||||
provider: AutoconfigProvider,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl AutoconfigSource {
|
||||
fn all(domain: &str, addr: &str) -> [Self; 5] {
|
||||
[
|
||||
AutoconfigSource {
|
||||
provider: AutoconfigProvider::Mozilla,
|
||||
url: format!(
|
||||
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
|
||||
domain, addr,
|
||||
),
|
||||
},
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
|
||||
AutoconfigSource {
|
||||
provider: AutoconfigProvider::Mozilla,
|
||||
url: format!(
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
domain, addr
|
||||
),
|
||||
},
|
||||
AutoconfigSource {
|
||||
provider: AutoconfigProvider::Outlook,
|
||||
url: format!("https://{}/autodiscover/autodiscover.xml", domain),
|
||||
},
|
||||
// Outlook uses always SSL but different domains (this comment describes the next two steps)
|
||||
AutoconfigSource {
|
||||
provider: AutoconfigProvider::Outlook,
|
||||
url: format!(
|
||||
"https://{}{}/autodiscover/autodiscover.xml",
|
||||
"autodiscover.", domain
|
||||
),
|
||||
},
|
||||
// always SSL for Thunderbird's database
|
||||
AutoconfigSource {
|
||||
provider: AutoconfigProvider::Mozilla,
|
||||
url: format!("https://autoconfig.thunderbird.net/v1.1/{}", domain),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<LoginParam> {
|
||||
let params = match self.provider {
|
||||
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, ¶m).await?,
|
||||
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, ¶m).await?,
|
||||
};
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve available autoconfigurations.
|
||||
///
|
||||
/// A Search configurations from the domain used in the email-address, prefer encrypted
|
||||
/// B. If we have no configuration yet, search configuration in Thunderbird's centeral database
|
||||
async fn get_autoconfig(
|
||||
ctx: &Context,
|
||||
param: &LoginParam,
|
||||
param_domain: &str,
|
||||
param_addr_urlencoded: &str,
|
||||
) -> Option<LoginParam> {
|
||||
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
|
||||
|
||||
let mut progress = 300;
|
||||
for source in &sources {
|
||||
let res = source.fetch(ctx, param).await;
|
||||
progress!(ctx, progress);
|
||||
progress += 10;
|
||||
if let Ok(res) = res {
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
|
||||
info!(
|
||||
context,
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
match provider.status {
|
||||
provider::Status::OK | provider::Status::PREPARATION => {
|
||||
let imap = provider.get_imap_server();
|
||||
let smtp = provider.get_smtp_server();
|
||||
// clippy complains about these is_some()/unwrap() settings,
|
||||
// however, rewriting the code to "if let" would make things less obvious,
|
||||
// esp. if we allow more combinations of servers (pop, jmap).
|
||||
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
|
||||
if let Some(imap) = imap {
|
||||
if let Some(smtp) = smtp {
|
||||
let mut p = LoginParam::new();
|
||||
p.addr = param.addr.clone();
|
||||
|
||||
p.mail_server = imap.hostname.to_string();
|
||||
p.mail_user = imap.apply_username_pattern(param.addr.clone());
|
||||
p.mail_port = imap.port as i32;
|
||||
p.imap_certificate_checks = CertificateChecks::Automatic;
|
||||
p.server_flags |= match imap.socket {
|
||||
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
|
||||
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
|
||||
};
|
||||
|
||||
p.send_server = smtp.hostname.to_string();
|
||||
p.send_user = smtp.apply_username_pattern(param.addr.clone());
|
||||
p.send_port = smtp.port as i32;
|
||||
p.smtp_certificate_checks = CertificateChecks::Automatic;
|
||||
p.server_flags |= match smtp.socket {
|
||||
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
|
||||
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
|
||||
};
|
||||
|
||||
info!(context, "offline autoconfig found: {}", p);
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
info!(context, "offline autoconfig found, but no servers defined");
|
||||
return None;
|
||||
}
|
||||
provider::Status::BROKEN => {
|
||||
info!(context, "offline autoconfig found, provider is broken");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(context, "no offline autoconfig found");
|
||||
None
|
||||
}
|
||||
|
||||
async fn try_imap_hostnames(
|
||||
context: &Context,
|
||||
mut param: LoginParam,
|
||||
imap: &mut Imap,
|
||||
) -> Result<LoginParam> {
|
||||
if param.mail_server.is_empty() {
|
||||
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
|
||||
param.mail_server = param_domain.clone();
|
||||
if let Ok(param) = try_imap_ports(context, param.clone(), imap).await {
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
progress!(context, 650);
|
||||
param.mail_server = "imap.".to_string() + ¶m_domain;
|
||||
if let Ok(param) = try_imap_ports(context, param.clone(), imap).await {
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
progress!(context, 700);
|
||||
param.mail_server = "mail.".to_string() + ¶m_domain;
|
||||
try_imap_ports(context, param, imap).await
|
||||
} else {
|
||||
progress!(context, 700);
|
||||
try_imap_ports(context, param, imap).await
|
||||
}
|
||||
}
|
||||
|
||||
// Try various IMAP ports and corresponding TLS settings.
|
||||
async fn try_imap_ports(
|
||||
context: &Context,
|
||||
mut param: LoginParam,
|
||||
imap: &mut Imap,
|
||||
) -> Result<LoginParam> {
|
||||
// Try to infer port from socket security.
|
||||
if param.mail_port == 0 {
|
||||
if 0 != param.server_flags & DC_LP_IMAP_SOCKET_SSL {
|
||||
param.mail_port = 993
|
||||
}
|
||||
if 0 != param.server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN) {
|
||||
param.mail_port = 143
|
||||
}
|
||||
}
|
||||
|
||||
if param.mail_port == 0 {
|
||||
// Neither port nor security is set.
|
||||
//
|
||||
// Try common secure combinations.
|
||||
|
||||
// Try TLS over port 993
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32;
|
||||
param.mail_port = 993;
|
||||
if let Ok(login_param) = try_imap_usernames(context, param.clone(), imap).await {
|
||||
return Ok(login_param);
|
||||
}
|
||||
|
||||
// Try STARTTLS over port 143
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32;
|
||||
param.mail_port = 143;
|
||||
try_imap_usernames(context, param, imap).await
|
||||
} else if 0 == param.server_flags & DC_LP_SMTP_SOCKET_FLAGS as i32 {
|
||||
// Try TLS over user-provided port.
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32;
|
||||
if let Ok(login_param) = try_imap_usernames(context, param.clone(), imap).await {
|
||||
return Ok(login_param);
|
||||
}
|
||||
|
||||
// Try STARTTLS over user-provided port.
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32;
|
||||
try_imap_usernames(context, param, imap).await
|
||||
} else {
|
||||
try_imap_usernames(context, param, imap).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_imap_usernames(
|
||||
context: &Context,
|
||||
mut param: LoginParam,
|
||||
imap: &mut Imap,
|
||||
) -> Result<LoginParam> {
|
||||
if param.mail_user.is_empty() {
|
||||
param.mail_user = param.addr.clone();
|
||||
if let Err(e) = try_imap_one_param(context, ¶m, imap).await {
|
||||
if let Some(at) = param.mail_user.find('@') {
|
||||
param.mail_user = param.mail_user.split_at(at).0.to_string();
|
||||
try_imap_one_param(context, ¶m, imap).await?;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(param)
|
||||
} else {
|
||||
try_imap_one_param(context, ¶m, imap).await?;
|
||||
Ok(param)
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
|
||||
param.mail_user,
|
||||
param.mail_server,
|
||||
param.mail_port,
|
||||
param.server_flags,
|
||||
param.imap_certificate_checks
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if imap.connect(context, ¶m).await {
|
||||
info!(context, "success: {}", inf);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bail!("Could not connect: {}", inf);
|
||||
}
|
||||
|
||||
async fn try_smtp_hostnames(
|
||||
context: &Context,
|
||||
mut param: LoginParam,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<LoginParam> {
|
||||
if param.send_server.is_empty() {
|
||||
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
|
||||
param.send_server = param_domain.clone();
|
||||
if let Ok(param) = try_smtp_ports(context, param.clone(), smtp).await {
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
progress!(context, 800);
|
||||
param.send_server = "smtp.".to_string() + ¶m_domain;
|
||||
if let Ok(param) = try_smtp_ports(context, param.clone(), smtp).await {
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
progress!(context, 850);
|
||||
param.send_server = "mail.".to_string() + ¶m_domain;
|
||||
try_smtp_ports(context, param, smtp).await
|
||||
} else {
|
||||
progress!(context, 850);
|
||||
try_smtp_ports(context, param, smtp).await
|
||||
}
|
||||
}
|
||||
|
||||
// Try various SMTP ports and corresponding TLS settings.
|
||||
async fn try_smtp_ports(
|
||||
context: &Context,
|
||||
mut param: LoginParam,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<LoginParam> {
|
||||
// Try to infer port from socket security.
|
||||
if param.send_port == 0 {
|
||||
if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
|
||||
param.send_port = 587;
|
||||
}
|
||||
if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
|
||||
param.send_port = 25;
|
||||
}
|
||||
if 0 != param.server_flags & DC_LP_SMTP_SOCKET_SSL as i32 {
|
||||
param.send_port = 465;
|
||||
}
|
||||
}
|
||||
|
||||
if param.send_port == 0 {
|
||||
// Neither port nor security is set.
|
||||
//
|
||||
// Try common secure combinations.
|
||||
|
||||
// Try TLS over port 465.
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32;
|
||||
param.send_port = 465;
|
||||
if let Ok(login_param) = try_smtp_usernames(context, param.clone(), smtp).await {
|
||||
return Ok(login_param);
|
||||
}
|
||||
|
||||
// Try STARTTLS over port 587.
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
||||
param.send_port = 587;
|
||||
try_smtp_usernames(context, param, smtp).await
|
||||
} else if 0 == param.server_flags & DC_LP_SMTP_SOCKET_FLAGS as i32 {
|
||||
// Try TLS over user-provided port.
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32;
|
||||
if let Ok(param) = try_smtp_usernames(context, param.clone(), smtp).await {
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
// Try STARTTLS over user-provided port.
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
||||
try_smtp_usernames(context, param, smtp).await
|
||||
} else {
|
||||
try_smtp_usernames(context, param, smtp).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_smtp_usernames(
|
||||
context: &Context,
|
||||
mut param: LoginParam,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<LoginParam> {
|
||||
if param.send_user.is_empty() {
|
||||
param.send_user = param.addr.clone();
|
||||
if let Err(e) = try_smtp_one_param(context, ¶m, smtp).await {
|
||||
if let Some(at) = param.send_user.find('@') {
|
||||
param.send_user = param.send_user.split_at(at).0.to_string();
|
||||
try_smtp_one_param(context, ¶m, smtp).await?;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(param)
|
||||
} else {
|
||||
try_smtp_one_param(context, ¶m, smtp).await?;
|
||||
Ok(param)
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} flags: 0x{:x}",
|
||||
param.send_user, param.send_server, param.send_port, param.server_flags
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp.connect(context, ¶m).await {
|
||||
bail!("could not connect: {}", err);
|
||||
}
|
||||
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid email address: {0:?}")]
|
||||
InvalidEmailAddress(String),
|
||||
|
||||
#[error("XML error at position {position}")]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[source]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[error("Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[error("Failed to get URL")]
|
||||
ReadUrlError(#[from] self::read_url::Error),
|
||||
|
||||
#[error("Number of redirection is exceeded")]
|
||||
RedirectionError,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::config::*;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_no_panic_on_bad_credentials() {
|
||||
let t = TestContext::new().await;
|
||||
t.ctx
|
||||
.set_config(Config::Addr, Some("probably@unexistant.addr"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::MailPw, Some("123456"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(t.ctx.configure().await.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_offline_autoconfig() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
|
||||
let mut params = LoginParam::new();
|
||||
params.addr = "someone123@example.org".to_string();
|
||||
assert!(get_offline_autoconfig(&context, ¶ms).is_none());
|
||||
|
||||
let mut params = LoginParam::new();
|
||||
params.addr = "someone123@nauta.cu".to_string();
|
||||
let found_params = get_offline_autoconfig(&context, ¶ms).unwrap();
|
||||
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
|
||||
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
|
||||
|
||||
assert_eq!(
|
||||
found_params.imap_certificate_checks,
|
||||
CertificateChecks::Automatic
|
||||
);
|
||||
assert_eq!(
|
||||
found_params.smtp_certificate_checks,
|
||||
CertificateChecks::Automatic
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
|
||||
match surf::get(url).recv_string().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
info!(context, "Can\'t read URL {}", url);
|
||||
info!(context, "Can\'t read URL {}: {}", url, err);
|
||||
|
||||
Err(Error::GetError(err))
|
||||
}
|
||||
|
||||
196
src/configure/server_params.rs
Normal file
196
src/configure/server_params.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
//! Variable server parameters lists
|
||||
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Set of variable parameters to try during configuration.
|
||||
///
|
||||
/// Can be loaded from offline provider database, online configuraiton
|
||||
/// or derived from user entered parameters.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ServerParams {
|
||||
/// Protocol, such as IMAP or SMTP.
|
||||
pub protocol: Protocol,
|
||||
|
||||
/// Server hostname, empty if unknown.
|
||||
pub hostname: String,
|
||||
|
||||
/// Server port, zero if unknown.
|
||||
pub port: u16,
|
||||
|
||||
/// Socket security, such as TLS or STARTTLS, Socket::Automatic if unknown.
|
||||
pub socket: Socket,
|
||||
|
||||
/// Username, empty if unknown.
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl ServerParams {
|
||||
fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
if self.username.is_empty() {
|
||||
self.username = addr.to_string();
|
||||
res.push(self.clone());
|
||||
|
||||
if let Some(at) = addr.find('@') {
|
||||
self.username = addr.split_at(at).0.to_string();
|
||||
res.push(self);
|
||||
}
|
||||
} else {
|
||||
res.push(self)
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
|
||||
let mut res = Vec::new();
|
||||
if self.hostname.is_empty() {
|
||||
self.hostname = param_domain.to_string();
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = match self.protocol {
|
||||
Protocol::Imap => "imap.".to_string() + param_domain,
|
||||
Protocol::Smtp => "smtp.".to_string() + param_domain,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = "mail.".to_string() + param_domain;
|
||||
res.push(self);
|
||||
} else {
|
||||
res.push(self);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn expand_ports(mut self) -> Vec<ServerParams> {
|
||||
// Try to infer port from socket security.
|
||||
if self.port == 0 {
|
||||
self.port = match self.socket {
|
||||
Socket::Ssl => match self.protocol {
|
||||
Protocol::Imap => 993,
|
||||
Protocol::Smtp => 465,
|
||||
},
|
||||
Socket::Starttls | Socket::Plain => match self.protocol {
|
||||
Protocol::Imap => 143,
|
||||
Protocol::Smtp => 587,
|
||||
},
|
||||
Socket::Automatic => 0,
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = Vec::new();
|
||||
if self.port == 0 {
|
||||
// Neither port nor security is set.
|
||||
//
|
||||
// Try common secure combinations.
|
||||
|
||||
// Try STARTTLS
|
||||
self.socket = Socket::Starttls;
|
||||
self.port = match self.protocol {
|
||||
Protocol::Imap => 143,
|
||||
Protocol::Smtp => 587,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
// Try TLS
|
||||
self.socket = Socket::Ssl;
|
||||
self.port = match self.protocol {
|
||||
Protocol::Imap => 993,
|
||||
Protocol::Smtp => 465,
|
||||
};
|
||||
res.push(self);
|
||||
} else if self.socket == Socket::Automatic {
|
||||
// Try TLS over user-provided port.
|
||||
self.socket = Socket::Ssl;
|
||||
res.push(self.clone());
|
||||
|
||||
// Try STARTTLS over user-provided port.
|
||||
self.socket = Socket::Starttls;
|
||||
res.push(self);
|
||||
} else {
|
||||
res.push(self);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands vector of `ServerParams`, replacing placeholders with
|
||||
/// variants to try.
|
||||
pub(crate) fn expand_param_vector(
|
||||
v: Vec<ServerParams>,
|
||||
addr: &str,
|
||||
domain: &str,
|
||||
) -> Vec<ServerParams> {
|
||||
v.into_iter()
|
||||
// The order of expansion is important: ports are expanded the
|
||||
// last, so they are changed the first. Username is only
|
||||
// changed if default value (address with domain) didn't work
|
||||
// for all available hosts and ports.
|
||||
.flat_map(|params| params.expand_usernames(addr).into_iter())
|
||||
.flat_map(|params| params.expand_hostnames(domain).into_iter())
|
||||
.flat_map(|params| params.expand_ports().into_iter())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_expand_param_vector() {
|
||||
let v = expand_param_vector(
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::Imap,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 0,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
v,
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::Imap,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 993,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
);
|
||||
|
||||
let v = expand_param_vector(
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Automatic,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
v,
|
||||
vec![
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string()
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Starttls,
|
||||
username: "foobar".to_string()
|
||||
}
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
258
src/constants.rs
258
src/constants.rs
@@ -1,20 +1,11 @@
|
||||
//! # Constants
|
||||
#![allow(dead_code)]
|
||||
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
||||
}
|
||||
use crate::chat::ChatId;
|
||||
|
||||
// some defaults
|
||||
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
|
||||
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -30,7 +21,7 @@ const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
#[repr(i8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Manually = 1,
|
||||
@@ -43,7 +34,9 @@ impl Default for Blocked {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
@@ -57,7 +50,9 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum MediaQuality {
|
||||
Balanced = 0,
|
||||
@@ -70,7 +65,9 @@ impl Default for MediaQuality {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
Default = 0,
|
||||
@@ -84,7 +81,9 @@ impl Default for KeyGenType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
Unknown = 0,
|
||||
@@ -110,27 +109,30 @@ pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
||||
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
|
||||
|
||||
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
pub const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
|
||||
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
|
||||
pub const DC_GCL_ADD_SELF: usize = 0x02;
|
||||
pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
|
||||
pub const DC_GCL_ADD_SELF: u32 = 0x02;
|
||||
|
||||
// unchanged user avatars are resent to the recipients every some days
|
||||
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
|
||||
// warn about an outdated app after a given number of days.
|
||||
// as we use the "provider-db generation date" as reference (that might not be updated very often)
|
||||
// and as not all system get speedy updates,
|
||||
// do not use too small value that will annoy users checking for nonexistant updates.
|
||||
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
|
||||
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
||||
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
|
||||
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1);
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: u32 = 3;
|
||||
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
|
||||
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
|
||||
/// virtual chat showing all messages flagged with msgs.starred=2
|
||||
pub const DC_CHAT_ID_STARRED: u32 = 5;
|
||||
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
|
||||
/// only an indicator in a chatlist
|
||||
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
|
||||
pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6);
|
||||
/// only an indicator in a chatlist
|
||||
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
|
||||
pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
|
||||
/// larger chat IDs are "real" chats, their messages are "real" messages.
|
||||
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
||||
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -152,7 +154,7 @@ pub enum Chattype {
|
||||
Undefined = 0,
|
||||
Single = 100,
|
||||
Group = 120,
|
||||
VerifiedGroup = 130,
|
||||
Mailinglist = 140,
|
||||
}
|
||||
|
||||
impl Default for Chattype {
|
||||
@@ -165,10 +167,36 @@ pub const DC_MSG_ID_MARKER1: u32 = 1;
|
||||
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
/// approx. max. length returned by dc_msg_get_text()
|
||||
const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
||||
/// string that indicates sth. is left out or truncated
|
||||
pub const DC_ELLIPSE: &str = "[...]";
|
||||
|
||||
/// to keep bubbles and chat flow usable,
|
||||
/// and to avoid problems with controls using very long texts,
|
||||
/// we limit the text length to DC_DESIRED_TEXT_LEN.
|
||||
/// if the text is longer, the full text can be retrieved usind has_html()/get_html().
|
||||
///
|
||||
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
|
||||
/// (a bit less as truncation may not be exact and ellipses may be added).
|
||||
///
|
||||
/// note, that DC_DESIRED_TEXT_LEN and DC_MAX_GET_TEXT_LEN
|
||||
/// define max. number of bytes, _not_ unicode graphemes.
|
||||
/// in general, that seems to be okay for such an upper limit,
|
||||
/// esp. as calculating the number of graphemes is not simple
|
||||
/// (one graphemes may be a sequence of code points which is a sequence of bytes).
|
||||
/// also even if we have the exact number of graphemes,
|
||||
/// that would not always help on getting an idea about the screen space used
|
||||
/// (to keep bubbles and chat flow usable).
|
||||
///
|
||||
/// therefore, the number of bytes is only a very rough estimation,
|
||||
/// however, the ~30K seems to work okayish for a while,
|
||||
/// if it turns out, it is too few for some alphabet, we can still increase.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = 29_000;
|
||||
|
||||
/// approx. max. length (number of bytes) returned by dc_msg_get_text()
|
||||
pub const DC_MAX_GET_TEXT_LEN: usize = 30_000;
|
||||
|
||||
/// approx. max. length returned by dc_get_msg_info()
|
||||
const DC_MAX_GET_INFO_LEN: usize = 100_000;
|
||||
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
|
||||
|
||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
||||
@@ -199,47 +227,15 @@ pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
|
||||
|
||||
/// Connect to IMAP via STARTTLS.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
|
||||
|
||||
/// Connect to IMAP via SSL.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 0x200;
|
||||
|
||||
/// Connect to IMAP unencrypted, this should not be used.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_IMAP_SOCKET_PLAIN: i32 = 0x400;
|
||||
|
||||
/// Connect to SMTP via STARTTLS.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_SMTP_SOCKET_STARTTLS: usize = 0x10000;
|
||||
|
||||
/// Connect to SMTP via SSL.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
|
||||
|
||||
/// Connect to SMTP unencrypted, this should not be used.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
||||
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
||||
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
|
||||
|
||||
// QR code scanning (view from Bob, the joiner)
|
||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
||||
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
|
||||
pub const DC_BOB_ERROR: i32 = 0;
|
||||
pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
/// How many existing messages shall be fetched after configuration.
|
||||
pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
|
||||
|
||||
// max. width/height of an avatar
|
||||
pub const AVATAR_SIZE: u32 = 192;
|
||||
pub const BALANCED_AVATAR_SIZE: u32 = 256;
|
||||
pub const WORSE_AVATAR_SIZE: u32 = 128;
|
||||
|
||||
// max. width/height of images
|
||||
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
@@ -248,6 +244,11 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
|
||||
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
|
||||
// this does not affect MIME'e `To:` header.
|
||||
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
|
||||
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
@@ -262,7 +263,7 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
pub enum Viewtype {
|
||||
Unknown = 0,
|
||||
|
||||
@@ -321,26 +322,6 @@ impl Default for Viewtype {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
}
|
||||
|
||||
// These constants are used as events
|
||||
// reported to the callback given to dc_context_new().
|
||||
// If you do not want to handle an event, it is always safe to return 0,
|
||||
// so there is no need to add a "case" for every event.
|
||||
|
||||
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
|
||||
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
|
||||
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
|
||||
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
|
||||
|
||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
@@ -349,3 +330,100 @@ pub enum KeyType {
|
||||
Public = 0,
|
||||
Private = 1,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
fn test_derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::default());
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
|
||||
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
|
||||
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
|
||||
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
|
||||
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
|
||||
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chattype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Chattype::Undefined, Chattype::default());
|
||||
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
|
||||
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
||||
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
|
||||
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keygentype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::default());
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
|
||||
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keytype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(ShowEmails::Off, ShowEmails::default());
|
||||
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
ShowEmails::AcceptedContacts,
|
||||
ShowEmails::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocked_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Blocked::Not, Blocked::default());
|
||||
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
|
||||
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Deaddrop, Blocked::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mediaquality_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(MediaQuality::Balanced, MediaQuality::default());
|
||||
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
|
||||
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_videochattype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::default());
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
VideochatType::BasicWebrtc,
|
||||
VideochatType::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
1158
src/contact.rs
1158
src/contact.rs
File diff suppressed because it is too large
Load Diff
656
src/context.rs
656
src/context.rs
@@ -3,25 +3,28 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::time::{Instant, SystemTime};
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||
use async_std::task;
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use async_std::{
|
||||
channel::{self, Receiver, Sender},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
task,
|
||||
};
|
||||
|
||||
use crate::chat::*;
|
||||
use crate::chat::{get_chat_cnt, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::dc_tools::duration_to_str;
|
||||
use crate::error::*;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::Contact;
|
||||
use crate::dc_tools::{duration_to_str, time};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::securejoin::Bob;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Context {
|
||||
@@ -44,7 +47,7 @@ pub struct InnerContext {
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) os_name: Option<String>,
|
||||
pub(crate) bob: RwLock<BobStatus>,
|
||||
pub(crate) bob: Bob,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
@@ -59,7 +62,12 @@ pub struct InnerContext {
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
|
||||
|
||||
/// Id for this context on the current device.
|
||||
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
|
||||
|
||||
/// ID for this `Context` in the current process.
|
||||
///
|
||||
/// This allows for multiple `Context`s open in a single process where each context can
|
||||
/// be identified by this ID.
|
||||
pub(crate) id: u32,
|
||||
|
||||
creation_time: SystemTime,
|
||||
@@ -83,6 +91,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
res.insert("num_cpus", num_cpus::get().to_string());
|
||||
res.insert("level", "awesome".into());
|
||||
res
|
||||
}
|
||||
@@ -121,7 +130,7 @@ impl Context {
|
||||
os_name: Some(os_name),
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(),
|
||||
bob: RwLock::new(Default::default()),
|
||||
bob: Default::default(),
|
||||
last_smeared_timestamp: RwLock::new(0),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
@@ -131,15 +140,13 @@ impl Context {
|
||||
scheduler: RwLock::new(Scheduler::Stopped),
|
||||
ephemeral_task: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
inner: Arc::new(inner),
|
||||
};
|
||||
ensure!(
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
|
||||
"Failed opening sqlite database"
|
||||
);
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
@@ -147,7 +154,7 @@ impl Context {
|
||||
/// Starts the IO scheduler.
|
||||
pub async fn start_io(&self) {
|
||||
info!(self, "starting IO");
|
||||
if self.is_io_running().await {
|
||||
if self.inner.is_io_running().await {
|
||||
info!(self, "IO is already running");
|
||||
return;
|
||||
}
|
||||
@@ -158,18 +165,9 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the IO scheduler is running.
|
||||
pub async fn is_io_running(&self) -> bool {
|
||||
self.inner.is_io_running().await
|
||||
}
|
||||
|
||||
/// Stops the IO scheduler.
|
||||
pub async fn stop_io(&self) {
|
||||
info!(self, "stopping IO");
|
||||
if !self.is_io_running().await {
|
||||
info!(self, "IO is not running");
|
||||
return;
|
||||
}
|
||||
|
||||
self.inner.stop_io().await;
|
||||
}
|
||||
@@ -200,7 +198,10 @@ impl Context {
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the next queued event.
|
||||
/// Returns a receiver for emitted events.
|
||||
///
|
||||
/// Multiple emitters can be created, but note that in this case each emitted event will
|
||||
/// only be received by one of the emitters, not by all of them.
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
@@ -222,7 +223,7 @@ impl Context {
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
let (sender, receiver) = channel(1);
|
||||
let (sender, receiver) = channel::bounded(1);
|
||||
s.cancel_sender = Some(sender);
|
||||
|
||||
Ok(receiver)
|
||||
@@ -249,7 +250,9 @@ impl Context {
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
if let Some(cancel) = s.cancel_sender.take() {
|
||||
cancel.send(()).await;
|
||||
if let Err(err) = cancel.send(()).await {
|
||||
warn!(self, "could not cancel ongoing: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if s.ongoing_running && !s.shall_stop_ongoing {
|
||||
@@ -268,64 +271,68 @@ impl Context {
|
||||
* UI chat/message related API
|
||||
******************************************************************************/
|
||||
|
||||
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let unset = "0";
|
||||
let l = LoginParam::from_database(self, "").await;
|
||||
let l2 = LoginParam::from_database(self, "configured_").await;
|
||||
let displayname = self.get_config(Config::Displayname).await;
|
||||
let chats = get_chat_cnt(self).await as usize;
|
||||
let l = LoginParam::from_database(self, "").await?;
|
||||
let l2 = LoginParam::from_database(self, "configured_").await?;
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await? as usize;
|
||||
let real_msgs = message::get_real_msg_cnt(self).await as usize;
|
||||
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
|
||||
let contacts = Contact::get_real_cnt(self).await as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured).await;
|
||||
let contacts = Contact::get_real_cnt(self).await? as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured).await?;
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "dbversion")
|
||||
.await
|
||||
.get_raw_config_int("dbversion")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let journal_mode = self
|
||||
.sql
|
||||
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
|
||||
.await
|
||||
.query_get_value("PRAGMA journal_mode;", paramsv![])
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await;
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
|
||||
let prv_key_cnt: Option<isize> = self
|
||||
let prv_key_cnt = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await;
|
||||
.count("SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await?;
|
||||
|
||||
let pub_key_cnt: Option<isize> = self
|
||||
let pub_key_cnt = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||
.await;
|
||||
.count("SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||
.await?;
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
|
||||
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
|
||||
let folders_configured = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "folders_configured")
|
||||
.await
|
||||
.get_raw_config_int("folders_configured")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let configured_sentbox_folder = self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_mvbox_folder = self
|
||||
.get_config(Config::ConfiguredMvboxFolder)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
|
||||
let mut res = get_info();
|
||||
|
||||
// insert values
|
||||
res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
|
||||
res.insert("number_of_chats", chats.to_string());
|
||||
res.insert("number_of_chat_messages", real_msgs.to_string());
|
||||
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
|
||||
@@ -338,41 +345,90 @@ impl Context {
|
||||
res.insert(
|
||||
"selfavatar",
|
||||
self.get_config(Config::Selfavatar)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2.to_string());
|
||||
res.insert(
|
||||
"fetch_existing_msgs",
|
||||
self.get_config_int(Config::FetchExistingMsgs)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert("inbox_watch", inbox_watch.to_string());
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_watch", mvbox_watch.to_string());
|
||||
res.insert("mvbox_move", mvbox_move.to_string());
|
||||
res.insert("sentbox_move", sentbox_move.to_string());
|
||||
res.insert("folders_configured", folders_configured.to_string());
|
||||
res.insert("configured_sentbox_folder", configured_sentbox_folder);
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert(
|
||||
"key_gen_type",
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert(
|
||||
"private_key_count",
|
||||
prv_key_cnt.unwrap_or_default().to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"public_key_count",
|
||||
pub_key_cnt.unwrap_or_default().to_string(),
|
||||
);
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
res.insert(
|
||||
"webrtc_instance",
|
||||
self.get_config(Config::WebrtcInstance)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"media_quality",
|
||||
self.get_config_int(Config::MediaQuality).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_device_after",
|
||||
self.get_config_int(Config::DeleteDeviceAfter)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_server_after",
|
||||
self.get_config_int(Config::DeleteServerAfter)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_housekeeping",
|
||||
self.get_config_int(Config::LastHousekeeping)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"scan_all_folders_debounce_secs",
|
||||
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
|
||||
res
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
|
||||
let show_deaddrop: i32 = 0;
|
||||
self.sql
|
||||
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
|
||||
///
|
||||
/// The list starts with the most recent message
|
||||
/// and is typically used to show notifications.
|
||||
/// Moreover, the number of returned messages
|
||||
/// can be used for a badge counter on the app icon.
|
||||
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
@@ -383,67 +439,41 @@ impl Context {
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.state=?",
|
||||
" AND m.hidden=0",
|
||||
" AND m.chat_id>?",
|
||||
" AND m.chat_id>9",
|
||||
" AND ct.blocked=0",
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" AND c.blocked=0",
|
||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||
paramsv![MessageState::InFresh, time()],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
ret.push(row?);
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(ret)
|
||||
Ok(list)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
|
||||
let real_query = query.as_ref().trim();
|
||||
/// Searches for messages containing the query string.
|
||||
///
|
||||
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
||||
/// is `None` this searches messages from all chats.
|
||||
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
|
||||
let real_query = query.trim();
|
||||
if real_query.is_empty() {
|
||||
return Vec::new();
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let strLikeInText = format!("%{}%", real_query);
|
||||
let strLikeBeg = format!("{}%", real_query);
|
||||
let str_like_in_text = format!("%{}%", real_query);
|
||||
|
||||
let query = if !chat_id.is_unset() {
|
||||
concat!(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" WHERE m.chat_id=?",
|
||||
" AND m.hidden=0",
|
||||
" AND ct.blocked=0",
|
||||
" AND (txt LIKE ? OR ct.name LIKE ?)",
|
||||
" ORDER BY m.timestamp,m.id;"
|
||||
)
|
||||
} else {
|
||||
concat!(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.chat_id>9",
|
||||
" AND m.hidden=0",
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" AND ct.blocked=0",
|
||||
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
)
|
||||
};
|
||||
|
||||
self.sql
|
||||
.query_map(
|
||||
let do_query = |query, params| {
|
||||
self.sql.query_map(
|
||||
query,
|
||||
paramsv![chat_id, strLikeInText, strLikeBeg],
|
||||
params,
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
@@ -453,23 +483,75 @@ impl Context {
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND ct.blocked=0
|
||||
AND txt LIKE ?
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
paramsv![chat_id, str_like_in_text],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// For performance reasons results are sorted only by `id`, that is in the order of
|
||||
// message reception.
|
||||
//
|
||||
// Unlike chat view, sorting by `timestamp` is not necessary but slows down the query by
|
||||
// ~25% according to benchmarks.
|
||||
//
|
||||
// To speed up incremental search, where queries for few characters usually return lots
|
||||
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
do_query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.chat_id>9
|
||||
AND m.hidden=0
|
||||
AND c.blocked=0
|
||||
AND ct.blocked=0
|
||||
AND m.txt LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
paramsv![str_like_in_text],
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredInboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_inbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
|
||||
Ok(inbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredSentboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
||||
|
||||
Ok(sentbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
|
||||
Ok(mvbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
|
||||
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
|
||||
|
||||
Ok(spam.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
|
||||
@@ -486,14 +568,19 @@ impl InnerContext {
|
||||
}
|
||||
|
||||
async fn stop_io(&self) {
|
||||
assert!(self.is_io_running().await, "context is already stopped");
|
||||
let token = {
|
||||
let lock = &*self.scheduler.read().await;
|
||||
lock.pre_stop().await
|
||||
};
|
||||
{
|
||||
let lock = &mut *self.scheduler.write().await;
|
||||
lock.stop(token).await;
|
||||
if self.is_io_running().await {
|
||||
let token = {
|
||||
let lock = &*self.scheduler.read().await;
|
||||
lock.pre_stop().await
|
||||
};
|
||||
{
|
||||
let lock = &mut *self.scheduler.write().await;
|
||||
lock.stop(token).await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ephemeral_task) = self.ephemeral_task.write().await.take() {
|
||||
ephemeral_task.cancel().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -508,13 +595,6 @@ impl Default for RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct BobStatus {
|
||||
pub expects: i32,
|
||||
pub status: i32,
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
|
||||
pub fn get_version_str() -> &'static str {
|
||||
&DC_VERSION_STR
|
||||
}
|
||||
@@ -523,7 +603,16 @@ pub fn get_version_str() -> &'static str {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::*;
|
||||
use crate::chat::{
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::TestContext;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_wrong_db() {
|
||||
@@ -537,10 +626,146 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let fresh = t.ctx.get_fresh_msgs().await;
|
||||
let fresh = t.get_fresh_msgs().await.unwrap();
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::load_from_db(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
|
||||
);
|
||||
println!("{}", msg);
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
// receive various mails in 3 chats
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
// mute one of the chats
|
||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||
|
||||
// receive more messages
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
// unmute claire again
|
||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msgs_and_muted_until() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
// have the same view to the database.
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with mute_until in the future
|
||||
set_muted(
|
||||
&t,
|
||||
bob.id,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with mute_until in the past,
|
||||
// we need to modify the database directly
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![time() - 3600, bob.id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with "forever" mute_until
|
||||
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=-2 WHERE id=?;",
|
||||
paramsv![bob.id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -580,7 +805,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -596,14 +821,14 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = TestContext::new().await;
|
||||
std::mem::drop(t.ctx);
|
||||
std::mem::drop(t);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let info = t.ctx.get_info().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
assert!(info.get("database_dir").is_some());
|
||||
}
|
||||
|
||||
@@ -614,4 +839,137 @@ mod tests {
|
||||
assert!(info.get("database_dir").is_none());
|
||||
assert_eq!(info.get("level").unwrap(), "awesome");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_info_completeness() {
|
||||
// For easier debugging,
|
||||
// get_info() shall return all important information configurable by the Config-values.
|
||||
//
|
||||
// There are exceptions for Config-values considered to be unimportant,
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
"mail_user",
|
||||
"mail_pw",
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"save_mime_headers",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
"send_pw",
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"smtp_certificate_checks",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
for key in Config::iter() {
|
||||
let key: String = key.to_string();
|
||||
if !skip_from_get_info.contains(&&*key)
|
||||
&& !key.starts_with("configured")
|
||||
&& !key.starts_with("sys.")
|
||||
{
|
||||
assert!(
|
||||
info.contains_key(&*key),
|
||||
"'{}' missing in get_info() output",
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let self_talk = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Global search finds nothing.
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds nothing.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_text(Some("foobar".to_string()));
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_text(Some("barbaz".to_string()));
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
let res = alice.search_msgs(None, "ob").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Global search for "bar" matches both "foobar" and "barbaz".
|
||||
let res = alice.search_msgs(None, "bar").await?;
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
// Message added later is returned first.
|
||||
assert_eq!(res.get(0), Some(&msg2.id));
|
||||
assert_eq!(res.get(1), Some(&msg1.id));
|
||||
|
||||
// Global search with longer text does not find any message.
|
||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search for random string finds nothing.
|
||||
let res = alice.search_msgs(None, "abc").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds the message.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Search in Saved Messages does not find the message.
|
||||
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foobar".to_string()));
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 999);
|
||||
|
||||
// Add one more message, no limit yet
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// Add one more message, that one is truncated then
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// In-chat should not be not limited
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
390
src/dc_tools.rs
390
src/dc_tools.rs
@@ -9,26 +9,27 @@ use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use chrono::{Local, TimeZone};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::chat::{add_device_msg, add_device_msg_with_importance};
|
||||
use crate::constants::{Viewtype, DC_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
|
||||
use crate::context::Context;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::events::EventType;
|
||||
|
||||
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
0 != v && 0 == v & (v - 1)
|
||||
}
|
||||
use crate::message::Message;
|
||||
use crate::provider::get_provider_update_timestamp;
|
||||
use crate::stock_str;
|
||||
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let ellipse = "[...]";
|
||||
|
||||
let count = buf.chars().count();
|
||||
if approx_chars > 0 && count > approx_chars + ellipse.len() {
|
||||
if approx_chars > 0 && count > approx_chars + DC_ELLIPSE.len() {
|
||||
let end_pos = buf
|
||||
.char_indices()
|
||||
.nth(approx_chars)
|
||||
@@ -36,40 +37,15 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
|
||||
Cow::Owned(format!("{}{}", &buf[..=index], ellipse))
|
||||
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSE))
|
||||
} else {
|
||||
Cow::Owned(format!("{}{}", &buf[..end_pos], ellipse))
|
||||
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSE))
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// the colors must fulfill some criterions as:
|
||||
/// - contrast to black and to white
|
||||
/// - work as a text-color
|
||||
/// - being noticeable on a typical map
|
||||
/// - harmonize together while being different enough
|
||||
/// (therefore, we cannot just use random rgb colors :)
|
||||
const COLORS: [u32; 16] = [
|
||||
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
|
||||
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
|
||||
];
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
||||
let str_lower = s.as_ref().to_lowercase();
|
||||
let mut checksum = 0;
|
||||
let bytes = str_lower.as_bytes();
|
||||
for (i, byte) in bytes.iter().enumerate() {
|
||||
checksum += (i + 1) * *byte as usize;
|
||||
checksum %= 0x00ff_ffff;
|
||||
}
|
||||
let color_index = checksum % COLORS.len();
|
||||
|
||||
COLORS[color_index]
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
* date/time tools
|
||||
******************************************************************************/
|
||||
@@ -153,6 +129,67 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize
|
||||
start
|
||||
}
|
||||
|
||||
// if the system time is not plausible, once a day, add a device message.
|
||||
// for testing we're using time() as that is also used for message timestamps.
|
||||
// moreover, add a warning if the app is outdated.
|
||||
pub(crate) async fn maybe_add_time_based_warnings(context: &Context) {
|
||||
if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await {
|
||||
maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestamp: i64) -> bool {
|
||||
if now < known_past_timestamp {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(
|
||||
stock_str::bad_time_msg_body(
|
||||
context,
|
||||
Local
|
||||
.timestamp(now, 0)
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string(),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
add_device_msg_with_importance(
|
||||
context,
|
||||
Some(
|
||||
format!(
|
||||
"bad-time-warning-{}",
|
||||
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day
|
||||
)
|
||||
.as_str(),
|
||||
),
|
||||
Some(&mut msg),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
|
||||
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::update_reminder_msg_body(context).await);
|
||||
add_device_msg(
|
||||
context,
|
||||
Some(
|
||||
format!(
|
||||
"outdated-warning-{}",
|
||||
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month
|
||||
)
|
||||
.as_str(),
|
||||
),
|
||||
Some(&mut msg),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
/* Message-ID tools */
|
||||
pub(crate) fn dc_create_id() -> String {
|
||||
/* generate an id. the generated ID should be as short and as unique as possible:
|
||||
@@ -296,6 +333,23 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
|
||||
match async_std::fs::read_dir(path).await {
|
||||
Ok(mut read_dir) => {
|
||||
while let Some(entry) = read_dir.next().await {
|
||||
match entry {
|
||||
Ok(file) => {
|
||||
dc_delete_file(context, file.file_name()).await;
|
||||
}
|
||||
Err(e) => warn!(context, "Could not read file to delete: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => warn!(context, "Could not read dir to delete: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_copy_file(
|
||||
context: &Context,
|
||||
src_path: impl AsRef<Path>,
|
||||
@@ -450,34 +504,15 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_next_backup_path_old(
|
||||
folder: impl AsRef<Path>,
|
||||
backup_time: i64,
|
||||
) -> Result<PathBuf, Error> {
|
||||
let folder = PathBuf::from(folder.as_ref());
|
||||
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
|
||||
.format("delta-chat-%Y-%m-%d")
|
||||
.to_string();
|
||||
|
||||
// 64 backup files per day should be enough for everyone
|
||||
for i in 0..64 {
|
||||
let mut path = folder.clone();
|
||||
path.push(format!("{}-{}.bak", stem, i));
|
||||
if !path.exists().await {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
bail!("could not create backup file, disk full?");
|
||||
}
|
||||
|
||||
/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded,
|
||||
/// it can be renamed to dest_path. This guarantees that the backup is complete.
|
||||
pub(crate) async fn get_next_backup_path_new(
|
||||
pub(crate) async fn get_next_backup_path(
|
||||
folder: impl AsRef<Path>,
|
||||
backup_time: i64,
|
||||
) -> Result<(PathBuf, PathBuf), Error> {
|
||||
let folder = PathBuf::from(folder.as_ref());
|
||||
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
|
||||
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
|
||||
.format("delta-chat-backup-%Y-%m-%d")
|
||||
.to_string();
|
||||
|
||||
@@ -605,20 +640,54 @@ impl rusqlite::types::ToSql for EmailAddress {
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility to check if a in the binary represantion of listflags
|
||||
/// the bit at position bitindex is 1.
|
||||
pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
|
||||
let listflags = listflags as usize;
|
||||
(listflags & bitindex) == bitindex
|
||||
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
|
||||
pub(crate) fn improve_single_line_input(input: &str) -> String {
|
||||
input
|
||||
.replace("\n", " ")
|
||||
.replace("\r", " ")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) trait IsNoneOrEmpty<T> {
|
||||
fn is_none_or_empty(&self) -> bool;
|
||||
}
|
||||
impl<T> IsNoneOrEmpty<T> for Option<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn is_none_or_empty(&self) -> bool {
|
||||
!matches!(self, Some(s) if !s.as_ref().is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_subject_prefix(last_subject: &str) -> String {
|
||||
let subject_start = if last_subject.starts_with("Chat:") {
|
||||
0
|
||||
} else {
|
||||
// "Antw:" is the longest abbreviation in
|
||||
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
|
||||
// so look at the first _5_ characters:
|
||||
match last_subject.chars().take(5).position(|c| c == ':') {
|
||||
Some(prefix_end) => prefix_end + 1,
|
||||
None => 0,
|
||||
}
|
||||
};
|
||||
last_subject
|
||||
.chars()
|
||||
.skip(subject_start)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::convert::TryInto;
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::test_utils::*;
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_rust_ftoa() {
|
||||
@@ -773,6 +842,9 @@ mod tests {
|
||||
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
|
||||
}
|
||||
|
||||
use crate::chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
@@ -804,7 +876,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_file_handling() {
|
||||
let t = TestContext::new().await;
|
||||
let context = &t.ctx;
|
||||
let context = &t;
|
||||
macro_rules! dc_file_exist {
|
||||
($ctx:expr, $fname:expr) => {
|
||||
$ctx.get_blobdir()
|
||||
@@ -814,16 +886,6 @@ mod tests {
|
||||
}
|
||||
|
||||
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
|
||||
if dc_file_exist!(context, "$BLOBDIR/foobar").await
|
||||
|| dc_file_exist!(context, "$BLOBDIR/dada").await
|
||||
|| dc_file_exist!(context, "$BLOBDIR/foobar.dadada").await
|
||||
|| dc_file_exist!(context, "$BLOBDIR/foobar-folder").await
|
||||
{
|
||||
dc_delete_file(context, "$BLOBDIR/foobar").await;
|
||||
dc_delete_file(context, "$BLOBDIR/dada").await;
|
||||
dc_delete_file(context, "$BLOBDIR/foobar.dadada").await;
|
||||
dc_delete_file(context, "$BLOBDIR/foobar-folder").await;
|
||||
}
|
||||
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
|
||||
.await
|
||||
.is_ok());
|
||||
@@ -866,29 +928,15 @@ mod tests {
|
||||
assert!(!dc_file_exist!(context, &fn0).await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_listflags_has() {
|
||||
let listflags: u32 = 0x1101;
|
||||
assert!(listflags_has(listflags, 0x1));
|
||||
assert!(!listflags_has(listflags, 0x10));
|
||||
assert!(listflags_has(listflags, 0x100));
|
||||
assert!(listflags_has(listflags, 0x1000));
|
||||
let listflags: u32 = (DC_GCL_ADD_SELF | DC_GCL_VERIFIED_ONLY).try_into().unwrap();
|
||||
assert!(listflags_has(listflags, DC_GCL_VERIFIED_ONLY));
|
||||
assert!(listflags_has(listflags, DC_GCL_ADD_SELF));
|
||||
let listflags: u32 = DC_GCL_VERIFIED_ONLY.try_into().unwrap();
|
||||
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_smeared_timestamp() {
|
||||
let t = TestContext::new().await;
|
||||
assert_ne!(
|
||||
dc_create_smeared_timestamp(&t.ctx).await,
|
||||
dc_create_smeared_timestamp(&t.ctx).await
|
||||
dc_create_smeared_timestamp(&t).await,
|
||||
dc_create_smeared_timestamp(&t).await
|
||||
);
|
||||
assert!(
|
||||
dc_create_smeared_timestamp(&t.ctx).await
|
||||
dc_create_smeared_timestamp(&t).await
|
||||
>= SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -900,13 +948,13 @@ mod tests {
|
||||
async fn test_create_smeared_timestamps() {
|
||||
let t = TestContext::new().await;
|
||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||
let next = dc_smeared_time(&t.ctx).await;
|
||||
let start = dc_create_smeared_timestamps(&t, count as usize).await;
|
||||
let next = dc_smeared_time(&t).await;
|
||||
assert!((start + count - 1) < next);
|
||||
|
||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||
let next = dc_smeared_time(&t.ctx).await;
|
||||
let start = dc_create_smeared_timestamps(&t, count as usize).await;
|
||||
let next = dc_smeared_time(&t).await;
|
||||
assert!((start + count - 1) < next);
|
||||
}
|
||||
|
||||
@@ -960,4 +1008,152 @@ mod tests {
|
||||
assert_eq!(w, 100);
|
||||
assert_eq!(h, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_improve_single_line_input() {
|
||||
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
|
||||
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_maybe_warn_on_bad_time() {
|
||||
let t = TestContext::new().await;
|
||||
let timestamp_now = time();
|
||||
let timestamp_future = timestamp_now + 60 * 60 * 24 * 7;
|
||||
let timestamp_past = NaiveDateTime::new(
|
||||
NaiveDate::from_ymd(2020, 9, 1),
|
||||
NaiveTime::from_hms(0, 0, 0),
|
||||
)
|
||||
.timestamp_millis()
|
||||
/ 1_000;
|
||||
|
||||
// a correct time must not add a device message
|
||||
maybe_warn_on_bad_time(&t, timestamp_now, get_provider_update_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// we cannot find out if a date in the future is wrong - a device message is not added
|
||||
maybe_warn_on_bad_time(&t, timestamp_future, get_provider_update_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// a date in the past must add a device message
|
||||
maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// the message should be added only once a day - test that an hour later and nearly a day later
|
||||
maybe_warn_on_bad_time(
|
||||
&t,
|
||||
timestamp_past + 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
maybe_warn_on_bad_time(
|
||||
&t,
|
||||
timestamp_past + 60 * 60 * 24 - 1,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// next day, there should be another device message
|
||||
maybe_warn_on_bad_time(
|
||||
&t,
|
||||
timestamp_past + 60 * 60 * 24,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(device_chat_id, chats.get_chat_id(0));
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_maybe_warn_on_outdated() {
|
||||
let t = TestContext::new().await;
|
||||
let timestamp_now: i64 = time();
|
||||
|
||||
// in about 6 months, the app should not be outdated
|
||||
// (if this fails, provider-db is not updated since 6 months)
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + 180 * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// in 1 year, the app should be considered as outdated
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + 365 * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// do not repeat the warning every day ...
|
||||
// (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message)
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + (365 + 1) * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + (365 + 2) * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let test_len = msgs.len();
|
||||
assert!(test_len == 1 || test_len == 2);
|
||||
|
||||
// ... but every month
|
||||
// (forward generous 33 days to avoid being in the same month as in the previous check)
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + (365 + 33) * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), test_len + 1);
|
||||
}
|
||||
}
|
||||
|
||||
225
src/dehtml.rs
225
src/dehtml.rs
@@ -2,20 +2,54 @@
|
||||
//!
|
||||
//! A module to remove HTML tags from the email text
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
use std::io::BufRead;
|
||||
|
||||
lazy_static! {
|
||||
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
|
||||
}
|
||||
use once_cell::sync::Lazy;
|
||||
use quick_xml::{
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
Reader,
|
||||
};
|
||||
|
||||
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
struct Dehtml {
|
||||
strbuilder: String,
|
||||
add_text: AddText,
|
||||
last_href: Option<String>,
|
||||
/// GMX wraps a quote in `<div name="quote">`. After a `<div name="quote">`, this count is
|
||||
/// increased at each `<div>` and decreased at each `</div>`. This way we know when the quote ends.
|
||||
/// If this is > `0`, then we are inside a `<div name="quote">`
|
||||
divs_since_quote_div: u32,
|
||||
/// Everything between <div name="quote"> and <div name="quoted-content"> is usually metadata
|
||||
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
|
||||
divs_since_quoted_content_div: u32,
|
||||
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
|
||||
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
|
||||
blockquotes_since_blockquote: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
impl Dehtml {
|
||||
fn line_prefix(&self) -> &str {
|
||||
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
|
||||
"> "
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
fn append_prefix(&self, line_end: impl AsRef<str>) -> String {
|
||||
// line_end is e.g. "\n\n". We add "> " if necessary.
|
||||
line_end.as_ref().to_owned() + self.line_prefix()
|
||||
}
|
||||
fn get_add_text(&self) -> AddText {
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
AddText::No // Everything between <div name="quoted"> and <div name="quoted_content"> is metadata which we don't want
|
||||
} else {
|
||||
self.add_text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum AddText {
|
||||
No,
|
||||
YesRemoveLineEnds,
|
||||
@@ -24,13 +58,28 @@ enum AddText {
|
||||
|
||||
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
|
||||
// the newlines are typically removed in further processing by the caller
|
||||
pub fn dehtml(buf: &str) -> String {
|
||||
let buf = buf.trim();
|
||||
pub fn dehtml(buf: &str) -> Option<String> {
|
||||
let s = dehtml_quick_xml(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return Some(s);
|
||||
}
|
||||
let s = dehtml_manually(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return Some(s);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn dehtml_quick_xml(buf: &str) -> String {
|
||||
let buf = buf.trim().trim_start_matches("<!doctype html>");
|
||||
|
||||
let mut dehtml = Dehtml {
|
||||
strbuilder: String::with_capacity(buf.len()),
|
||||
add_text: AddText::YesRemoveLineEnds,
|
||||
last_href: None,
|
||||
divs_since_quote_div: 0,
|
||||
divs_since_quoted_content_div: 0,
|
||||
blockquotes_since_blockquote: 0,
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
@@ -69,13 +118,16 @@ pub fn dehtml(buf: &str) -> String {
|
||||
}
|
||||
|
||||
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
if dehtml.add_text == AddText::YesPreserveLineEnds
|
||||
|| dehtml.add_text == AddText::YesRemoveLineEnds
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
|
||||
|
||||
if dehtml.add_text == AddText::YesRemoveLineEnds {
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
|
||||
} else if !dehtml.line_prefix().is_empty() {
|
||||
let l = dehtml.append_prefix("\n");
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
|
||||
} else {
|
||||
dehtml.strbuilder += &last_added;
|
||||
}
|
||||
@@ -83,13 +135,16 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
}
|
||||
|
||||
fn dehtml_cdata_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
if dehtml.add_text == AddText::YesPreserveLineEnds
|
||||
|| dehtml.add_text == AddText::YesRemoveLineEnds
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
|
||||
|
||||
if dehtml.add_text == AddText::YesRemoveLineEnds {
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
|
||||
} else if !dehtml.line_prefix().is_empty() {
|
||||
let l = dehtml.append_prefix("\n");
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
|
||||
} else {
|
||||
dehtml.strbuilder += &last_added;
|
||||
}
|
||||
@@ -100,8 +155,15 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
match tag.as_str() {
|
||||
"p" | "div" | "table" | "td" | "style" | "script" | "title" | "pre" => {
|
||||
dehtml.strbuilder += "\n\n";
|
||||
"p" | "table" | "td" | "style" | "script" | "title" | "pre" => {
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"div" => {
|
||||
pop_tag(&mut dehtml.divs_since_quote_div);
|
||||
pop_tag(&mut dehtml.divs_since_quoted_content_div);
|
||||
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"a" => {
|
||||
@@ -112,11 +174,16 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
}
|
||||
}
|
||||
"b" | "strong" => {
|
||||
dehtml.strbuilder += "*";
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "*";
|
||||
}
|
||||
}
|
||||
"i" | "em" => {
|
||||
dehtml.strbuilder += "_";
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "_";
|
||||
}
|
||||
}
|
||||
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -129,19 +196,27 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
match tag.as_str() {
|
||||
"p" | "div" | "table" | "td" => {
|
||||
dehtml.strbuilder += "\n\n";
|
||||
"p" | "table" | "td" => {
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
#[rustfmt::skip]
|
||||
"div" => {
|
||||
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
|
||||
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
|
||||
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"br" => {
|
||||
dehtml.strbuilder += "\n";
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n");
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"style" | "script" | "title" => {
|
||||
dehtml.add_text = AddText::No;
|
||||
}
|
||||
"pre" => {
|
||||
dehtml.strbuilder += "\n\n";
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
dehtml.add_text = AddText::YesPreserveLineEnds;
|
||||
}
|
||||
"a" => {
|
||||
@@ -162,18 +237,74 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
}
|
||||
}
|
||||
"b" | "strong" => {
|
||||
dehtml.strbuilder += "*";
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "*";
|
||||
}
|
||||
}
|
||||
"i" | "em" => {
|
||||
dehtml.strbuilder += "_";
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "_";
|
||||
}
|
||||
}
|
||||
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
|
||||
/// The `counts`s are stored in the `Dehtml` struct.
|
||||
fn pop_tag(count: &mut u32) {
|
||||
if *count > 0 {
|
||||
*count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
|
||||
/// The `counts`s are stored in the `Dehtml` struct.
|
||||
fn maybe_push_tag(
|
||||
event: &BytesStart,
|
||||
reader: &Reader<impl BufRead>,
|
||||
tag_name: &str,
|
||||
count: &mut u32,
|
||||
) {
|
||||
if *count > 0 || tag_contains_attr(event, reader, tag_name) {
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_contains_attr(event: &BytesStart, reader: &Reader<impl BufRead>, name: &str) -> bool {
|
||||
event.attributes().any(|r| {
|
||||
r.map(|a| {
|
||||
a.unescape_and_decode_value(reader)
|
||||
.map(|v| v == name)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dehtml_manually(buf: &str) -> String {
|
||||
// Just strip out everything between "<" and ">"
|
||||
let mut strbuilder = String::new();
|
||||
let mut show_next_chars = true;
|
||||
for c in buf.chars() {
|
||||
match c {
|
||||
'<' => show_next_chars = false,
|
||||
'>' => show_next_chars = true,
|
||||
_ => {
|
||||
if show_next_chars {
|
||||
strbuilder.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
strbuilder
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::simplify::simplify;
|
||||
|
||||
#[test]
|
||||
fn test_dehtml() {
|
||||
@@ -182,27 +313,33 @@ mod tests {
|
||||
"<a href='https://example.com'> Foo </a>",
|
||||
"[ Foo ](https://example.com)",
|
||||
),
|
||||
("<img href='/foo.png'>", ""),
|
||||
("<b> bar </b>", "* bar *"),
|
||||
("<i>foo</i>", "_foo_"),
|
||||
("<b> bar <i> foo", "* bar _ foo"),
|
||||
("& bar", "& bar"),
|
||||
// Note missing '
|
||||
("<a href='/foo.png>Hi</a> ", ""),
|
||||
// Despite missing ', this should be shown:
|
||||
("<a href='/foo.png>Hi</a> ", "Hi "),
|
||||
(
|
||||
"<a href='https://get.delta.chat/'/>",
|
||||
"[](https://get.delta.chat/)",
|
||||
),
|
||||
("", ""),
|
||||
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
|
||||
// Invalid html (at least DC should show the text if the html is invalid):
|
||||
("<!some invalid html code>\n<b>some text</b>", "some text"),
|
||||
];
|
||||
for (input, output) in cases {
|
||||
assert_eq!(dehtml(input), output);
|
||||
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
|
||||
}
|
||||
let none_cases = vec!["<html> </html>", ""];
|
||||
for input in none_cases {
|
||||
assert_eq!(dehtml(input), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_br() {
|
||||
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
|
||||
let plain = dehtml(html);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
|
||||
}
|
||||
@@ -210,7 +347,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dehtml_parse_href() {
|
||||
let html = "<a href=url>text</a";
|
||||
let plain = dehtml(html);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
assert_eq!(plain, "[text](url)");
|
||||
}
|
||||
@@ -218,7 +355,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dehtml_bold_text() {
|
||||
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
|
||||
let plain = dehtml(html);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
assert_eq!(plain, "text *bold*<>");
|
||||
}
|
||||
@@ -228,7 +365,7 @@ mod tests {
|
||||
let html =
|
||||
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
||||
|
||||
let plain = dehtml(html);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
plain,
|
||||
@@ -251,7 +388,27 @@ mod tests {
|
||||
</body>
|
||||
</html>
|
||||
"##;
|
||||
let txt = dehtml(input);
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(txt.trim(), "lots of text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_tag() {
|
||||
let input = "<html><pre>\ntwo\nlines\n</pre></html>";
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(txt.trim(), "two\nlines");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_quote_div() {
|
||||
let input = include_str!("../test-data/message/gmx-quote-body.eml");
|
||||
let dehtml = dehtml(input).unwrap();
|
||||
println!("{}", dehtml);
|
||||
let (msg, forwarded, cut, top_quote, footer) = simplify(dehtml, false);
|
||||
assert_eq!(msg, "Test");
|
||||
assert_eq!(forwarded, false);
|
||||
assert_eq!(cut, false);
|
||||
assert_eq!(top_quote.as_deref(), Some("test"));
|
||||
assert_eq!(footer, None);
|
||||
}
|
||||
}
|
||||
|
||||
357
src/e2ee.rs
357
src/e2ee.rs
@@ -2,18 +2,18 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::error::*;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::headerdef::HeaderDefMap;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::keyring::Keyring;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::pgp;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -26,9 +26,9 @@ pub struct EncryptHelper {
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await? {
|
||||
None => {
|
||||
bail!("addr not configured!");
|
||||
}
|
||||
@@ -51,23 +51,42 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
/// Determines if we can and should encrypt.
|
||||
///
|
||||
/// For encryption to be enabled, `e2ee_guaranteed` should be true, or strictly more than a half
|
||||
/// of peerstates should prefer encryption. Own preference is counted equally to peer
|
||||
/// preferences, even if message copy is not sent to self.
|
||||
///
|
||||
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
|
||||
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
|
||||
///
|
||||
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
|
||||
pub fn should_encrypt(
|
||||
&self,
|
||||
context: &Context,
|
||||
e2ee_guaranteed: bool,
|
||||
peerstates: &[(Option<Peerstate>, &str)],
|
||||
) -> Result<bool> {
|
||||
if !(self.prefer_encrypt == EncryptPreference::Mutual || e2ee_guaranteed) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for (peerstate, addr) in peerstates {
|
||||
match peerstate {
|
||||
Some(peerstate) => {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !e2ee_guaranteed {
|
||||
info!(context, "peerstate for {:?} is no-encrypt", addr);
|
||||
return Ok(false);
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
|
||||
);
|
||||
match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference => {}
|
||||
EncryptPreference::Mutual => prefer_encrypt_count += 1,
|
||||
EncryptPreference::Reset => {
|
||||
if !e2ee_guaranteed {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
None => {
|
||||
let msg = format!("peerstate for {:?} missing, cannot encrypt", addr);
|
||||
@@ -81,7 +100,11 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
|
||||
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
@@ -90,7 +113,7 @@ impl EncryptHelper {
|
||||
context: &Context,
|
||||
min_verified: PeerstateVerifiedStatus,
|
||||
mail_to_encrypt: lettre_email::PartBuilder,
|
||||
peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
|
||||
peerstates: Vec<(Option<Peerstate>, &str)>,
|
||||
) -> Result<String> {
|
||||
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
|
||||
@@ -130,46 +153,36 @@ pub async fn try_decrypt(
|
||||
let from = mail
|
||||
.headers
|
||||
.get_header(HeaderDef::From_)
|
||||
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
|
||||
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
|
||||
.and_then(|from| from.extract_single_info())
|
||||
.map(|from| from.addr)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut peerstate = None;
|
||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||
|
||||
if message_time > 0 {
|
||||
peerstate = Peerstate::from_addr(context, &from).await?;
|
||||
let mut peerstate = Peerstate::from_addr(context, &from).await?;
|
||||
|
||||
// Apply Autocrypt header
|
||||
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if let Some(ref header) = autocryptheader {
|
||||
peerstate.apply_header(&header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
} else if let Some(ref header) = autocryptheader {
|
||||
let p = Peerstate::from_header(context, header, message_time);
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
/* possibly perform decryption */
|
||||
// Possibly perform decryption
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &from).await?;
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.handle_fingerprint_change(context).await?;
|
||||
if let Some(key) = peerstate.public_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
} else if let Some(key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,48 +194,89 @@ pub async fn try_decrypt(
|
||||
&mut signatures,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
if out_mail.is_none()
|
||||
&& message_time > peerstate.last_seen_autocrypt
|
||||
&& !contains_report(mail)
|
||||
{
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((out_mail, signatures))
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
|
||||
ensure!(
|
||||
mail.ctype.mimetype == "multipart/encrypted",
|
||||
"Not a multipart/encrypted message: {}",
|
||||
mail.ctype.mimetype
|
||||
);
|
||||
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
|
||||
///
|
||||
/// Returns `None` if the message is not a valid PGP/MIME message.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/encrypted" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
ensure!(
|
||||
first_part.ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
first_part.ctype,
|
||||
);
|
||||
|
||||
ensure!(
|
||||
second_part.ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
second_part.ctype
|
||||
);
|
||||
|
||||
Ok(second_part)
|
||||
if first_part.ctype.mimetype == "application/pgp-encrypted"
|
||||
&& second_part.ctype.mimetype == "application/octet-stream"
|
||||
{
|
||||
Some(second_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
bail!("Invalid Autocrypt Level 1 Mime Parts")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message<'a>(
|
||||
/// Returns a reference to the encrypted payload of a ["Mixed
|
||||
/// Up"][pgpmime-message-mangling] message.
|
||||
///
|
||||
/// According to [RFC 3156] encrypted messages should have
|
||||
/// `multipart/encrypted` MIME type and two parts, but Microsoft
|
||||
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
|
||||
/// structure by changing the type to `multipart/mixed` and prepending
|
||||
/// an empty part at the start.
|
||||
///
|
||||
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
|
||||
/// "Empty Message", so we don't check its contents at all, checking
|
||||
/// only for `text/plain` type.
|
||||
///
|
||||
/// Returns `None` if the message is not a "Mixed Up" message.
|
||||
///
|
||||
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
|
||||
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
|
||||
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/mixed" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part, third_part] = &mail.subparts[..] {
|
||||
if first_part.ctype.mimetype == "text/plain"
|
||||
&& second_part.ctype.mimetype == "application/pgp-encrypted"
|
||||
&& third_part.ctype.mimetype == "application/octet-stream"
|
||||
{
|
||||
Some(third_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'a>,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail) {
|
||||
Err(_) => {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
|
||||
None => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(res) => res,
|
||||
Some(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
|
||||
@@ -304,7 +358,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
||||
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
format_err!(concat!(
|
||||
"Failed to get self address, ",
|
||||
@@ -319,7 +373,12 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::*;
|
||||
use crate::chat;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::message::Message;
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
use crate::test_utils::{bob_keypair, TestContext};
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
use super::*;
|
||||
@@ -328,13 +387,13 @@ mod tests {
|
||||
async fn test_prexisting() {
|
||||
let t = TestContext::new().await;
|
||||
let test_addr = t.configure_alice().await;
|
||||
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
|
||||
assert_eq!(ensure_secret_key_exists(&t).await.unwrap(), test_addr);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_not_configured() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
|
||||
assert!(ensure_secret_key_exists(&t).await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,4 +438,162 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
let data = b"blas";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), false);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice sends unencrypted message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives unencrypted message from Alice
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
|
||||
// Parsing a message is enough to update peerstate
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Bob sends encrypted message to Alice
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&bob.ctx, chat_bob, &mut msg).await?;
|
||||
chat::send_msg(&bob.ctx, chat_bob, &mut msg).await?;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice receives encrypted message from Bob
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
|
||||
let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Now Alice and Bob have established keys.
|
||||
|
||||
// Alice sends encrypted message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message with Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, &'static str)> {
|
||||
let addr = "bob@foo.bar";
|
||||
let pub_key = bob_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: addr.into(),
|
||||
last_seen: 13,
|
||||
last_seen_autocrypt: 14,
|
||||
prefer_encrypt,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: Some(pub_key.clone()),
|
||||
gossip_timestamp: 15,
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
vec![(Some(peerstate), addr)]
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_should_encrypt() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
|
||||
|
||||
// test with EncryptPreference::NoPreference:
|
||||
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with EncryptPreference::Reset
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with EncryptPreference::Mutual (self is also Mutual)
|
||||
let ps = new_peerstates(EncryptPreference::Mutual);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with missing peerstate
|
||||
let ps = vec![(None, "bob@foo.bar")];
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_up_mime() -> Result<()> {
|
||||
// "Mixed Up" mail as received when sending an encrypted
|
||||
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
|
||||
// Bridge.
|
||||
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
|
||||
let mail = mailparse::parse_mail(mixed_up_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_none());
|
||||
assert!(get_mixed_up_mime(&mail).is_some());
|
||||
|
||||
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
|
||||
//
|
||||
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
|
||||
// header although the repairing is done by the built-in
|
||||
// OpenPGP support, not Enigmail.
|
||||
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
|
||||
let mail = mailparse::parse_mail(repaired_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_some());
|
||||
assert!(get_mixed_up_mime(&mail).is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
410
src/ephemeral.rs
410
src/ephemeral.rs
@@ -56,24 +56,26 @@
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use async_std::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{send_msg, ChatId};
|
||||
use crate::constants::{
|
||||
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use async_std::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use crate::stock_str;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
@@ -145,10 +147,10 @@ impl rusqlite::types::FromSql for Timer {
|
||||
|
||||
impl ChatId {
|
||||
/// Get ephemeral message timer value in seconds.
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
.query_get_value(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
@@ -164,7 +166,7 @@ impl ChatId {
|
||||
self,
|
||||
context: &Context,
|
||||
timer: Timer,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<()> {
|
||||
ensure!(!self.is_special(), "Invalid chat ID");
|
||||
|
||||
context
|
||||
@@ -187,7 +189,7 @@ impl ChatId {
|
||||
/// Set ephemeral message timer value in seconds.
|
||||
///
|
||||
/// If timer value is 0, disable ephemeral message timer.
|
||||
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
|
||||
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<()> {
|
||||
if timer == self.get_ephemeral_timer(context).await? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -211,29 +213,58 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
timer: Timer,
|
||||
from_id: u32,
|
||||
) -> String {
|
||||
let stock_message = match timer {
|
||||
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
|
||||
match timer {
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
60 => StockMessage::MsgEphemeralTimerMinute,
|
||||
3600 => StockMessage::MsgEphemeralTimerHour,
|
||||
86400 => StockMessage::MsgEphemeralTimerDay,
|
||||
604_800 => StockMessage::MsgEphemeralTimerWeek,
|
||||
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
|
||||
_ => StockMessage::MsgEphemeralTimerEnabled,
|
||||
0..=59 => {
|
||||
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
|
||||
}
|
||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
|
||||
61..=3599 => {
|
||||
stock_str::msg_ephemeral_timer_minutes(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await,
|
||||
3601..=86399 => {
|
||||
stock_str::msg_ephemeral_timer_hours(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await,
|
||||
86401..=604_799 => {
|
||||
stock_str::msg_ephemeral_timer_days(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
|
||||
_ => {
|
||||
stock_str::msg_ephemeral_timer_weeks(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
context
|
||||
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
.query_get_value(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
@@ -246,7 +277,7 @@ impl MsgId {
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time() + i64::from(duration);
|
||||
|
||||
@@ -273,30 +304,35 @@ impl MsgId {
|
||||
/// false. This function does not emit the MsgsChanged event itself,
|
||||
/// because it is also called when chatlist is reloaded, and emitting
|
||||
/// MsgsChanged there will cause infinite reload loop.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool> {
|
||||
let mut updated = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE \
|
||||
ephemeral_timestamp != 0 \
|
||||
AND ephemeral_timestamp < ? \
|
||||
AND chat_id != ?",
|
||||
// If you change which information is removed here, also change MsgId::trash() and
|
||||
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
.context("update failed")?
|
||||
> 0;
|
||||
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
||||
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
|
||||
let self_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let device_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
@@ -321,7 +357,8 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
|
||||
device_chat_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("deleted update failed")?;
|
||||
|
||||
updated |= rows_modified > 0;
|
||||
}
|
||||
@@ -343,13 +380,15 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
|
||||
pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
let ephemeral_timestamp: Option<i64> = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timestamp \
|
||||
FROM msgs \
|
||||
WHERE ephemeral_timestamp != 0 \
|
||||
AND chat_id != ? \
|
||||
ORDER BY ephemeral_timestamp ASC \
|
||||
LIMIT 1",
|
||||
.query_get_value(
|
||||
r#"
|
||||
SELECT ephemeral_timestamp
|
||||
FROM msgs
|
||||
WHERE ephemeral_timestamp != 0
|
||||
AND chat_id != ?
|
||||
ORDER BY ephemeral_timestamp ASC
|
||||
LIMIT 1;
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
@@ -403,10 +442,10 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
///
|
||||
/// It looks up the trash chat too, to find messages that are already
|
||||
/// deleted locally, but not deleted on the server.
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
|
||||
let now = time();
|
||||
|
||||
let threshold_timestamp = match context.get_config_delete_server_after().await {
|
||||
let threshold_timestamp = match context.get_config_delete_server_after().await? {
|
||||
None => 0,
|
||||
Some(delete_server_after) => now - delete_server_after,
|
||||
};
|
||||
@@ -417,12 +456,16 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
timestamp < ? \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp < ?) \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
||||
LIMIT 1",
|
||||
paramsv![threshold_timestamp, now],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
paramsv![threshold_timestamp, now, job::Action::DeleteMsgOnImap],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -436,7 +479,7 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
|
||||
///
|
||||
/// This function is supposed to be called in the background,
|
||||
/// e.g. from housekeeping task.
|
||||
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
|
||||
pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -459,12 +502,19 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::param::Params;
|
||||
use async_std::task::sleep;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::{
|
||||
chat::{self, Chat, ChatItem},
|
||||
dc_tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
let context = TestContext::new().await;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
|
||||
@@ -472,24 +522,78 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await,
|
||||
"Message deletion timer is disabled."
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 1 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 s by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
|
||||
"Message deletion timer is set to 1 s."
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 30 s by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
|
||||
"Message deletion timer is set to 30 s."
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 minute by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 minute."
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 90 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1.5 minutes by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 hour."
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 * 60 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 30 minutes by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 * 60 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 hour by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 5400 },
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1.5 hours by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 60 * 60
|
||||
},
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 2 hours by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -497,10 +601,21 @@ mod tests {
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 day."
|
||||
"Message deletion timer is set to 1 day by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 24 * 60 * 60
|
||||
},
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 2 days by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -508,10 +623,10 @@ mod tests {
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 week."
|
||||
"Message deletion timer is set to 1 week by me."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -519,10 +634,147 @@ mod tests {
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
DC_CONTACT_ID_SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 4 weeks."
|
||||
"Message deletion timer is set to 4 weeks by me."
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_timer() -> anyhow::Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice sends message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends second message to Bob, with no timer
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
// Bob sets ephemeral timer and sends a message about timer change
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent_timer_change = bob.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob receives message from Alice.
|
||||
// Alice message has no timer. However, Bob should not disable timer,
|
||||
// because Alice replies to old message.
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Alice receives message from Bob
|
||||
alice.recv_msg(&sent_timer_change).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_delete_msgs() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
|
||||
t.send_text(chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(chat.id).await;
|
||||
msg.id.delete_from_db(&t).await.unwrap();
|
||||
check_msg_was_deleted(&t, &chat, msg.id).await;
|
||||
|
||||
chat.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = t
|
||||
.send_text(chat.id, "Saved message, disappearing after 1s")
|
||||
.await;
|
||||
|
||||
sleep(Duration::from_millis(1100)).await;
|
||||
|
||||
// Check checks that the msg was deleted locally
|
||||
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
|
||||
|
||||
// Check that the msg will be deleted on the server
|
||||
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_uid=1 WHERE id=?",
|
||||
paramsv![msg.sender_msg_id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let job = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(
|
||||
job,
|
||||
Some(job::Job::new(
|
||||
job::Action::DeleteMsgOnImap,
|
||||
msg.sender_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
))
|
||||
);
|
||||
// Let's assume that executing the job fails on first try and the job is saved to the db
|
||||
job.unwrap().save(&t).await.unwrap();
|
||||
|
||||
// Make sure that we don't get yet another job when loading from db
|
||||
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(job2, None);
|
||||
}
|
||||
|
||||
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
||||
assert!(msg.is_info())
|
||||
}
|
||||
}
|
||||
|
||||
// Check that if there is a message left, the text and metadata are gone
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, 0);
|
||||
assert_eq!(msg.to_id, 0);
|
||||
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user