mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
1168 Commits
update_dep
...
sqlx
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df546b9d2e | ||
|
|
5c13d2322a | ||
|
|
2d5caf9d3e | ||
|
|
876e3ed58e | ||
|
|
cdb5f0d536 | ||
|
|
0d791bb6b3 | ||
|
|
9152f93a46 | ||
|
|
6a4b6fddac | ||
|
|
e3c90aff22 | ||
|
|
b7464f7a5c | ||
|
|
53128cc64b | ||
|
|
ccf8eeacd6 | ||
|
|
aeb8a2e260 | ||
|
|
93797bc82f | ||
|
|
07236efc45 | ||
|
|
0fbddc939b | ||
|
|
a031151587 | ||
|
|
545ff4f7ba | ||
|
|
73e695537a | ||
|
|
16e3c113b7 | ||
|
|
88d7bf49ff | ||
|
|
74ea884aa4 | ||
|
|
16c53637d9 | ||
|
|
f63f0550b0 | ||
|
|
530503932b | ||
|
|
d2dc4edd82 | ||
|
|
8de1bc6cbd | ||
|
|
76e39bfa7c | ||
|
|
cf09942737 | ||
|
|
6fe1f01c5f | ||
|
|
f880d6188b | ||
|
|
22c62ea6af | ||
|
|
e3af3a24a8 | ||
|
|
7bfadb14ea | ||
|
|
75d20b899a | ||
|
|
31a5811241 | ||
|
|
cd1f5bf229 | ||
|
|
632fc19f41 | ||
|
|
7ad95ea165 | ||
|
|
9d7b756ddb | ||
|
|
73412db267 | ||
|
|
059a7bcd7f | ||
|
|
3e47564b2f | ||
|
|
d8be0cdf35 | ||
|
|
26a44b6d32 | ||
|
|
12eacaae36 | ||
|
|
2d8148a1a3 | ||
|
|
916007ed2d | ||
|
|
b91b88e11b | ||
|
|
b6c0f44608 | ||
|
|
2a623541d7 | ||
|
|
0007e93e80 | ||
|
|
c655fd8a64 | ||
|
|
ad531876fd | ||
|
|
53bee68acb | ||
|
|
b5400cf551 | ||
|
|
491af1b583 | ||
|
|
5b1d06cb28 | ||
|
|
7df5195d77 | ||
|
|
baff13ecab | ||
|
|
a7bf05bebb | ||
|
|
aa9b5da1c0 | ||
|
|
dfd705f9c6 | ||
|
|
472c0bcea5 | ||
|
|
8c2af132c8 | ||
|
|
79145576ab | ||
|
|
8ca55b0f60 | ||
|
|
74cb4ca1cd | ||
|
|
351e5dc6f3 | ||
|
|
4eee4a08e7 | ||
|
|
b5fa0f8924 | ||
|
|
baba91c054 | ||
|
|
40c9c2752b | ||
|
|
f4a1a526f5 | ||
|
|
7d80179ed1 | ||
|
|
71080ed6d5 | ||
|
|
44037dd711 | ||
|
|
bc275d8670 | ||
|
|
eb29f9c4c1 | ||
|
|
6340b278d9 | ||
|
|
519e1c1cd0 | ||
|
|
d2320394ca | ||
|
|
9307f2d49f | ||
|
|
7362941245 | ||
|
|
f7c7f414ed | ||
|
|
23d6012c1f | ||
|
|
15b30ceed1 | ||
|
|
45b871f76d | ||
|
|
9f1112833f | ||
|
|
fc88bff32f | ||
|
|
bbf049e95b | ||
|
|
52dfa9b536 | ||
|
|
1fe85dfb3c | ||
|
|
27ff1c4a75 | ||
|
|
adf4035775 | ||
|
|
990c80cedf | ||
|
|
8ebce0c861 | ||
|
|
ffb6a84b1f | ||
|
|
c60ec00aac | ||
|
|
dd3f81a556 | ||
|
|
8938cb2573 | ||
|
|
995660020b | ||
|
|
7997e7dde4 | ||
|
|
20ad98d168 | ||
|
|
c827c9d209 | ||
|
|
bde97b20e9 | ||
|
|
777df24c75 | ||
|
|
e1711855cc | ||
|
|
3899d70b3c | ||
|
|
e7aee5b4f4 | ||
|
|
bd2a7a3d40 | ||
|
|
2e59d5674e | ||
|
|
98b5f768b6 | ||
|
|
b7d0f29002 | ||
|
|
df9cb5e3b8 | ||
|
|
a30486112f | ||
|
|
016b96e30e | ||
|
|
6b763bf417 | ||
|
|
6ded0d3bc1 | ||
|
|
f0837cfa73 | ||
|
|
8350729cbb | ||
|
|
3757e5dca1 | ||
|
|
f02c17cae4 | ||
|
|
e08e817988 | ||
|
|
dad6381519 | ||
|
|
d35cf7d6a2 | ||
|
|
1d34e1f27a | ||
|
|
e03246d105 | ||
|
|
944f1ec005 | ||
|
|
d208905473 | ||
|
|
6d2d31928d | ||
|
|
f5156f3df6 | ||
|
|
554160db15 | ||
|
|
d8bd9b0515 | ||
|
|
27b75103ca | ||
|
|
69e01862b7 | ||
|
|
91f46b1291 | ||
|
|
9de3774715 | ||
|
|
4dbe836dfa | ||
|
|
322cc5a013 | ||
|
|
7cc5243130 | ||
|
|
ba549bd559 | ||
|
|
84be82c670 | ||
|
|
acb42982b7 | ||
|
|
3370c51b35 | ||
|
|
dcfed03702 | ||
|
|
e7dd74e4b1 | ||
|
|
19b53c76da | ||
|
|
95b40ad1d8 | ||
|
|
0efb2215e4 | ||
|
|
0c8f951d8f | ||
|
|
0bb4ef0bd9 | ||
|
|
f93a863f5f | ||
|
|
f263843c5f | ||
|
|
503202376a | ||
|
|
ca70c6a205 | ||
|
|
7d5fba8416 | ||
|
|
3a85b671a1 | ||
|
|
1083cab972 | ||
|
|
7677650b39 | ||
|
|
1f2087190e | ||
|
|
59fadee9e0 | ||
|
|
4a3825c302 | ||
|
|
52e74c241f | ||
|
|
3fa69c1852 | ||
|
|
b3074f854e | ||
|
|
95c5128d9f | ||
|
|
dc17006b16 | ||
|
|
e4a4c230fe | ||
|
|
f56a4450f3 | ||
|
|
913db3b958 | ||
|
|
7de23f86b1 | ||
|
|
35566f5ea5 | ||
|
|
34579974c3 | ||
|
|
c6f19ea0a4 | ||
|
|
64ab955ad7 | ||
|
|
4fdf496cac | ||
|
|
6497e6397d | ||
|
|
d8bbe2fcce | ||
|
|
b6cc44a956 | ||
|
|
0105c831f1 | ||
|
|
d40f96ac65 | ||
|
|
69135709ac | ||
|
|
612a9d012c | ||
|
|
2ad014faf4 | ||
|
|
f3a59e19d8 | ||
|
|
17283c86a3 | ||
|
|
945943a849 | ||
|
|
34c69785d0 | ||
|
|
d5ea4f9b1a | ||
|
|
191009372b | ||
|
|
39faddc74d | ||
|
|
5d1623b98f | ||
|
|
af0dc42df3 | ||
|
|
c18705fae3 | ||
|
|
22973899b8 | ||
|
|
f172e92098 | ||
|
|
e1ff657c78 | ||
|
|
3e6cd3ff34 | ||
|
|
f8680724f8 | ||
|
|
30c76976fc | ||
|
|
f0f020d9d2 | ||
|
|
17a13f0f83 | ||
|
|
ec441b16f1 | ||
|
|
5239f2edad | ||
|
|
cd751a64cb | ||
|
|
6d9ff3d248 | ||
|
|
d97d9980dd | ||
|
|
4ad4d6d10d | ||
|
|
82731ee86c | ||
|
|
04bdfa17f7 | ||
|
|
7a5759de4b | ||
|
|
e29dcbf8eb | ||
|
|
882f90b5ff | ||
|
|
469451d5dd | ||
|
|
af33c2dea7 | ||
|
|
d076ab4d6d | ||
|
|
e66ed8eadb | ||
|
|
05e1c00cd1 | ||
|
|
ca95f25639 | ||
|
|
95cde55a7f | ||
|
|
8756c0cbe1 | ||
|
|
86c6b09814 | ||
|
|
6ce27a7f87 | ||
|
|
7addb15be5 | ||
|
|
7b3a962498 | ||
|
|
41bba7e780 | ||
|
|
419b7d1d5c | ||
|
|
6d8b4a7ec0 | ||
|
|
84963e198e | ||
|
|
408e9946af | ||
|
|
43f49f8917 | ||
|
|
b6161c431b | ||
|
|
a236a619ad | ||
|
|
cdbd3d7d84 | ||
|
|
8efc880b77 | ||
|
|
4bade7e13a | ||
|
|
53099bbfd1 | ||
|
|
7d1d02bf3b | ||
|
|
4f477ec6d2 | ||
|
|
0a4d6fe09b | ||
|
|
8640bd5ee6 | ||
|
|
19a6a30fe2 | ||
|
|
23b6974386 | ||
|
|
103ee966f4 | ||
|
|
6100a23e80 | ||
|
|
9f7f387540 | ||
|
|
307357df70 | ||
|
|
bd903d8e8f | ||
|
|
3db6d5a458 | ||
|
|
4330da232c | ||
|
|
157dd44df0 | ||
|
|
460e60063c | ||
|
|
ec601a3381 | ||
|
|
2156c6cd7a | ||
|
|
13811c06ee | ||
|
|
230d40daa0 | ||
|
|
45aba61ac8 | ||
|
|
2adeadfd73 | ||
|
|
477e689c74 | ||
|
|
00e8f2271a | ||
|
|
9442df0cf8 | ||
|
|
d9de33820f | ||
|
|
f13fbe4398 | ||
|
|
811655bc98 | ||
|
|
0760bfaf7b | ||
|
|
fa3ee4205d | ||
|
|
7f4627356b | ||
|
|
a068b82671 | ||
|
|
177cd52039 | ||
|
|
72d4da0095 | ||
|
|
d4ddc2f9da | ||
|
|
1ab6186eaa | ||
|
|
0ea442ca36 | ||
|
|
e55dc2213a | ||
|
|
05f79c1c01 | ||
|
|
8569e1c18b | ||
|
|
2b1d4651fb | ||
|
|
c53a3d5cb4 | ||
|
|
014d2946b2 | ||
|
|
26b0c43cc4 | ||
|
|
4b4e6e1732 | ||
|
|
d0686ada83 | ||
|
|
c43285f6ac | ||
|
|
3947e90b36 | ||
|
|
916935b8d0 | ||
|
|
229606fcc5 | ||
|
|
23ceda5ad9 | ||
|
|
12e66f5a96 | ||
|
|
371a7552f5 | ||
|
|
641955a1ec | ||
|
|
f97538a399 | ||
|
|
7c8758dc26 | ||
|
|
da7db04c0e | ||
|
|
28ef5164ce | ||
|
|
d1f9563e1f | ||
|
|
70a2dbb4bb | ||
|
|
c43e7cdbdc | ||
|
|
931967353e | ||
|
|
69f095687d | ||
|
|
7b10ec26a3 | ||
|
|
82c85566dc | ||
|
|
c89d7b5b18 | ||
|
|
e8e82d9760 | ||
|
|
9817ccebcf | ||
|
|
ad522cd798 | ||
|
|
fedc946886 | ||
|
|
c3458ec59f | ||
|
|
2f09bb468e | ||
|
|
2279e18329 | ||
|
|
16e519430a | ||
|
|
0ec5b8d6dd | ||
|
|
1029c63a20 | ||
|
|
9e43540dfa | ||
|
|
3c7b3faa7f | ||
|
|
4855584de9 | ||
|
|
f67c86cb39 | ||
|
|
779a906d97 | ||
|
|
b91d7f314b | ||
|
|
3703a1c36c | ||
|
|
03fd311bfe | ||
|
|
7e78d6a92b | ||
|
|
71f7a3e902 | ||
|
|
84abe257f1 | ||
|
|
133ff4914d | ||
|
|
ba4df23bff | ||
|
|
14c9161268 | ||
|
|
236e9562fd | ||
|
|
a6409dcd27 | ||
|
|
cfd68f9f2e | ||
|
|
2cfd5754ca | ||
|
|
f81c1afde7 | ||
|
|
f7a7debd9d | ||
|
|
af56ebb04e | ||
|
|
6483b8c138 | ||
|
|
9702647044 | ||
|
|
3b2192a046 | ||
|
|
b864911e18 | ||
|
|
9e0506cb48 | ||
|
|
a0000b9489 | ||
|
|
df3fac4e5e | ||
|
|
8a36f55439 | ||
|
|
ab253744f8 | ||
|
|
fff1eaba45 | ||
|
|
eafb7b979d | ||
|
|
9e22bf39cc | ||
|
|
fc6691ce5b | ||
|
|
598dc86ca5 | ||
|
|
3e2bfc35e3 | ||
|
|
6658ad8618 | ||
|
|
759ccdbee2 | ||
|
|
635060a02d | ||
|
|
a128e7e7ab | ||
|
|
3cb69b8035 | ||
|
|
7dc58bb330 | ||
|
|
8bd0a62cb3 | ||
|
|
b25bec53d8 | ||
|
|
8a7923c974 | ||
|
|
3ee81cbee0 | ||
|
|
4b744337fe | ||
|
|
8d7d2f7a44 | ||
|
|
24bf1dbffb | ||
|
|
c1890bb126 | ||
|
|
a4a570896a | ||
|
|
0594034ee6 | ||
|
|
396e376f49 | ||
|
|
fca9eae0fd | ||
|
|
f0d9bdd901 | ||
|
|
c185d5b0b5 | ||
|
|
6ece2a3449 | ||
|
|
682d52441d | ||
|
|
c2c0c81f1c | ||
|
|
fe23907eb3 | ||
|
|
e2bf8a8a11 | ||
|
|
3f0136ae7c | ||
|
|
9f992409c7 | ||
|
|
cd53ed16e9 | ||
|
|
cc56edc91d | ||
|
|
25eb4b3547 | ||
|
|
c5eb112f5a | ||
|
|
8d904f415a | ||
|
|
c34173ca6e | ||
|
|
aa292ac6b8 | ||
|
|
c36227e2fc | ||
|
|
a406e0416f | ||
|
|
215cc5e71d | ||
|
|
0e72acee10 | ||
|
|
000ed3175d | ||
|
|
2f6bae4e2a | ||
|
|
0fefe11bfd | ||
|
|
2dbb1bbbea | ||
|
|
a586a1d525 | ||
|
|
4724101e75 | ||
|
|
1b921cd533 | ||
|
|
fcf3786fc5 | ||
|
|
d78f75aa60 | ||
|
|
56056cf10e | ||
|
|
5fd9b20213 | ||
|
|
076cdae3fd | ||
|
|
6543c7c26f | ||
|
|
3035c8af30 | ||
|
|
3cbd647dad | ||
|
|
4efcbee772 | ||
|
|
bb59cf94e9 | ||
|
|
96436814f5 | ||
|
|
e8763e936d | ||
|
|
c41a6b87b8 | ||
|
|
54395a7252 | ||
|
|
4322b8b932 | ||
|
|
f444af825f | ||
|
|
0d0e7f774e | ||
|
|
30ed27ae5c | ||
|
|
b7283487b2 | ||
|
|
551f7dc05a | ||
|
|
1b485770b6 | ||
|
|
50e18f84c2 | ||
|
|
9eab96090d | ||
|
|
737a741a54 | ||
|
|
46253039df | ||
|
|
2f5b6a115d | ||
|
|
05d63fcbe6 | ||
|
|
ae5bc76123 | ||
|
|
b70e92effb | ||
|
|
2a9b967d2d | ||
|
|
459fec56db | ||
|
|
bfdd6f36e2 | ||
|
|
432e4b7f0a | ||
|
|
39cb9c425c | ||
|
|
dff1ae0fb4 | ||
|
|
2943624439 | ||
|
|
94064e526a | ||
|
|
95cac4dfb9 | ||
|
|
46fb6a21ee | ||
|
|
3d6ca973c4 | ||
|
|
fc03f4c87a | ||
|
|
502ec2a56f | ||
|
|
66d5c3f620 | ||
|
|
220500efbb | ||
|
|
d29c5eabbb | ||
|
|
979d7c5625 | ||
|
|
4e828199c8 | ||
|
|
61c84c8e01 | ||
|
|
30be5e33d7 | ||
|
|
b1c524d4a3 | ||
|
|
7c33c7f7da | ||
|
|
7846c23edd | ||
|
|
6c0dd8543d | ||
|
|
70c082a1b1 | ||
|
|
02cda1e611 | ||
|
|
a1c82eaea6 | ||
|
|
9eda710538 | ||
|
|
a87a2d0b71 | ||
|
|
6e2f4d85a3 | ||
|
|
511727fdfa | ||
|
|
a86c7c767a | ||
|
|
da88d8f17f | ||
|
|
dbd3705441 | ||
|
|
8d2f526ee7 | ||
|
|
b075a73222 | ||
|
|
eb5387ca38 | ||
|
|
1eaef94c71 | ||
|
|
e1903edd04 | ||
|
|
0b6b8ced92 | ||
|
|
cc6ce72f6e | ||
|
|
857a384d8b | ||
|
|
13dd88b7ad | ||
|
|
e85cdc8c9f | ||
|
|
1760740a4c | ||
|
|
daf40fde82 | ||
|
|
e909b8199b | ||
|
|
ec089faf3a | ||
|
|
9fcb30ac33 | ||
|
|
cb92579461 | ||
|
|
0327000f8d | ||
|
|
3a91c87c73 | ||
|
|
db5b5d321b | ||
|
|
76b7e7408a | ||
|
|
324e5d0258 | ||
|
|
d997bbc081 | ||
|
|
1c21d4f356 | ||
|
|
5f574cf283 | ||
|
|
92f1e6da1e | ||
|
|
24aa3c781b | ||
|
|
32bd6109e3 | ||
|
|
52442017e2 | ||
|
|
278454287c | ||
|
|
5ded8fb400 | ||
|
|
24730e7ad6 | ||
|
|
016a780632 | ||
|
|
6d89638ca4 | ||
|
|
fdc091319b | ||
|
|
960c8745d9 | ||
|
|
134b09dba5 | ||
|
|
76b93274e8 | ||
|
|
ea455323d8 | ||
|
|
1855f84fe0 | ||
|
|
323d996d5f | ||
|
|
1b858393c5 | ||
|
|
ca88c5b41c | ||
|
|
7e1470ea46 | ||
|
|
f98d0bbc1f | ||
|
|
2a34022619 | ||
|
|
57f879a6ba | ||
|
|
d4ba09c753 | ||
|
|
6c3a8448cf | ||
|
|
d4e1c1b109 | ||
|
|
a1d5120e58 | ||
|
|
724e1ea97e | ||
|
|
6f8067ffd3 | ||
|
|
f38386d164 | ||
|
|
d66829702f | ||
|
|
36b50436d7 | ||
|
|
33dd747ec7 | ||
|
|
d8e14d9993 | ||
|
|
f61b9f7964 | ||
|
|
91cdc76414 | ||
|
|
a1379f61da | ||
|
|
a665d6de59 | ||
|
|
6a6a719ab6 | ||
|
|
84f17b7539 | ||
|
|
57141e478c | ||
|
|
6213917089 | ||
|
|
fb33c31378 | ||
|
|
79f5e736b0 | ||
|
|
0d4b6f5627 | ||
|
|
5c8f558f60 | ||
|
|
c851f9d5a3 | ||
|
|
2d74514dd0 | ||
|
|
84012e760e | ||
|
|
6baef49f9d | ||
|
|
f55d4fa73a | ||
|
|
ce00c627d4 | ||
|
|
d3c6f530e2 | ||
|
|
cf6391d51b | ||
|
|
57311d731e | ||
|
|
95d45b386f | ||
|
|
bbc8bed39c | ||
|
|
ec67b3975c | ||
|
|
e9967c32e6 | ||
|
|
493a213d41 | ||
|
|
f65dbee74b | ||
|
|
f51fd1267f | ||
|
|
711f3f69da | ||
|
|
24f4cbbb27 | ||
|
|
307a3e078e | ||
|
|
d31265895d | ||
|
|
6e35a879a3 | ||
|
|
9c2a3b8a82 | ||
|
|
916fab7d4b | ||
|
|
3163ef87c6 | ||
|
|
1934181b52 | ||
|
|
1b815a7d96 | ||
|
|
4e0a08106d | ||
|
|
e8cc739fbd | ||
|
|
fc57cbfb49 | ||
|
|
7522fec044 | ||
|
|
237dabb907 | ||
|
|
3686048ab6 | ||
|
|
2bf4c5d7e7 | ||
|
|
051d80b2f3 | ||
|
|
adaa1e856c | ||
|
|
4daa57c98e | ||
|
|
7e67b2cbb3 | ||
|
|
270d18a88a | ||
|
|
25f8a735a9 | ||
|
|
9eb672ea17 | ||
|
|
9febc762da | ||
|
|
4b742c220c | ||
|
|
9d03d441e1 | ||
|
|
ff8b249cc6 | ||
|
|
248e6ea5e7 | ||
|
|
be0afdebfd | ||
|
|
9f19d20344 | ||
|
|
aea8a32ba5 | ||
|
|
d1a4c82937 | ||
|
|
4f73812673 | ||
|
|
33150615a1 | ||
|
|
491f83c86d | ||
|
|
41f776763b | ||
|
|
65fdfac866 | ||
|
|
cb0c00bc6d | ||
|
|
ad53678c19 | ||
|
|
62097765a6 | ||
|
|
efb7280e99 | ||
|
|
bdb2a47743 | ||
|
|
c4677190be | ||
|
|
055aba189c | ||
|
|
314c3d5e78 | ||
|
|
6db03356b5 | ||
|
|
28af919b09 | ||
|
|
8f7a456a39 | ||
|
|
5b3bec1aac | ||
|
|
f2aa17c9d0 | ||
|
|
bc06b9e051 | ||
|
|
6d216af507 | ||
|
|
b2f1d9f376 | ||
|
|
a653e469f2 | ||
|
|
4f4241ba3a | ||
|
|
2cf9c68040 | ||
|
|
cc0f977d6f | ||
|
|
7879952fde | ||
|
|
4452cab987 | ||
|
|
98bd64621a | ||
|
|
c1c769ceb0 | ||
|
|
d64e55c66f | ||
|
|
76fc84be37 | ||
|
|
8cd5f5990e | ||
|
|
6ffe54d68f | ||
|
|
d78ea882c8 | ||
|
|
958802a233 | ||
|
|
00b02efdc2 | ||
|
|
50569f12f5 | ||
|
|
8aa4ceb570 | ||
|
|
a7afbf85ad | ||
|
|
8fdf3dcdb8 | ||
|
|
818c20e0cb | ||
|
|
49b2f80ded | ||
|
|
c1d4996777 | ||
|
|
fd3e6e0ee4 | ||
|
|
edc5754c68 | ||
|
|
bd75dea000 | ||
|
|
e09a0a548f | ||
|
|
15ee8b4362 | ||
|
|
f8cd602cbd | ||
|
|
97951aec15 | ||
|
|
3871c5a4a0 | ||
|
|
ab8d75b192 | ||
|
|
e135c969c9 | ||
|
|
36e7090466 | ||
|
|
f28f177c6b | ||
|
|
785973c624 | ||
|
|
9c06acff72 | ||
|
|
69f1e1753c | ||
|
|
4fabddeb47 | ||
|
|
17ff1ab372 | ||
|
|
01b88f876e | ||
|
|
0ead27a05b | ||
|
|
c9742bb6ea | ||
|
|
d50c1e3658 | ||
|
|
3a9c2a0356 | ||
|
|
b616a2b3e7 | ||
|
|
3c34096392 | ||
|
|
70e0d3b571 | ||
|
|
20ef115eb2 | ||
|
|
ae5a2396f3 | ||
|
|
8f82bf40e0 | ||
|
|
fe398de2fa | ||
|
|
a770d75e2e | ||
|
|
a330104e9b | ||
|
|
9b4c195872 | ||
|
|
11fa60d690 | ||
|
|
1214609546 | ||
|
|
d798356c19 | ||
|
|
4d6a9e4f14 | ||
|
|
8a7eaba668 | ||
|
|
1846f20f6e | ||
|
|
18c1787552 | ||
|
|
aae3cae4bb | ||
|
|
e7e4821804 | ||
|
|
9654802acc | ||
|
|
06a24fa4d0 | ||
|
|
62b1b0519a | ||
|
|
9981e84a42 | ||
|
|
9d313b4e0e | ||
|
|
ab2cb1ad1f | ||
|
|
f85b14a7f7 | ||
|
|
94c6a01420 | ||
|
|
563b550f08 | ||
|
|
aa45716ef7 | ||
|
|
846ef043d5 | ||
|
|
ce5b95f8e5 | ||
|
|
efc17983c3 | ||
|
|
7140898db9 | ||
|
|
6db253e1cc | ||
|
|
4c9d049b10 | ||
|
|
818e921192 | ||
|
|
6ea1d665bb | ||
|
|
7326ba1403 | ||
|
|
62bfa5157b | ||
|
|
43a8828430 | ||
|
|
618202cf8b | ||
|
|
9614a23506 | ||
|
|
10afdfecdd | ||
|
|
c0e08fb927 | ||
|
|
6d6bc9b050 | ||
|
|
4714fb6887 | ||
|
|
5f47810964 | ||
|
|
0f6024e055 | ||
|
|
fafc15f80c | ||
|
|
9a85ea861d | ||
|
|
9541960307 | ||
|
|
95073deb96 | ||
|
|
82b4647b95 | ||
|
|
0c770a8b37 | ||
|
|
0e4031348f | ||
|
|
5cc26762c2 | ||
|
|
b8b4853d1f | ||
|
|
fbabe27fc1 | ||
|
|
4d1554c85b | ||
|
|
ab40495d5c | ||
|
|
42ebf49f92 | ||
|
|
c5ccd88f79 | ||
|
|
dbd1b227d9 | ||
|
|
63baac3c61 | ||
|
|
7c39bb6659 | ||
|
|
4f8c5965ac | ||
|
|
900a17fc00 | ||
|
|
78f36aaa0d | ||
|
|
e064e02794 | ||
|
|
e22e5045f1 | ||
|
|
087f35482b | ||
|
|
23ff5fea28 | ||
|
|
34347ccaf5 | ||
|
|
e704eb6cef | ||
|
|
bf63423fec | ||
|
|
f6d71ed8ef | ||
|
|
3c342339a1 | ||
|
|
33463856c5 | ||
|
|
a18f4c9b1b | ||
|
|
783c7ee4c5 | ||
|
|
a0b2a692d0 | ||
|
|
a59d368101 | ||
|
|
5c36fb29ed | ||
|
|
508b8ef2e2 | ||
|
|
e94c62e5b3 | ||
|
|
b65a6c2829 | ||
|
|
c4a20d0798 | ||
|
|
9cb7ea524e | ||
|
|
0ac0eeda34 | ||
|
|
4d066b4fd2 | ||
|
|
840e321dd9 | ||
|
|
4b6963122b | ||
|
|
d5d662bc41 | ||
|
|
0b0ed56901 | ||
|
|
13e361aabc | ||
|
|
d1a26e66a7 | ||
|
|
ffe3c84e7c | ||
|
|
702c7382a7 | ||
|
|
b138d486e4 | ||
|
|
3a25d6b275 | ||
|
|
66e2f51233 | ||
|
|
8fdb048b6a | ||
|
|
fa3d98a492 | ||
|
|
d9dda44409 | ||
|
|
7368c01a8f | ||
|
|
21ac5be7ca | ||
|
|
e14a113277 | ||
|
|
66d3440675 | ||
|
|
6b6be3b03d | ||
|
|
cda8158bec | ||
|
|
332e0dc4a8 | ||
|
|
f7b4c6837b | ||
|
|
531928bf0b | ||
|
|
490c8e055b | ||
|
|
bcbf192bbc | ||
|
|
78d855c5ca | ||
|
|
1fa9aa88a8 | ||
|
|
08c77c2668 | ||
|
|
793ebe1b0f | ||
|
|
4c42acc7e1 | ||
|
|
4eb9660bfa | ||
|
|
8ed08f701d | ||
|
|
784964efad | ||
|
|
adb96e72b9 | ||
|
|
439c6f7296 | ||
|
|
e2f1ea1444 | ||
|
|
2977ceb459 | ||
|
|
e00d4e0ed8 | ||
|
|
772127d9d8 | ||
|
|
6ba45c88ec | ||
|
|
5a4040cf0b | ||
|
|
b54f580e66 | ||
|
|
a9ac69fe9c | ||
|
|
5c52b5e404 | ||
|
|
b80360b7da | ||
|
|
2753883687 | ||
|
|
ced73ffb14 | ||
|
|
672fe2dfd7 | ||
|
|
04bb6997a2 | ||
|
|
c8a8dbbbae | ||
|
|
1f9520dc78 | ||
|
|
84f8627890 | ||
|
|
a177df32b7 | ||
|
|
f25d5dd123 | ||
|
|
4cfa9e6165 | ||
|
|
0303ea7f57 | ||
|
|
2813e01e61 | ||
|
|
e3420da60f | ||
|
|
60493d30f6 | ||
|
|
6efe8e7d7c | ||
|
|
2e8409f146 | ||
|
|
ac4b2b9dfe | ||
|
|
23b6178e78 | ||
|
|
5e5d45fb0a | ||
|
|
1765b8f2cf | ||
|
|
5678562ce2 | ||
|
|
7274197da0 | ||
|
|
c79fcb380b | ||
|
|
6a98eade07 | ||
|
|
9008a65c14 | ||
|
|
4e07e4c7f3 | ||
|
|
e440d8503a | ||
|
|
e9bacff830 | ||
|
|
9cc99ffcd6 | ||
|
|
beb91271de | ||
|
|
7e9585ebc5 | ||
|
|
0c4b3f71e5 | ||
|
|
5c17ec5f01 | ||
|
|
8b4edc46a7 | ||
|
|
2b7a0a4585 | ||
|
|
1882176489 | ||
|
|
875e89e71a | ||
|
|
52520635ea | ||
|
|
aa50a9ba83 | ||
|
|
489e5111ac | ||
|
|
2d4c20af35 | ||
|
|
66fdb447f7 | ||
|
|
c801775a39 | ||
|
|
f5bb57d6a6 | ||
|
|
1071ab05db | ||
|
|
8461cf6443 | ||
|
|
bb10501f56 | ||
|
|
c4d5f657da | ||
|
|
cabb58a9aa | ||
|
|
e64ce5bb4f | ||
|
|
bc750d61d2 | ||
|
|
3150901b6e | ||
|
|
4f6745b742 | ||
|
|
bcdc323a97 | ||
|
|
4fd4cc709d | ||
|
|
0fac463144 | ||
|
|
d809dfac65 | ||
|
|
983fd70260 | ||
|
|
ea11a5274e | ||
|
|
42356c2a67 | ||
|
|
5a84ab2011 | ||
|
|
b4573e341f | ||
|
|
df252c4704 | ||
|
|
d1912f873b | ||
|
|
e525c42c7d | ||
|
|
0242322d24 | ||
|
|
ded6fafc8a | ||
|
|
4aebd678c3 | ||
|
|
afc9ed2274 | ||
|
|
d73d021e3c | ||
|
|
c8fe81e21d | ||
|
|
621f1df913 | ||
|
|
5f4274b449 | ||
|
|
4acb37156f | ||
|
|
1ca23a7479 | ||
|
|
61daf7218d | ||
|
|
dc6671fc4e | ||
|
|
f34237ebc8 | ||
|
|
aadeb3b87e | ||
|
|
1fb75c1af3 | ||
|
|
e04d28c885 | ||
|
|
6d80b3675a | ||
|
|
07d698f8dc | ||
|
|
ef158504e7 | ||
|
|
63be1ae5a9 | ||
|
|
b9ba1a4f69 | ||
|
|
e006d9b033 | ||
|
|
1538684c6c | ||
|
|
b37e83caab | ||
|
|
d11d3ab08b | ||
|
|
1144a536a5 | ||
|
|
515c753d11 | ||
|
|
0864e640ed | ||
|
|
b876e49393 | ||
|
|
fc0292bf8a | ||
|
|
a903805cd9 | ||
|
|
abab34573e | ||
|
|
fa1b94af60 | ||
|
|
81ff5d1224 | ||
|
|
5c3a8819a4 | ||
|
|
4d0a08d858 | ||
|
|
c41bdaa2b7 | ||
|
|
4bf2fc18e5 | ||
|
|
145fd8657f | ||
|
|
01b55d1d29 | ||
|
|
98b3151c5f | ||
|
|
c7eca8deb3 | ||
|
|
627b54f712 | ||
|
|
8ef7b6fc54 | ||
|
|
d83652b0fc | ||
|
|
cce32229e0 | ||
|
|
24edd83c8a | ||
|
|
268c5b6482 | ||
|
|
a66a754126 | ||
|
|
18059734ce | ||
|
|
c883e709c3 | ||
|
|
7be0bd3583 | ||
|
|
d9ab37ea58 | ||
|
|
ff075ba612 | ||
|
|
9c9294a730 | ||
|
|
2a0842b8ae | ||
|
|
3cfe45ffc2 | ||
|
|
80dc7bfc52 | ||
|
|
10f26f17ba | ||
|
|
4ba7402f28 | ||
|
|
d4da2e0d9c | ||
|
|
b180d004ba | ||
|
|
de5bd96f08 | ||
|
|
3a05b5dacc | ||
|
|
b3c4e32b68 | ||
|
|
375a48f135 | ||
|
|
1750ab92e6 | ||
|
|
8364ddd10b | ||
|
|
63043cb45d | ||
|
|
aaa6497659 | ||
|
|
4fc6fa9c8a | ||
|
|
7d0dcfb3a5 | ||
|
|
a97ea0ad63 | ||
|
|
da66a4d22f | ||
|
|
748e54d4c2 | ||
|
|
0f172595d7 | ||
|
|
5ffdbd99e8 | ||
|
|
fbe57c4c71 | ||
|
|
61726168d6 | ||
|
|
a80632ab36 | ||
|
|
893eb8b73b | ||
|
|
07a5ee7d2c | ||
|
|
c7a300be2f | ||
|
|
ef842fca89 | ||
|
|
bdbe9e1ca5 | ||
|
|
0c7f65222c | ||
|
|
a8fa644d25 | ||
|
|
cf7ccb5b8c | ||
|
|
49fdd6fc5b | ||
|
|
5e91c74304 | ||
|
|
b83e6f6e7c | ||
|
|
2687777a82 | ||
|
|
918b8036ea | ||
|
|
7ea0e4d4db | ||
|
|
622c1705aa | ||
|
|
3a08929b05 | ||
|
|
99e30d561d | ||
|
|
033a44580c | ||
|
|
4734bcfbb4 | ||
|
|
099fe6f477 | ||
|
|
889327b5f6 | ||
|
|
a2845f44ab | ||
|
|
a7477516d1 | ||
|
|
938d5828fc | ||
|
|
bf3eab453c | ||
|
|
ebab893330 | ||
|
|
4c67b3a118 | ||
|
|
d74b06f8bf | ||
|
|
c54e211147 | ||
|
|
8817cf5116 | ||
|
|
fb568513b2 | ||
|
|
b4ebfdb84a | ||
|
|
7040bd804a | ||
|
|
2f2fc17bd8 | ||
|
|
d5d4b49aaf | ||
|
|
042c4efddf | ||
|
|
01251d162c | ||
|
|
64026fde7c | ||
|
|
adcdae4abe | ||
|
|
88d138b925 | ||
|
|
2773b89815 | ||
|
|
949d93fdaa | ||
|
|
90a4303c8e | ||
|
|
d3b1972505 | ||
|
|
3807d5fbd0 | ||
|
|
e49e2021e5 | ||
|
|
84a5276ab0 | ||
|
|
1732c3b350 | ||
|
|
0f52f63863 | ||
|
|
0043e95ba7 | ||
|
|
a3f2088046 | ||
|
|
186f5553b8 | ||
|
|
d8454d9da5 | ||
|
|
318194a216 | ||
|
|
b50d2358d3 | ||
|
|
763587ffb4 | ||
|
|
8a375c12e9 | ||
|
|
11afdb51f3 | ||
|
|
e4353f4650 | ||
|
|
9d4437a7f5 | ||
|
|
493bf5ed08 | ||
|
|
93800fd834 | ||
|
|
79bc34ed76 | ||
|
|
b40ad7e87e | ||
|
|
1ab3fba212 | ||
|
|
3cd0bbc0f4 | ||
|
|
a806728e43 | ||
|
|
683037ca69 | ||
|
|
a113127df1 | ||
|
|
fecfaaf812 | ||
|
|
8df8f1f6f7 | ||
|
|
5502ca5f58 | ||
|
|
75b8cfa92e | ||
|
|
f7b23fb0e2 | ||
|
|
42f7abf2f5 | ||
|
|
08dd0e34d1 | ||
|
|
7540770dec | ||
|
|
213c5df706 | ||
|
|
578e4b2785 | ||
|
|
8f8db0c431 | ||
|
|
df7253b13e | ||
|
|
fcafd63f41 | ||
|
|
c82fdb9fa1 | ||
|
|
454c52f4ab | ||
|
|
73c69c3f93 | ||
|
|
614ec30e05 | ||
|
|
d7c07e0ed3 | ||
|
|
08bd16c1b6 | ||
|
|
4f06c72798 | ||
|
|
166fe2a4e5 | ||
|
|
b7635a71c8 | ||
|
|
6ad4bdea83 | ||
|
|
efb9a11d22 | ||
|
|
047d09bcb1 | ||
|
|
e1ca6b5181 | ||
|
|
36a2569537 | ||
|
|
10ceddfa67 | ||
|
|
929677afe0 | ||
|
|
ee4adc1363 | ||
|
|
c7b2bdfaac | ||
|
|
fde8fb960b | ||
|
|
9a43d26c60 | ||
|
|
742f603b3b | ||
|
|
a82f2a5df3 | ||
|
|
06bc5513ae | ||
|
|
47b937f880 | ||
|
|
775d27b6a9 | ||
|
|
6c838ab57c | ||
|
|
8eebd2aa67 | ||
|
|
fc78a08657 | ||
|
|
7b3cc95ab7 | ||
|
|
bd70765b7d | ||
|
|
5b424aec22 | ||
|
|
d6cc0694f0 | ||
|
|
9738515129 | ||
|
|
b05ee10f41 | ||
|
|
67d85f0f86 | ||
|
|
2ddeef761f | ||
|
|
43e0109d44 | ||
|
|
02bb41697d | ||
|
|
12cd56e3e8 | ||
|
|
72cfb70e35 | ||
|
|
40dc180b88 | ||
|
|
4529c326c6 | ||
|
|
f6660af014 | ||
|
|
a52131b574 | ||
|
|
a48d0492c8 | ||
|
|
2bba1be817 | ||
|
|
8a394fb08f | ||
|
|
693ae9e8f2 | ||
|
|
4b7b6d6cb3 | ||
|
|
9a3bdfb14b | ||
|
|
7aeddc63ac | ||
|
|
2990a1c255 | ||
|
|
91eea03b18 | ||
|
|
8702f290af | ||
|
|
d0b5b7ba03 | ||
|
|
f3d68c6f25 | ||
|
|
91100d3fac | ||
|
|
61833c32e5 | ||
|
|
1468f67b9b | ||
|
|
cb6376094c | ||
|
|
ff175e661b | ||
|
|
fe741f168a | ||
|
|
eaaf500d5e | ||
|
|
05f85b33c5 | ||
|
|
c4e0647bda | ||
|
|
6dcc1e09b9 | ||
|
|
ea03e4d34a | ||
|
|
b7a2d17e93 | ||
|
|
e1bd740249 | ||
|
|
de52c2da80 | ||
|
|
a31c6d82c9 | ||
|
|
25842894d2 | ||
|
|
83c98c2d55 | ||
|
|
fe2011742d | ||
|
|
d87b676d60 | ||
|
|
ffa6378108 | ||
|
|
f73ba895af | ||
|
|
cb2a1147f0 | ||
|
|
6702ef4a71 | ||
|
|
1d46791364 | ||
|
|
f61846dec9 | ||
|
|
4776e196da | ||
|
|
839a48b678 | ||
|
|
e203901224 | ||
|
|
76d03f7fd2 | ||
|
|
7f5e3aaaf7 | ||
|
|
5bfbae4b00 | ||
|
|
521a854635 | ||
|
|
ce15ef2db9 | ||
|
|
6d5cf89d33 | ||
|
|
b3b984f351 | ||
|
|
d5a0f1e711 | ||
|
|
67c36f3d98 | ||
|
|
8e0a29e9b5 | ||
|
|
47be879445 | ||
|
|
860f8a7906 | ||
|
|
5d9baa053a | ||
|
|
da174eae71 | ||
|
|
7fac71aa81 | ||
|
|
7d51c1140d | ||
|
|
cd198223ea | ||
|
|
300fff40e3 | ||
|
|
08abac350d | ||
|
|
a57decf8be | ||
|
|
ee95e59243 | ||
|
|
b0694bcf2c | ||
|
|
c9f6e31ca9 | ||
|
|
fe4080d59f | ||
|
|
7f6a1ad1a7 | ||
|
|
980bb35441 | ||
|
|
01df2e2dc7 | ||
|
|
ec40dd1b6f | ||
|
|
f2f8898004 | ||
|
|
3afa37c6ea | ||
|
|
8c0ce38301 | ||
|
|
cc6aa3209c | ||
|
|
76a86763dd | ||
|
|
09fb039528 | ||
|
|
174d3300c4 | ||
|
|
8b57ce1792 | ||
|
|
6c14e429eb | ||
|
|
5f200c6bc3 | ||
|
|
d52347ee1d | ||
|
|
d0d9aa4400 | ||
|
|
c3d909c818 | ||
|
|
d9e718b0e0 | ||
|
|
a7b55edb9b | ||
|
|
000479d55e | ||
|
|
7ef22f2940 | ||
|
|
a242fcfd2c | ||
|
|
73c21ae0a9 | ||
|
|
2398454838 | ||
|
|
c1ba5a30c5 | ||
|
|
2c0f847d3e | ||
|
|
7d5e95f013 | ||
|
|
9000342de8 | ||
|
|
61b47aa0de | ||
|
|
22f5c5fb74 | ||
|
|
801162d7be | ||
|
|
f81f3fb060 | ||
|
|
15792d8426 | ||
|
|
d7f345eef8 | ||
|
|
e3031462c1 | ||
|
|
47d14271ab | ||
|
|
2bf9fd6cbc | ||
|
|
19e716b522 | ||
|
|
1ee15942cc | ||
|
|
898e641256 | ||
|
|
cda4ccff2a | ||
|
|
ba274482f7 | ||
|
|
435f734d60 | ||
|
|
a5f949c4e2 | ||
|
|
9fc556864e | ||
|
|
a0645dc713 | ||
|
|
5893cd309d | ||
|
|
09c7ab1ee6 | ||
|
|
4bacae3711 | ||
|
|
8d3e536582 | ||
|
|
697cc0a79b | ||
|
|
a34ed5c02a | ||
|
|
256bb01606 | ||
|
|
1de535363d | ||
|
|
24d9011939 | ||
|
|
6284bdc98f | ||
|
|
e7351b1bb8 | ||
|
|
b3ee89c6e5 |
@@ -7,6 +7,10 @@ executors:
|
|||||||
doxygen:
|
doxygen:
|
||||||
docker:
|
docker:
|
||||||
- image: hrektts/doxygen
|
- image: hrektts/doxygen
|
||||||
|
python:
|
||||||
|
docker:
|
||||||
|
- image: 3.7.7-stretch
|
||||||
|
|
||||||
|
|
||||||
restore-workspace: &restore-workspace
|
restore-workspace: &restore-workspace
|
||||||
attach_workspace:
|
attach_workspace:
|
||||||
@@ -120,47 +124,25 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- c-docs
|
- c-docs
|
||||||
|
|
||||||
# build_test_docs_wheel:
|
remote_python_packaging:
|
||||||
# docker:
|
|
||||||
# - image: deltachat/coredeps
|
|
||||||
# environment:
|
|
||||||
# TESTS: 1
|
|
||||||
# DOCS: 1
|
|
||||||
# working_directory: /mnt/crate
|
|
||||||
# steps:
|
|
||||||
# - *restore-workspace
|
|
||||||
# - *restore-cache
|
|
||||||
# - run:
|
|
||||||
# name: build docs, run tests and build wheels
|
|
||||||
# command: ci_scripts/run-python.sh
|
|
||||||
# - run:
|
|
||||||
# name: copying docs and wheels to workspace
|
|
||||||
# command: |
|
|
||||||
# mkdir -p workspace/python
|
|
||||||
# # cp -av docs workspace/c-docs
|
|
||||||
# cp -av python/.docker-tox/wheelhouse workspace/
|
|
||||||
# cp -av python/doc/_build/ workspace/py-docs
|
|
||||||
# - persist_to_workspace:
|
|
||||||
# root: workspace
|
|
||||||
# paths:
|
|
||||||
# # - c-docs
|
|
||||||
# - py-docs
|
|
||||||
# - wheelhouse
|
|
||||||
|
|
||||||
remote_tests_rust:
|
|
||||||
machine: true
|
machine: true
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: ci_scripts/remote_tests_rust.sh
|
# the following commands on success produces
|
||||||
|
# workspace/{wheelhouse,py-docs} as artefact directories
|
||||||
|
- run: bash ci_scripts/remote_python_packaging.sh
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: workspace
|
||||||
|
paths:
|
||||||
|
# - c-docs
|
||||||
|
- py-docs
|
||||||
|
- wheelhouse
|
||||||
|
|
||||||
remote_tests_python:
|
remote_tests_python:
|
||||||
machine: true
|
machine: true
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
#- attach_workspace:
|
|
||||||
# at: workspace
|
|
||||||
- run: ci_scripts/remote_tests_python.sh
|
- run: ci_scripts/remote_tests_python.sh
|
||||||
# workspace/py-docs workspace/wheelhouse workspace/c-docs
|
|
||||||
|
|
||||||
upload_docs_wheels:
|
upload_docs_wheels:
|
||||||
machine: true
|
machine: true
|
||||||
@@ -168,6 +150,7 @@ jobs:
|
|||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: workspace
|
at: workspace
|
||||||
|
- run: pyenv versions
|
||||||
- run: pyenv global 3.5.2
|
- run: pyenv global 3.5.2
|
||||||
- run: ls -laR workspace
|
- run: ls -laR workspace
|
||||||
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||||
@@ -189,14 +172,29 @@ workflows:
|
|||||||
jobs:
|
jobs:
|
||||||
# - cargo_fetch
|
# - cargo_fetch
|
||||||
|
|
||||||
- remote_tests_rust
|
- remote_tests_python:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
- remote_tests_python
|
- remote_python_packaging:
|
||||||
|
requires:
|
||||||
|
- remote_tests_python
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
# - upload_docs_wheels:
|
- upload_docs_wheels:
|
||||||
# requires:
|
requires:
|
||||||
# - build_test_docs_wheel
|
- remote_python_packaging
|
||||||
# - build_doxygen
|
- build_doxygen
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
# - rustfmt:
|
# - rustfmt:
|
||||||
# requires:
|
# requires:
|
||||||
# - cargo_fetch
|
# - cargo_fetch
|
||||||
@@ -204,7 +202,12 @@ workflows:
|
|||||||
# requires:
|
# requires:
|
||||||
# - cargo_fetch
|
# - cargo_fetch
|
||||||
|
|
||||||
- build_doxygen
|
- build_doxygen:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
# Linux Desktop 64bit
|
# Linux Desktop 64bit
|
||||||
# - test_x86_64-unknown-linux-gnu:
|
# - test_x86_64-unknown-linux-gnu:
|
||||||
|
|||||||
89
.github/workflows/ci.yml
vendored
Normal file
89
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
name: Rust CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- trying
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: Rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: 1.43.1
|
||||||
|
override: true
|
||||||
|
- run: rustup component add rustfmt
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
run_clippy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.43.1
|
||||||
|
components: clippy
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/clippy-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
|
build_and_test:
|
||||||
|
name: Build and test
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
|
rust: [nightly, 1.43.1]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
- name: Install ${{ matrix.rust }}
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||||
|
|
||||||
|
- name: check
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --workspace --all --bins --examples --tests
|
||||||
|
|
||||||
|
- name: tests
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --workspace
|
||||||
|
|
||||||
47
.github/workflows/code-quality.yml
vendored
47
.github/workflows/code-quality.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
on: push
|
|
||||||
name: Code Quality
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
check:
|
|
||||||
name: Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: nightly-2019-11-06
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
name: Rustfmt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: nightly-2019-11-06
|
|
||||||
override: true
|
|
||||||
- run: rustup component add rustfmt
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: fmt
|
|
||||||
args: --all -- --check
|
|
||||||
|
|
||||||
run_clippy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: nightly-2019-11-06
|
|
||||||
components: clippy
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/clippy-check@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
args: --all-features
|
|
||||||
54
ASYNC-API-TODO.txt
Normal file
54
ASYNC-API-TODO.txt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
|
||||||
|
|
||||||
|
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
|
||||||
|
|
||||||
|
APIs:
|
||||||
|
dc_context_new # opens the database
|
||||||
|
dc_open # FFI only
|
||||||
|
-> drop it and move parameters to dc_context_new()
|
||||||
|
|
||||||
|
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
|
||||||
|
dc_imex NEVER goes through the job system
|
||||||
|
dc_imex import_backup needs to ensure dc_stop_jobs()
|
||||||
|
|
||||||
|
dc_start_io # start smtp/imap and job handling subsystems
|
||||||
|
dc_stop_io # stop smtp/imap and job handling subsystems
|
||||||
|
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
|
||||||
|
|
||||||
|
dc_close # FFI only
|
||||||
|
-> can be dropped
|
||||||
|
dc_context_unref
|
||||||
|
|
||||||
|
for ios share-extension:
|
||||||
|
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
|
||||||
|
0: message was sent
|
||||||
|
1: message failed to go out, is queued as a job to be retried later
|
||||||
|
2: message permanently failed?
|
||||||
|
|
||||||
|
EVENT handling:
|
||||||
|
start a callback thread and call get_next_event() which is BLOCKING
|
||||||
|
it's fine to start this callback thread later, it will see all events.
|
||||||
|
Note that the core infinitely fills the internal queue if you never drain it.
|
||||||
|
|
||||||
|
FFI-get_next_event() returns NULL if the context is unrefed already?
|
||||||
|
|
||||||
|
sidenote: how python's callback thread does it currently:
|
||||||
|
CB-thread runs this while loop:
|
||||||
|
while not QUITFLAG:
|
||||||
|
ev = context.get_next_event( )
|
||||||
|
...
|
||||||
|
So in order to shutdown properly one has to set QUITFLAG
|
||||||
|
before calling dc_stop_jobs() and dc_context_unref
|
||||||
|
|
||||||
|
event API:
|
||||||
|
get_data1_int
|
||||||
|
get_data2_int
|
||||||
|
get_data3_str
|
||||||
|
|
||||||
|
|
||||||
|
- userdata likely only used for the callbacks, likely can be dropped, needs verification
|
||||||
|
|
||||||
|
|
||||||
|
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going
|
||||||
|
|
||||||
341
CHANGELOG.md
341
CHANGELOG.md
@@ -1,5 +1,346 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.39.0
|
||||||
|
|
||||||
|
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658
|
||||||
|
|
||||||
|
- fix potential panics, update dependencies #1650 #1655
|
||||||
|
|
||||||
|
|
||||||
|
## 1.38.0
|
||||||
|
|
||||||
|
- fix sorting, esp. for multi-device
|
||||||
|
|
||||||
|
|
||||||
|
## 1.37.0
|
||||||
|
|
||||||
|
- improve ndn heuristics #1630
|
||||||
|
|
||||||
|
- get oauth2 authorizer from provider-db #1641
|
||||||
|
|
||||||
|
- removed linebreaks and spaces from generated qr-code #1631
|
||||||
|
|
||||||
|
- more fixes #1633 #1635 #1636 #1637
|
||||||
|
|
||||||
|
|
||||||
|
## 1.36.0
|
||||||
|
|
||||||
|
- parse ndn (network delivery notification) reports
|
||||||
|
and report failed messages as such #1552 #1622 #1630
|
||||||
|
|
||||||
|
- add oauth2 support for gsuite domains #1626
|
||||||
|
|
||||||
|
- read image orientation from exif before recoding #1619
|
||||||
|
|
||||||
|
- improve logging #1593 #1598
|
||||||
|
|
||||||
|
- improve python and bot bindings #1583 #1609
|
||||||
|
|
||||||
|
- improve imap logout #1595
|
||||||
|
|
||||||
|
- fix sorting #1600 #1604
|
||||||
|
|
||||||
|
- fix qr code generation #1631
|
||||||
|
|
||||||
|
- update rustcrypto releases #1603
|
||||||
|
|
||||||
|
- refactorings #1617
|
||||||
|
|
||||||
|
|
||||||
|
## 1.35.0
|
||||||
|
|
||||||
|
- enable strict-tls from a new provider-db setting #1587
|
||||||
|
|
||||||
|
- new subject 'Message from USER' for one-to-one chats #1395
|
||||||
|
|
||||||
|
- recode images #1563
|
||||||
|
|
||||||
|
- improve reconnect handling #1549 #1580
|
||||||
|
|
||||||
|
- improve importing addresses #1544
|
||||||
|
|
||||||
|
- improve configure and folder detection #1539 #1548
|
||||||
|
|
||||||
|
- improve test suite #1559 #1564 #1580 #1581 #1582 #1584 #1588:
|
||||||
|
|
||||||
|
- fix ad-hoc groups #1566
|
||||||
|
|
||||||
|
- preventions against being marked as spam #1575
|
||||||
|
|
||||||
|
- refactorings #1542 #1569
|
||||||
|
|
||||||
|
|
||||||
|
## 1.34.0
|
||||||
|
|
||||||
|
- new api for io, thread and event handling #1356,
|
||||||
|
see the example atop of `deltachat.h` to get an overview
|
||||||
|
|
||||||
|
- LOTS of speed improvements due to async processing #1356
|
||||||
|
|
||||||
|
- enable WAL mode for sqlite #1492
|
||||||
|
|
||||||
|
- process incoming messages in bulk #1527
|
||||||
|
|
||||||
|
- improve finding out the sent-folder #1488
|
||||||
|
|
||||||
|
- several bug fixes
|
||||||
|
|
||||||
|
|
||||||
|
## 1.33.0
|
||||||
|
|
||||||
|
- let `dc_set_muted()` also mute one-to-one chats #1470
|
||||||
|
|
||||||
|
- fix a bug that led to load and traffic if the server does not use sent-folder
|
||||||
|
#1472
|
||||||
|
|
||||||
|
|
||||||
|
## 1.32.0
|
||||||
|
|
||||||
|
- fix endless loop when trying to download messages with bad RFC Message-ID,
|
||||||
|
also be more reliable on similar errors #1463 #1466 #1462
|
||||||
|
|
||||||
|
- fix bug with comma in contact request #1438
|
||||||
|
|
||||||
|
- do not refer to hidden messages on replies #1459
|
||||||
|
|
||||||
|
- improve error handling #1468 #1465 #1464
|
||||||
|
|
||||||
|
|
||||||
|
## 1.31.0
|
||||||
|
|
||||||
|
- always describe the context of the displayed error #1451
|
||||||
|
|
||||||
|
- do not emit `DC_EVENT_ERROR` when message sending fails;
|
||||||
|
`dc_msg_get_state()` and `dc_get_msg_info()` are sufficient #1451
|
||||||
|
|
||||||
|
- new config-option `media_quality` #1449
|
||||||
|
|
||||||
|
- try over if writing message to database fails #1447
|
||||||
|
|
||||||
|
|
||||||
|
## 1.30.0
|
||||||
|
|
||||||
|
- expunge deleted messages #1440
|
||||||
|
|
||||||
|
- do not send `DC_EVENT_MSGS_CHANGED|INCOMING_MSG` on hidden messages #1439
|
||||||
|
|
||||||
|
|
||||||
|
## 1.29.0
|
||||||
|
|
||||||
|
- new config options `delete_device_after` and `delete_server_after`,
|
||||||
|
each taking an amount of seconds after which messages
|
||||||
|
are deleted from the device and/or the server #1310 #1335 #1411 #1417 #1423
|
||||||
|
|
||||||
|
- new api `dc_estimate_deletion_cnt()` to estimate the effect
|
||||||
|
of `delete_device_after` and `delete_server_after`
|
||||||
|
|
||||||
|
- use Ed25519 keys by default, these keys are much shorter
|
||||||
|
than RSA keys, which results in saving traffic and speed improvements #1362
|
||||||
|
|
||||||
|
- improve message ellipsizing #1397 #1430
|
||||||
|
|
||||||
|
- emit `DC_EVENT_ERROR_NETWORK` also on smtp-errors #1378
|
||||||
|
|
||||||
|
- do not show badly formatted non-delta-messages as empty #1384
|
||||||
|
|
||||||
|
- try over SMTP on potentially recoverable error 5.5.0 #1379
|
||||||
|
|
||||||
|
- remove device-chat from forward-to-chat-list #1367
|
||||||
|
|
||||||
|
- improve group-handling #1368
|
||||||
|
|
||||||
|
- `dc_get_info()` returns uptime (how long the context is in use)
|
||||||
|
|
||||||
|
- python improvements and adaptions #1408 #1415
|
||||||
|
|
||||||
|
- log to the stdout and stderr in tests #1416
|
||||||
|
|
||||||
|
- refactoring, code improvements #1363 #1365 #1366 #1370 #1375 #1389 #1390 #1418 #1419
|
||||||
|
|
||||||
|
- removed api: `dc_chat_get_subtitle()`, `dc_get_version_str()`, `dc_array_add_id()`
|
||||||
|
|
||||||
|
- removed events: `DC_EVENT_MEMBER_ADDED`, `DC_EVENT_MEMBER_REMOVED`
|
||||||
|
|
||||||
|
|
||||||
|
## 1.28.0
|
||||||
|
|
||||||
|
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
|
||||||
|
that will sort the "saved messages" chat to the top of the chatlist #1336
|
||||||
|
- mark mails as being deleted from server in dc_empty_server() #1333
|
||||||
|
- fix interaction with servers that do not allow folder creation on root-level;
|
||||||
|
use path separator as defined by the email server #1359
|
||||||
|
- fix group creation if group was created by non-delta clients #1357
|
||||||
|
- fix showing replies from non-delta clients #1353
|
||||||
|
- fix member list on rejoining left groups #1343
|
||||||
|
- fix crash when using empty groups #1354
|
||||||
|
- fix potential crash on special names #1350
|
||||||
|
|
||||||
|
|
||||||
|
## 1.27.0
|
||||||
|
|
||||||
|
- handle keys reliably on armv7 #1327
|
||||||
|
|
||||||
|
|
||||||
|
## 1.26.0
|
||||||
|
|
||||||
|
- change generated key type back to RSA as shipped versions
|
||||||
|
have problems to encrypt to Ed25519 keys
|
||||||
|
|
||||||
|
- update rPGP to encrypt reliably to Ed25519 keys;
|
||||||
|
one of the next versions can finally use Ed25519 keys then
|
||||||
|
|
||||||
|
|
||||||
|
## 1.25.0
|
||||||
|
|
||||||
|
- save traffic by downloading only messages that are really displayed #1236
|
||||||
|
|
||||||
|
- change generated key type to Ed25519, these keys are much shorter
|
||||||
|
than RSA keys, which results in saving traffic and speed improvements #1287
|
||||||
|
|
||||||
|
- improve key handling #1237 #1240 #1242 #1247
|
||||||
|
|
||||||
|
- mute handling, apis are dc_set_chat_mute_duration()
|
||||||
|
dc_chat_is_muted() and dc_chat_get_remaining_mute_duration() #1143
|
||||||
|
|
||||||
|
- pinning chats, new apis are dc_set_chat_visibility() and
|
||||||
|
dc_chat_get_visibility() #1248
|
||||||
|
|
||||||
|
- add dc_provider_new_from_email() api that queries the new, integrated
|
||||||
|
provider-database #1207
|
||||||
|
|
||||||
|
- account creation by scanning a qr code
|
||||||
|
in the DCACCOUNT scheme (https://mailadm.readthedocs.io),
|
||||||
|
new api is dc_set_config_from_qr() #1249
|
||||||
|
|
||||||
|
- if possible, dc_join_securejoin(), returns the new chat-id immediately
|
||||||
|
and does the handshake in background #1225
|
||||||
|
|
||||||
|
- update imap and smtp dependencies #1115
|
||||||
|
|
||||||
|
- check for MOVE capability before using MOVE command #1263
|
||||||
|
|
||||||
|
- allow inline attachments from RFC 2183 #1280
|
||||||
|
|
||||||
|
- fix updating names from incoming mails #1298
|
||||||
|
|
||||||
|
- fix error messages shown on import #1234
|
||||||
|
|
||||||
|
- directly attempt to re-connect if the smtp connection is maybe stale #1296
|
||||||
|
|
||||||
|
- improve adding group members #1291
|
||||||
|
|
||||||
|
- improve rust-api #1261
|
||||||
|
|
||||||
|
- cleanup #1302 #1283 #1282 #1276 #1270-#1274 #1267 #1258-#1260
|
||||||
|
#1257 #1239 #1231 #1224
|
||||||
|
|
||||||
|
- update spec #1286 #1291
|
||||||
|
|
||||||
|
|
||||||
|
## 1.0.0-beta.24
|
||||||
|
|
||||||
|
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219
|
||||||
|
|
||||||
|
- fix panic when receiving eg. cyrillic filenames #1216
|
||||||
|
|
||||||
|
- delete all consumed secure-join handshake messagess #1209 #1212
|
||||||
|
|
||||||
|
- rust-level cleanups #1218 #1217 #1210 #1205
|
||||||
|
|
||||||
|
- python-level cleanups #1204 #1202 #1201
|
||||||
|
|
||||||
|
|
||||||
|
## 1.0.0-beta.23
|
||||||
|
|
||||||
|
- #1197 fix imap-deletion of messages
|
||||||
|
|
||||||
|
- #1171 Combine multiple MDNs into a single mail, reducing traffic
|
||||||
|
|
||||||
|
- #1155 fix to not send out gossip always, reducing traffic
|
||||||
|
|
||||||
|
- #1160 fix reply-to-encrypted determination
|
||||||
|
|
||||||
|
- #1182 Add "Auto-Submitted: auto-replied" header to MDNs
|
||||||
|
|
||||||
|
- #1194 produce python wheels again, fix c/py.delta.chat
|
||||||
|
master-deployment
|
||||||
|
|
||||||
|
- rust-level housekeeping and improvements #1161 #1186 #1185 #1190 #1194 #1199 #1191 #1190 #1184 and more
|
||||||
|
|
||||||
|
- #1063 clarify licensing
|
||||||
|
|
||||||
|
- #1147 use mailparse 0.10.2
|
||||||
|
|
||||||
|
|
||||||
|
## 1.0.0-beta.22
|
||||||
|
|
||||||
|
- #1095 normalize email lineends to CRLF
|
||||||
|
|
||||||
|
- #1095 enable link-time-optimization, saves eg. on android 11 mb
|
||||||
|
|
||||||
|
- #1099 fix import regarding devicechats
|
||||||
|
|
||||||
|
- #1092 improve logging
|
||||||
|
|
||||||
|
- #1096 #1097 #1094 #1090 #1091 internal cleanups
|
||||||
|
|
||||||
|
## 1.0.0-beta.21
|
||||||
|
|
||||||
|
- #1078 #1082 ensure RFC compliance by producing 78 column lines for
|
||||||
|
encoded attachments.
|
||||||
|
|
||||||
|
- #1080 don't recreate and thus break group membership if an unknown
|
||||||
|
sender (or mailer-daemon) sends a message referencing the group chat
|
||||||
|
|
||||||
|
- #1081 #1079 some internal cleanups
|
||||||
|
|
||||||
|
- update imap-proto dependency, to fix yandex/oauth
|
||||||
|
|
||||||
|
## 1.0.0-beta.20
|
||||||
|
|
||||||
|
- #1074 fix OAUTH2/gmail
|
||||||
|
- #1072 fix group members not appearing in contact list
|
||||||
|
- #1071 never block interrupt_idle (thus hopefully also not on maybe_network())
|
||||||
|
- #1069 reduce smtp-timeout to 30 seconds
|
||||||
|
- #1066 #1065 avoid unwrap in dehtml, make literals more readable
|
||||||
|
|
||||||
|
## 1.0.0-beta.19
|
||||||
|
|
||||||
|
- #1058 timeout smtp-send if it doesn't complete in 15 minutes
|
||||||
|
|
||||||
|
- #1059 trim down logging
|
||||||
|
|
||||||
|
## 1.0.0-beta.18
|
||||||
|
|
||||||
|
- #1056 avoid panicking when we couldn't read imap-server's greeting
|
||||||
|
message
|
||||||
|
|
||||||
|
- #1055 avoid panicking when we don't have a selected folder
|
||||||
|
|
||||||
|
- #1052 #1049 #1051 improve logging to add thread-id/name and
|
||||||
|
file/lineno to each info/warn message.
|
||||||
|
|
||||||
|
- #1050 allow python bindings to initialize Account with "os_name".
|
||||||
|
|
||||||
|
|
||||||
|
## 1.0.0-beta.17
|
||||||
|
|
||||||
|
- #1044 implement avatar recoding to 192x192 in core to keep file sizes small.
|
||||||
|
|
||||||
|
- #1024 fix #1021 SQL/injection malformed Chat-Group-Name breakage
|
||||||
|
|
||||||
|
- #1036 fix smtp crash by pulling in a fixed async-smtp
|
||||||
|
|
||||||
|
- #1039 fix read-receipts appearing as normal messages when you change
|
||||||
|
MDN settings
|
||||||
|
|
||||||
|
- #1040 do not panic on SystemTimeDifference
|
||||||
|
|
||||||
|
- #1043 avoid potential crashes in malformed From/Chat-Disposition... headers
|
||||||
|
|
||||||
|
- #1045 #1041 #1038 #1035 #1034 #1029 #1025 various cleanups and doc
|
||||||
|
improvments
|
||||||
|
|
||||||
## 1.0.0-beta.16
|
## 1.0.0-beta.16
|
||||||
|
|
||||||
- alleviate login problems with providers which only
|
- alleviate login problems with providers which only
|
||||||
|
|||||||
4098
Cargo.lock
generated
4098
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
73
Cargo.toml
73
Cargo.toml
@@ -1,66 +1,78 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.0.0-beta.16"
|
version = "1.39.0"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
|
|
||||||
libc = "0.2.51"
|
libc = "0.2.51"
|
||||||
pgp = { version = "0.4.0", default-features = false }
|
pgp = { version = "0.6.0", default-features = false }
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
sha2 = "0.8.0"
|
sha2 = "0.9.0"
|
||||||
rand = "0.7.0"
|
rand = "0.7.0"
|
||||||
smallvec = "1.0.0"
|
smallvec = "1.0.0"
|
||||||
reqwest = { version = "0.9.15" }
|
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||||
num-derive = "0.3.0"
|
num-derive = "0.3.0"
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch = "master" }
|
async-smtp = { version = "0.3" }
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "native_tls" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
async-imap = "0.3.1"
|
||||||
async-native-tls = "0.1.1"
|
async-native-tls = { version = "0.3.3" }
|
||||||
async-std = { version = "1.0", features = ["unstable"] }
|
async-std = { version = "1.6.1", features = ["unstable"] }
|
||||||
base64 = "0.11"
|
base64 = "0.12"
|
||||||
charset = "0.1"
|
charset = "0.1"
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.6"
|
||||||
failure = "0.1.5"
|
|
||||||
failure_derive = "0.1.5"
|
|
||||||
indexmap = "1.3.0"
|
indexmap = "1.3.0"
|
||||||
# TODO: make optional
|
kamadak-exif = "0.5"
|
||||||
rustyline = "4.1.0"
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
regex = "1.1.6"
|
regex = "1.1.6"
|
||||||
rusqlite = { version = "0.20", features = ["bundled"] }
|
strum = "0.18.0"
|
||||||
r2d2_sqlite = "0.12.0"
|
strum_macros = "0.18.0"
|
||||||
r2d2 = "0.8.5"
|
|
||||||
strum = "0.16.0"
|
|
||||||
strum_macros = "0.16.0"
|
|
||||||
thread-local-object = "0.1.0"
|
|
||||||
backtrace = "0.3.33"
|
backtrace = "0.3.33"
|
||||||
byteorder = "1.3.1"
|
byteorder = "1.3.1"
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
image-meta = "0.1.0"
|
image-meta = "0.1.0"
|
||||||
quick-xml = "0.17.1"
|
quick-xml = "0.18.1"
|
||||||
escaper = "0.1.0"
|
escaper = "0.1.0"
|
||||||
bitflags = "1.1.0"
|
bitflags = "1.1.0"
|
||||||
debug_stub_derive = "0.3.0"
|
|
||||||
sanitize-filename = "0.2.1"
|
sanitize-filename = "0.2.1"
|
||||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||||
mailparse = "0.10.1"
|
mailparse = "0.12.1"
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||||
native-tls = "0.2.3"
|
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"
|
||||||
|
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std", "sqlite", "macros"] }
|
||||||
|
libsqlite3-sys = { version = "0.18", features = ["bundled", "min_sqlite_version_3_7_16"] }
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
pretty_assertions = "0.6.1"
|
pretty_assertions = "0.6.1"
|
||||||
pretty_env_logger = "0.3.0"
|
pretty_env_logger = "0.4.0"
|
||||||
proptest = "0.9.4"
|
proptest = "0.10"
|
||||||
|
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
|
||||||
|
smol = "0.1.11"
|
||||||
|
log = "0.4.8"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
@@ -71,14 +83,17 @@ members = [
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "simple"
|
name = "simple"
|
||||||
path = "examples/simple.rs"
|
path = "examples/simple.rs"
|
||||||
|
required-features = ["repl"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "repl"
|
name = "repl"
|
||||||
path = "examples/repl/main.rs"
|
path = "examples/repl/main.rs"
|
||||||
|
required-features = ["repl"]
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["nightly", "ringbuf"]
|
default = []
|
||||||
vendored = ["native-tls/vendored", "reqwest/default-tls-vendored"]
|
internals = []
|
||||||
|
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
|
||||||
|
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||||
nightly = ["pgp/nightly"]
|
nightly = ["pgp/nightly"]
|
||||||
ringbuf = ["pgp/ringbuf"]
|
|
||||||
|
|||||||
3
LICENSE
3
LICENSE
@@ -2,9 +2,6 @@ The files in this directory and under its subdirectories
|
|||||||
are (c) 2019 by Bjoern Petersen and contributors and released under the
|
are (c) 2019 by Bjoern Petersen and contributors and released under the
|
||||||
Mozilla Public License Version 2.0, see below for a copy.
|
Mozilla Public License Version 2.0, see below for a copy.
|
||||||
|
|
||||||
NOTE that the files in the "libs" directory are copyrighted by third parties
|
|
||||||
and come with their own respective licenses.
|
|
||||||
|
|
||||||
Mozilla Public License Version 2.0
|
Mozilla Public License Version 2.0
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -9,7 +9,7 @@
|
|||||||
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://sh.rustup.rs -sSf | sh
|
$ curl https://sh.rustup.rs -sSf | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using the CLI client
|
## Using the CLI client
|
||||||
@@ -17,8 +17,9 @@ curl https://sh.rustup.rs -sSf | sh
|
|||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo run --example repl -- /path/to/db
|
$ RUST_LOG=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.
|
||||||
|
|
||||||
Configure your account (if not already configured):
|
Configure your account (if not already configured):
|
||||||
|
|
||||||
@@ -108,9 +109,29 @@ $ cargo test -- --ignored
|
|||||||
|
|
||||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||||
- `nightly`: Enable nightly only performance and security related features.
|
- `nightly`: Enable nightly only performance and security related features.
|
||||||
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
|
|
||||||
|
|
||||||
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
|
[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/
|
[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-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
|
[appveyor]: https://ci.appveyor.com/project/dignifiedquire/deltachat-core-rust/branch/master
|
||||||
|
|
||||||
|
## Language bindings and frontend projects
|
||||||
|
|
||||||
|
Language bindings are available for:
|
||||||
|
|
||||||
|
- [C](https://c.delta.chat)
|
||||||
|
- [Node.js](https://www.npmjs.com/package/deltachat-node)
|
||||||
|
- [Python](https://py.delta.chat)
|
||||||
|
- [Go](https://github.com/hugot/go-deltachat/)
|
||||||
|
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
|
||||||
|
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||||
|
|
||||||
|
The following "frontend" projects make use of the Rust-library
|
||||||
|
or its language bindings:
|
||||||
|
|
||||||
|
- [Android](https://github.com/deltachat/deltachat-android)
|
||||||
|
- [iOS](https://github.com/deltachat/deltachat-ios)
|
||||||
|
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||||
|
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||||
|
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||||
|
- several **Bots**
|
||||||
|
|||||||
19
appveyor.yml
19
appveyor.yml
@@ -1,19 +0,0 @@
|
|||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
|
||||||
|
|
||||||
install:
|
|
||||||
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
|
||||||
- rustup-init -yv --default-toolchain nightly-2019-07-10
|
|
||||||
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
|
||||||
- rustc -vV
|
|
||||||
- cargo -vV
|
|
||||||
|
|
||||||
build: false
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
- cargo test --release --all
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- target
|
|
||||||
- C:\Users\appveyor\.cargo\registry
|
|
||||||
@@ -14,24 +14,22 @@ DOXYDOCDIR=${3:?directory where doxygen docs to be found}
|
|||||||
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
||||||
|
|
||||||
|
|
||||||
# DISABLED: python docs to py.delta.chat
|
# python docs to py.delta.chat
|
||||||
#ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
|
||||||
#rsync -avz \
|
rsync -avz \
|
||||||
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
--delete \
|
||||||
# "$PYDOCDIR/html/" \
|
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
# delta@py.delta.chat:build/${BRANCH}
|
"$PYDOCDIR/html/" \
|
||||||
|
delta@py.delta.chat:build/${BRANCH}
|
||||||
|
|
||||||
# C docs to c.delta.chat
|
# 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}
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
|
||||||
rsync -avz \
|
rsync -avz \
|
||||||
|
--delete \
|
||||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
"$DOXYDOCDIR/html/" \
|
"$DOXYDOCDIR/html/" \
|
||||||
delta@c.delta.chat:build-c/${BRANCH}
|
delta@c.delta.chat:build-c/${BRANCH}
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
# OUTDATED -- for re-use from python release-scripts
|
|
||||||
|
|
||||||
echo -----------------------
|
echo -----------------------
|
||||||
echo upload wheels
|
echo upload wheels
|
||||||
echo -----------------------
|
echo -----------------------
|
||||||
@@ -39,6 +37,7 @@ echo -----------------------
|
|||||||
# Bundle external shared libraries into the wheels
|
# Bundle external shared libraries into the wheels
|
||||||
pushd $WHEELHOUSEDIR
|
pushd $WHEELHOUSEDIR
|
||||||
|
|
||||||
|
pip3 install -U pip setuptools
|
||||||
pip3 install devpi-client
|
pip3 install devpi-client
|
||||||
devpi use https://m.devpi.net
|
devpi use https://m.devpi.net
|
||||||
devpi login dc --password $DEVPI_LOGIN
|
devpi login dc --password $DEVPI_LOGIN
|
||||||
@@ -50,6 +49,9 @@ devpi use dc/$N_BRANCH || {
|
|||||||
devpi use dc/$N_BRANCH
|
devpi use dc/$N_BRANCH
|
||||||
}
|
}
|
||||||
devpi index $N_BRANCH bases=/root/pypi
|
devpi index $N_BRANCH bases=/root/pypi
|
||||||
devpi upload deltachat*.whl
|
devpi upload deltachat*
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
# remove devpi non-master dc indices if thy are too old
|
||||||
|
python ci_scripts/cleanup_devpi_indices.py
|
||||||
|
|||||||
72
ci_scripts/cleanup_devpi_indices.py
Normal file
72
ci_scripts/cleanup_devpi_indices.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Remove old "dc" indices except for master which always stays.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from requests import Session
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
MAXDAYS=7
|
||||||
|
|
||||||
|
session = Session()
|
||||||
|
session.headers["Accept"] = "application/json"
|
||||||
|
|
||||||
|
|
||||||
|
def get_indexes(baseurl, username):
|
||||||
|
response = session.get(baseurl + username)
|
||||||
|
assert response.status_code == 200
|
||||||
|
result = response.json()["result"]
|
||||||
|
return result["indexes"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_projectnames(baseurl, username, indexname):
|
||||||
|
response = session.get(baseurl + username + "/" + indexname)
|
||||||
|
assert response.status_code == 200
|
||||||
|
result = response.json()["result"]
|
||||||
|
return result["projects"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_release_dates(baseurl, username, indexname, projectname):
|
||||||
|
response = session.get(baseurl + username + "/" + indexname + "/" + projectname)
|
||||||
|
assert response.status_code == 200
|
||||||
|
result = response.json()["result"]
|
||||||
|
dates = set()
|
||||||
|
for value in result.values():
|
||||||
|
if "+links" not in value:
|
||||||
|
continue
|
||||||
|
for link in value["+links"]:
|
||||||
|
for log in link["log"]:
|
||||||
|
dates.add(tuple(log["when"]))
|
||||||
|
return dates
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
baseurl = "https://m.devpi.net/"
|
||||||
|
username = "dc"
|
||||||
|
for indexname in get_indexes(baseurl, username):
|
||||||
|
projectnames = get_projectnames(baseurl, username, indexname)
|
||||||
|
if indexname == "master" or not indexname:
|
||||||
|
continue
|
||||||
|
clear_index = not projectnames
|
||||||
|
for projectname in projectnames:
|
||||||
|
dates = get_release_dates(baseurl, username, indexname, projectname)
|
||||||
|
if not dates:
|
||||||
|
print(
|
||||||
|
"%s has no releases" % (baseurl + username + "/" + indexname),
|
||||||
|
file=sys.stderr)
|
||||||
|
date = datetime.datetime.now()
|
||||||
|
else:
|
||||||
|
date = datetime.datetime(*max(dates))
|
||||||
|
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
||||||
|
assert username and indexname
|
||||||
|
clear_index = True
|
||||||
|
break
|
||||||
|
if clear_index:
|
||||||
|
url = baseurl + username + "/" + indexname
|
||||||
|
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
FROM quay.io/pypa/manylinux1_x86_64
|
FROM quay.io/pypa/manylinux2010_x86_64
|
||||||
|
|
||||||
# Configure ld.so/ldconfig and pkg-config
|
# Configure ld.so/ldconfig and pkg-config
|
||||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||||
echo /usr/local/lib >> /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
|
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||||
|
|
||||||
# Install a recent Perl, needed to install OpenSSL
|
# Install a recent Perl, needed to install the openssl crate
|
||||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
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
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
# Install OpenSSL
|
|
||||||
ADD deps/build_openssl.sh /builder/build_openssl.sh
|
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
|
|
||||||
|
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||||
|
|
||||||
# Install python tools (auditwheels,tox, ...)
|
# Install python tools (auditwheels,tox, ...)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
set -e -x
|
set -e -x
|
||||||
|
|
||||||
# Install Rust
|
# Install Rust
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-09-12 -y
|
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
|
||||||
export PATH=/root/.cargo/bin:$PATH
|
export PATH=/root/.cargo/bin:$PATH
|
||||||
rustc --version
|
rustc --version
|
||||||
|
|
||||||
|
# remove some 300-400 MB that we don't need for automated builds
|
||||||
|
rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Build the Delta Chat C/Rust library
|
|
||||||
#
|
|
||||||
set -e -x
|
|
||||||
|
|
||||||
# perform clean build of core and install
|
|
||||||
export TOXWORKDIR=.docker-tox
|
|
||||||
|
|
||||||
# build core library
|
|
||||||
|
|
||||||
cargo build --release -p deltachat_ffi
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
#
|
|
||||||
# run python tests
|
|
||||||
#
|
|
||||||
|
|
||||||
if [ -n "$TESTS" ]; then
|
|
||||||
|
|
||||||
echo ----------------
|
|
||||||
echo run python tests
|
|
||||||
echo ----------------
|
|
||||||
|
|
||||||
pushd python
|
|
||||||
# first run all tests ...
|
|
||||||
rm -rf tests/__pycache__
|
|
||||||
rm -rf src/deltachat/__pycache__
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
tox --workdir "$TOXWORKDIR" -e py27,py35,py36,py37
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -n "$DOCS" ]; then
|
|
||||||
echo -----------------------
|
|
||||||
echo generating python docs
|
|
||||||
echo -----------------------
|
|
||||||
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
|
||||||
fi
|
|
||||||
@@ -46,6 +46,7 @@ if [ -n "$TESTS" ]; then
|
|||||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||||
unset DCC_PY_LIVECONFIG
|
unset DCC_PY_LIVECONFIG
|
||||||
|
unset DCC_NEW_TMP_EMAIL
|
||||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||||
popd
|
popd
|
||||||
|
|||||||
51
ci_scripts/remote_python_packaging.sh
Executable file
51
ci_scripts/remote_python_packaging.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/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}
|
||||||
|
|
||||||
|
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
|
||||||
|
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||||
|
git ls-files >.rsynclist
|
||||||
|
# we seem to need .git for setuptools_scm versioning
|
||||||
|
find .git >>.rsynclist
|
||||||
|
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
|
set +x
|
||||||
|
|
||||||
|
# we have to create a remote file for the remote-docker run
|
||||||
|
# so we can do a simple ssh command with a TTY
|
||||||
|
# so that when our job dies, all container-runs are aborted.
|
||||||
|
# sidenote: the circle-ci machinery will kill ongoing jobs
|
||||||
|
# if there are new commits and we want to ensure that
|
||||||
|
# everything is terminated/cleaned up and we have no orphaned
|
||||||
|
# useless still-running docker-containers consuming resources.
|
||||||
|
|
||||||
|
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||||
|
set +x -e
|
||||||
|
cd $BUILDDIR
|
||||||
|
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||||
|
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 \
|
||||||
|
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||||
|
deltachat/coredeps ci_scripts/run_all.sh
|
||||||
|
|
||||||
|
_HERE
|
||||||
|
|
||||||
|
echo "--- Running $CIRCLE_JOB remotely"
|
||||||
|
|
||||||
|
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||||
|
mkdir -p workspace
|
||||||
|
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||||
|
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||||
|
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||||
@@ -30,6 +30,7 @@ ssh $SSHTARGET <<_HERE
|
|||||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||||
export TARGET=release
|
export TARGET=release
|
||||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
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
|
#we rely on tox/virtualenv being available in the host
|
||||||
#rm -rf virtualenv venv
|
#rm -rf virtualenv venv
|
||||||
|
|||||||
@@ -3,5 +3,4 @@
|
|||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
cd deltachat-ffi
|
cd deltachat-ffi
|
||||||
doxygen
|
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
set -e -x
|
set -e -x
|
||||||
|
|
||||||
# for core-building and python install step
|
# for core-building and python install step
|
||||||
export DCC_RS_TARGET=release
|
export DCC_RS_TARGET=debug
|
||||||
export DCC_RS_DEV=`pwd`
|
export DCC_RS_DEV=`pwd`
|
||||||
|
|
||||||
cd python
|
cd python
|
||||||
|
|||||||
50
ci_scripts/run_all.sh
Executable file
50
ci_scripts/run_all.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Build the Delta Chat Core Rust library, Python wheels and docs
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
# Perform clean build of core and install.
|
||||||
|
export TOXWORKDIR=.docker-tox
|
||||||
|
|
||||||
|
# compile 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
|
||||||
|
rm -f python3.5
|
||||||
|
ln -s /opt/python/cp35-cp35m/bin/python3.5
|
||||||
|
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||||
|
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||||
|
ln -s /opt/python/cp38-cp38/bin/python3.8
|
||||||
|
popd
|
||||||
|
|
||||||
|
pushd python
|
||||||
|
# prepare a clean tox run
|
||||||
|
rm -rf tests/__pycache__
|
||||||
|
rm -rf src/deltachat/__pycache__
|
||||||
|
mkdir -p $TOXWORKDIR
|
||||||
|
|
||||||
|
# disable live-account testing to speed up test runs and wheel building
|
||||||
|
# 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
|
||||||
|
popd
|
||||||
|
|
||||||
|
|
||||||
|
echo -----------------------
|
||||||
|
echo generating python docs
|
||||||
|
echo -----------------------
|
||||||
|
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.0.0-beta.16"
|
version = "1.39.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||||
categories = ["cryptography", "std", "email"]
|
categories = ["cryptography", "std", "email"]
|
||||||
@@ -16,14 +16,16 @@ crate-type = ["cdylib", "staticlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat = { path = "../", default-features = false }
|
deltachat = { path = "../", default-features = false }
|
||||||
deltachat-provider-database = "0.2.1"
|
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
human-panic = "1.0.1"
|
human-panic = "1.0.1"
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
failure = "0.1.6"
|
serde_json = "1.0"
|
||||||
|
async-std = "1.6.0"
|
||||||
|
anyhow = "1.0.28"
|
||||||
|
thiserror = "1.0.14"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored", "nightly", "ringbuf"]
|
default = ["vendored"]
|
||||||
vendored = ["deltachat/vendored"]
|
vendored = ["deltachat/vendored"]
|
||||||
nightly = ["deltachat/nightly"]
|
nightly = ["deltachat/nightly"]
|
||||||
ringbuf = ["deltachat/ringbuf"]
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ use crate::location::Location;
|
|||||||
|
|
||||||
/* * the structure behind dc_array_t */
|
/* * the structure behind dc_array_t */
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
pub enum dc_array_t {
|
pub enum dc_array_t {
|
||||||
Locations(Vec<Location>),
|
Locations(Vec<Location>),
|
||||||
Uint(Vec<u32>),
|
Uint(Vec<u32>),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
|||||||
extern crate deltachat_provider_database;
|
|
||||||
|
|
||||||
use std::ptr;
|
|
||||||
|
|
||||||
use crate::string::{to_string_lossy, StrExt};
|
|
||||||
use deltachat_provider_database::StatusState;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub type dc_provider_t = deltachat_provider_database::Provider;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_new_from_domain(
|
|
||||||
domain: *const libc::c_char,
|
|
||||||
) -> *const dc_provider_t {
|
|
||||||
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_new_from_email(
|
|
||||||
email: *const libc::c_char,
|
|
||||||
) -> *const dc_provider_t {
|
|
||||||
let email = to_string_lossy(email);
|
|
||||||
let domain = deltachat_provider_database::get_domain_from_email(&email);
|
|
||||||
match deltachat_provider_database::get_provider_info(domain) {
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! null_guard {
|
|
||||||
($context:tt) => {
|
|
||||||
if $context.is_null() {
|
|
||||||
return ptr::null_mut() as *mut libc::c_char;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_overview_page(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
|
|
||||||
(*provider).overview_page
|
|
||||||
)
|
|
||||||
.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).name.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_markdown(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).markdown.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_status_date(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).status.date.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
|
|
||||||
if provider.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
match (*provider).status.state {
|
|
||||||
StatusState::OK => 1,
|
|
||||||
StatusState::PREPARATION => 2,
|
|
||||||
StatusState::BROKEN => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {
|
|
||||||
()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO expose general provider overview url?
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use failure::Fail;
|
|
||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
/// Duplicates a string
|
/// Duplicates a string
|
||||||
///
|
///
|
||||||
@@ -8,7 +8,7 @@ use std::ffi::{CStr, CString};
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust,norun
|
/// ```rust,norun
|
||||||
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
/// use crate::string::{dc_strdup, to_string_lossy};
|
||||||
/// unsafe {
|
/// unsafe {
|
||||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
||||||
/// let str_a_copy = dc_strdup(str_a);
|
/// let str_a_copy = dc_strdup(str_a);
|
||||||
@@ -16,7 +16,7 @@ use std::ffi::{CStr, CString};
|
|||||||
/// assert_ne!(str_a, str_a_copy);
|
/// assert_ne!(str_a, str_a_copy);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||||
let ret: *mut libc::c_char;
|
let ret: *mut libc::c_char;
|
||||||
if !s.is_null() {
|
if !s.is_null() {
|
||||||
ret = libc::strdup(s);
|
ret = libc::strdup(s);
|
||||||
@@ -30,13 +30,13 @@ pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Error type for the [OsStrExt] trait
|
/// Error type for the [OsStrExt] trait
|
||||||
#[derive(Debug, Fail, PartialEq)]
|
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||||
pub enum CStringError {
|
pub(crate) enum CStringError {
|
||||||
/// The string contains an interior null byte
|
/// The string contains an interior null byte
|
||||||
#[fail(display = "String contains an interior null byte")]
|
#[error("String contains an interior null byte")]
|
||||||
InteriorNullByte,
|
InteriorNullByte,
|
||||||
/// The string is not valid Unicode
|
/// The string is not valid Unicode
|
||||||
#[fail(display = "String is not valid unicode")]
|
#[error("String is not valid unicode")]
|
||||||
NotUnicode,
|
NotUnicode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ pub enum CStringError {
|
|||||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait OsStrExt {
|
pub(crate) trait OsStrExt {
|
||||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
||||||
///
|
///
|
||||||
/// This is useful to convert e.g. a [std::path::Path] to
|
/// This is useful to convert e.g. a [std::path::Path] to
|
||||||
@@ -130,27 +130,28 @@ fn os_str_to_c_string_unicode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience methods/associated functions for working with [CString]
|
/// Convenience methods/associated functions for working with [CString]
|
||||||
///
|
trait CStringExt {
|
||||||
/// This is helps transitioning from unsafe code.
|
/// Create a new [CString], best effort
|
||||||
pub trait CStringExt {
|
|
||||||
/// Create a new [CString], yolo style
|
|
||||||
///
|
///
|
||||||
/// This unwrap the result, panicking when there are embedded NULL
|
/// Like the [to_string_lossy] this doesn't give up in the face of
|
||||||
/// bytes.
|
/// bad input (embedded null bytes in this case) instead it does
|
||||||
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
/// the best it can by stripping the embedded null bytes.
|
||||||
CString::new(t).expect("String contains null byte, can not be CString")
|
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||||
|
let mut s = t.into();
|
||||||
|
s.retain(|&c| c != 0);
|
||||||
|
CString::new(s).unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CStringExt for CString {}
|
impl CStringExt for CString {}
|
||||||
|
|
||||||
/// Convenience methods to make transitioning from raw C strings easier.
|
/// Convenience methods to turn strings into C strings.
|
||||||
///
|
///
|
||||||
/// To interact with (legacy) C APIs we often need to convert from
|
/// To interact with (legacy) C APIs we often need to convert from
|
||||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
||||||
/// and the compiler sometimes allows it in an unsafe way. These
|
/// and the compiler sometimes allows it in an unsafe way. These
|
||||||
/// methods make it more succinct and help you get it right.
|
/// methods make it more succinct and help you get it right.
|
||||||
pub trait StrExt {
|
pub(crate) trait Strdup {
|
||||||
/// Allocate a new raw C `*char` version of this string.
|
/// Allocate a new raw C `*char` version of this string.
|
||||||
///
|
///
|
||||||
/// This allocates a new raw C string which must be freed using
|
/// This allocates a new raw C string which must be freed using
|
||||||
@@ -167,14 +168,52 @@ pub trait StrExt {
|
|||||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>> StrExt for T {
|
impl<T: AsRef<str>> Strdup for T {
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
let tmp = CString::yolo(self.as_ref());
|
let tmp = CString::new_lossy(self.as_ref());
|
||||||
dc_strdup(tmp.as_ptr())
|
dc_strdup(tmp.as_ptr())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
// We can not implement for AsRef<OsStr> because we already implement
|
||||||
|
// AsRev<str> and this conflicts. So implement for Path directly.
|
||||||
|
impl Strdup for std::path::Path {
|
||||||
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
|
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||||
|
dc_strdup(tmp.as_ptr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience methods to turn optional strings into C strings.
|
||||||
|
///
|
||||||
|
/// This is the same as the [Strdup] trait but a different trait name
|
||||||
|
/// to work around the type system not allowing to implement [Strdup]
|
||||||
|
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
|
||||||
|
/// for `AsRef<&str>`.
|
||||||
|
///
|
||||||
|
/// When the [Option] is [Option::Some] this behaves just like
|
||||||
|
/// [Strdup::strdup], when it is [Option::None] a null pointer is
|
||||||
|
/// returned.
|
||||||
|
pub(crate) trait OptStrdup {
|
||||||
|
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||||
|
///
|
||||||
|
/// See [Strdup::strdup] for details.
|
||||||
|
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||||
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
|
match self {
|
||||||
|
Some(s) => {
|
||||||
|
let tmp = CString::new_lossy(s.as_ref());
|
||||||
|
dc_strdup(tmp.as_ptr())
|
||||||
|
}
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||||
if s.is_null() {
|
if s.is_null() {
|
||||||
return "".into();
|
return "".into();
|
||||||
}
|
}
|
||||||
@@ -184,7 +223,7 @@ pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
|||||||
cstr.to_string_lossy().to_string()
|
cstr.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||||
if s.is_null() {
|
if s.is_null() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -205,7 +244,7 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
|||||||
///
|
///
|
||||||
/// [Path]: std::path::Path
|
/// [Path]: std::path::Path
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -217,7 +256,7 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|||||||
|
|
||||||
// as_path() implementation for windows, documented above.
|
// as_path() implementation for windows, documented above.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
as_path_unicode(s)
|
as_path_unicode(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,8 +363,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cstring_yolo() {
|
fn test_cstring_new_lossy() {
|
||||||
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
assert!(CString::new("hel\x00lo").is_err());
|
||||||
|
assert!(CString::new(String::from("hel\x00o")).is_err());
|
||||||
|
let r = CString::new("hello").unwrap();
|
||||||
|
assert_eq!(CString::new_lossy("hello"), r);
|
||||||
|
assert_eq!(CString::new_lossy("hel\x00lo"), r);
|
||||||
|
assert_eq!(CString::new_lossy(String::from("hello")), r);
|
||||||
|
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -347,4 +392,19 @@ mod tests {
|
|||||||
assert_eq!(cmp, 0);
|
assert_eq!(cmp, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strdup_opt_string() {
|
||||||
|
unsafe {
|
||||||
|
let s = Some("hello");
|
||||||
|
let c = s.strdup();
|
||||||
|
let cmp = strcmp(c, b"hello\x00" as *const u8 as *const libc::c_char);
|
||||||
|
free(c as *mut libc::c_void);
|
||||||
|
assert_eq!(cmp, 0);
|
||||||
|
|
||||||
|
let s: Option<&str> = None;
|
||||||
|
let c = s.strdup();
|
||||||
|
assert_eq!(c, ptr::null_mut());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_derive"
|
name = "deltachat_derive"
|
||||||
version = "0.1.0"
|
version = "2.0.0"
|
||||||
authors = ["Dmitry Bogatov <KAction@debian.org>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
syn = "0.14.4"
|
syn = "1.0.13"
|
||||||
quote = "0.6.3"
|
quote = "1.0.2"
|
||||||
|
|||||||
@@ -3,42 +3,38 @@ extern crate proc_macro;
|
|||||||
|
|
||||||
use crate::proc_macro::TokenStream;
|
use crate::proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn;
|
|
||||||
|
|
||||||
// For now, assume (not check) that these macroses are applied to enum without
|
// For now, assume (not check) that these macroses are applied to enum without
|
||||||
// data. If this assumption is violated, compiler error will point to
|
// data. If this assumption is violated, compiler error will point to
|
||||||
// generated code, which is not very user-friendly.
|
// generated code, which is not very user-friendly.
|
||||||
|
|
||||||
#[proc_macro_derive(ToSql)]
|
#[proc_macro_derive(Sqlx)]
|
||||||
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
pub fn sqlx_derive(input: TokenStream) -> TokenStream {
|
||||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||||
let name = &ast.ident;
|
let name = &ast.ident;
|
||||||
|
|
||||||
let gen = quote! {
|
let gen = quote! {
|
||||||
impl rusqlite::types::ToSql for #name {
|
impl<'q> sqlx::encode::Encode<'q, sqlx::sqlite::Sqlite> for #name {
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
fn encode_by_ref(&self, buf: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>) -> sqlx::encode::IsNull {
|
||||||
let num = *self as i64;
|
num_traits::ToPrimitive::to_i32(self).expect("invalid type").encode(buf)
|
||||||
let value = rusqlite::types::Value::Integer(num);
|
|
||||||
let output = rusqlite::types::ToSqlOutput::Owned(value);
|
|
||||||
std::result::Result::Ok(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
gen.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(FromSql)]
|
|
||||||
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
|
||||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
|
||||||
let name = &ast.ident;
|
|
||||||
|
|
||||||
let gen = quote! {
|
|
||||||
impl rusqlite::types::FromSql for #name {
|
|
||||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
||||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
|
||||||
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<'de> sqlx::decode::Decode<'de, sqlx::sqlite::Sqlite> for #name {
|
||||||
|
fn decode(value: sqlx::sqlite::SqliteValueRef) -> std::result::Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
|
||||||
|
let raw: i32 = sqlx::decode::Decode::decode(value)?;
|
||||||
|
|
||||||
|
Ok(num_traits::FromPrimitive::from_i32(raw).unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sqlx::types::Type<sqlx::sqlite::Sqlite> for #name {
|
||||||
|
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
|
||||||
|
<i32 as sqlx::types::Type<_>>::type_info()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
gen.into()
|
gen.into()
|
||||||
}
|
}
|
||||||
|
|||||||
126
draft/group-sync.rst
Normal file
126
draft/group-sync.rst
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- Alice adds Carol, 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.
|
||||||
|
|
||||||
|
Note that for verified groups any mitigation mechanism likely
|
||||||
|
needs to make all clients to know who originally added a member.
|
||||||
|
|
||||||
|
|
||||||
|
solution: memorize+attach (possible encrypted) chat-meta mime messages
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- mechanism works for both encrypted and unencrypted add/del messages
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- multiple member-added/removed messages can be attached in a single
|
||||||
|
correction message
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- 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
|
||||||
|
b) could lead to a lot of members sending corrections
|
||||||
|
|
||||||
|
- instead of broadcasting correction messages we could only send it to
|
||||||
|
the sender of the inconsistent member-added/removed message.
|
||||||
|
A receiver of such a correction message would then need to forward
|
||||||
|
the message to the members it thinks also have an inconsistent view.
|
||||||
|
This sounds complicated and error-prone. Concretely, if Alice
|
||||||
|
receives Bob's "Member-added: Doris" message, then Alice
|
||||||
|
broadcasting the correction message with "Member-added: Carol"
|
||||||
|
would reach all four members, healing group consistency in one step.
|
||||||
|
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
|
||||||
|
Bob would broadcast a correction message to all four members as well.
|
||||||
|
(Imagine a situation where Alice/Bob added Carol/Doris
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,27 +6,21 @@
|
|||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
#[macro_use]
|
|
||||||
extern crate failure;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate rusqlite;
|
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
|
|
||||||
|
use ansi_term::Color;
|
||||||
|
use anyhow::{bail, Error};
|
||||||
|
use async_std::path::Path;
|
||||||
|
use deltachat::chat::ChatId;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::configure::*;
|
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
use deltachat::job::*;
|
|
||||||
use deltachat::oauth2::*;
|
use deltachat::oauth2::*;
|
||||||
use deltachat::securejoin::*;
|
use deltachat::securejoin::*;
|
||||||
use deltachat::Event;
|
use deltachat::Event;
|
||||||
|
use log::{error, info, warn};
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::config::OutputStreamType;
|
use rustyline::config::OutputStreamType;
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
@@ -39,179 +33,81 @@ use rustyline::{
|
|||||||
mod cmdline;
|
mod cmdline;
|
||||||
use self::cmdline::*;
|
use self::cmdline::*;
|
||||||
|
|
||||||
// Event Handler
|
/// Event Handler
|
||||||
|
fn receive_event(event: Event) {
|
||||||
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
|
let yellow = Color::Yellow.normal();
|
||||||
match event {
|
match event {
|
||||||
Event::Info(msg) => {
|
Event::Info(msg) => {
|
||||||
/* do not show the event as this would fill the screen */
|
/* do not show the event as this would fill the screen */
|
||||||
println!("{}", msg);
|
info!("{}", msg);
|
||||||
}
|
}
|
||||||
Event::SmtpConnected(msg) => {
|
Event::SmtpConnected(msg) => {
|
||||||
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg);
|
info!("[SMTP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
Event::ImapConnected(msg) => {
|
Event::ImapConnected(msg) => {
|
||||||
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg);
|
info!("[IMAP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
Event::SmtpMessageSent(msg) => {
|
Event::SmtpMessageSent(msg) => {
|
||||||
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg);
|
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||||
}
|
}
|
||||||
Event::Warning(msg) => {
|
Event::Warning(msg) => {
|
||||||
println!("[Warning] {}", msg);
|
warn!("{}", msg);
|
||||||
}
|
}
|
||||||
Event::Error(msg) => {
|
Event::Error(msg) => {
|
||||||
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg);
|
error!("{}", msg);
|
||||||
}
|
}
|
||||||
Event::ErrorNetwork(msg) => {
|
Event::ErrorNetwork(msg) => {
|
||||||
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg);
|
error!("[NETWORK] msg={}", msg);
|
||||||
}
|
}
|
||||||
Event::ErrorSelfNotInGroup(msg) => {
|
Event::ErrorSelfNotInGroup(msg) => {
|
||||||
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg);
|
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||||
}
|
}
|
||||||
Event::MsgsChanged { chat_id, msg_id } => {
|
Event::MsgsChanged { chat_id, msg_id } => {
|
||||||
print!(
|
info!(
|
||||||
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m",
|
"{}",
|
||||||
chat_id, msg_id,
|
yellow.paint(format!(
|
||||||
|
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
|
||||||
|
chat_id, msg_id,
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ContactsChanged(_) => {
|
Event::ContactsChanged(_) => {
|
||||||
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
|
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
||||||
}
|
}
|
||||||
Event::LocationChanged(contact) => {
|
Event::LocationChanged(contact) => {
|
||||||
print!(
|
info!(
|
||||||
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m",
|
"{}",
|
||||||
contact,
|
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ConfigureProgress(progress) => {
|
Event::ConfigureProgress(progress) => {
|
||||||
print!(
|
info!(
|
||||||
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
|
"{}",
|
||||||
progress,
|
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ImexProgress(progress) => {
|
Event::ImexProgress(progress) => {
|
||||||
print!(
|
info!(
|
||||||
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
|
"{}",
|
||||||
progress,
|
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ImexFileWritten(file) => {
|
Event::ImexFileWritten(file) => {
|
||||||
print!(
|
info!(
|
||||||
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
|
"{}",
|
||||||
file.display()
|
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ChatModified(chat) => {
|
Event::ChatModified(chat) => {
|
||||||
print!(
|
info!(
|
||||||
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
|
"{}",
|
||||||
chat
|
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
|
info!("Received {:?}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Threads for waiting for messages and for jobs
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
|
|
||||||
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Handle {
|
|
||||||
handle_imap: Option<std::thread::JoinHandle<()>>,
|
|
||||||
handle_mvbox: Option<std::thread::JoinHandle<()>>,
|
|
||||||
handle_sentbox: Option<std::thread::JoinHandle<()>>,
|
|
||||||
handle_smtp: Option<std::thread::JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! while_running {
|
|
||||||
($code:block) => {
|
|
||||||
if IS_RUNNING.load(Ordering::Relaxed) {
|
|
||||||
$code
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_threads(c: Arc<RwLock<Context>>) {
|
|
||||||
if HANDLE.clone().lock().unwrap().is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Starting threads");
|
|
||||||
IS_RUNNING.store(true, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let ctx = c.clone();
|
|
||||||
let handle_imap = std::thread::spawn(move || loop {
|
|
||||||
while_running!({
|
|
||||||
perform_inbox_jobs(&ctx.read().unwrap());
|
|
||||||
perform_inbox_fetch(&ctx.read().unwrap());
|
|
||||||
while_running!({
|
|
||||||
let context = ctx.read().unwrap();
|
|
||||||
perform_inbox_idle(&context);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let ctx = c.clone();
|
|
||||||
let handle_mvbox = std::thread::spawn(move || loop {
|
|
||||||
while_running!({
|
|
||||||
perform_mvbox_fetch(&ctx.read().unwrap());
|
|
||||||
while_running!({
|
|
||||||
perform_mvbox_idle(&ctx.read().unwrap());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let ctx = c.clone();
|
|
||||||
let handle_sentbox = std::thread::spawn(move || loop {
|
|
||||||
while_running!({
|
|
||||||
perform_sentbox_fetch(&ctx.read().unwrap());
|
|
||||||
while_running!({
|
|
||||||
perform_sentbox_idle(&ctx.read().unwrap());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let ctx = c;
|
|
||||||
let handle_smtp = std::thread::spawn(move || loop {
|
|
||||||
while_running!({
|
|
||||||
perform_smtp_jobs(&ctx.read().unwrap());
|
|
||||||
while_running!({
|
|
||||||
perform_smtp_idle(&ctx.read().unwrap());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
*HANDLE.clone().lock().unwrap() = Some(Handle {
|
|
||||||
handle_imap: Some(handle_imap),
|
|
||||||
handle_mvbox: Some(handle_mvbox),
|
|
||||||
handle_sentbox: Some(handle_sentbox),
|
|
||||||
handle_smtp: Some(handle_smtp),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_threads(context: &Context) {
|
|
||||||
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
|
|
||||||
println!("Stopping threads");
|
|
||||||
IS_RUNNING.store(false, Ordering::Relaxed);
|
|
||||||
|
|
||||||
interrupt_inbox_idle(context, true);
|
|
||||||
interrupt_mvbox_idle(context);
|
|
||||||
interrupt_sentbox_idle(context);
|
|
||||||
interrupt_smtp_idle(context);
|
|
||||||
|
|
||||||
handle.handle_imap.take().unwrap().join().unwrap();
|
|
||||||
handle.handle_mvbox.take().unwrap().join().unwrap();
|
|
||||||
handle.handle_sentbox.take().unwrap().join().unwrap();
|
|
||||||
handle.handle_smtp.take().unwrap().join().unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === The main loop
|
// === The main loop
|
||||||
@@ -250,10 +146,8 @@ const IMEX_COMMANDS: [&str; 12] = [
|
|||||||
"stop",
|
"stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DB_COMMANDS: [&str; 11] = [
|
const DB_COMMANDS: [&str; 9] = [
|
||||||
"info",
|
"info",
|
||||||
"open",
|
|
||||||
"close",
|
|
||||||
"set",
|
"set",
|
||||||
"get",
|
"get",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
@@ -264,7 +158,7 @@ const DB_COMMANDS: [&str; 11] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 24] = [
|
const CHAT_COMMANDS: [&str; 26] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"chat",
|
"chat",
|
||||||
@@ -288,6 +182,8 @@ const CHAT_COMMANDS: [&str; 24] = [
|
|||||||
"listmedia",
|
"listmedia",
|
||||||
"archive",
|
"archive",
|
||||||
"unarchive",
|
"unarchive",
|
||||||
|
"pin",
|
||||||
|
"unpin",
|
||||||
"delchat",
|
"delchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||||
@@ -308,8 +204,17 @@ const CONTACT_COMMANDS: [&str; 6] = [
|
|||||||
"delcontact",
|
"delcontact",
|
||||||
"cleanupcontacts",
|
"cleanupcontacts",
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 9] = [
|
const MISC_COMMANDS: [&str; 10] = [
|
||||||
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
"getqr",
|
||||||
|
"getbadqr",
|
||||||
|
"checkqr",
|
||||||
|
"event",
|
||||||
|
"fileinfo",
|
||||||
|
"clear",
|
||||||
|
"exit",
|
||||||
|
"quit",
|
||||||
|
"help",
|
||||||
|
"estimatedeletion",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
@@ -361,69 +266,81 @@ impl Highlighter for DcHelper {
|
|||||||
|
|
||||||
impl Helper for DcHelper {}
|
impl Helper for DcHelper {}
|
||||||
|
|
||||||
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
println!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
return Err(format_err!("No db-name specified"));
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = Context::new(
|
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
|
||||||
Box::new(receive_event),
|
|
||||||
"CLI".into(),
|
let events = context.get_event_emitter();
|
||||||
Path::new(&args[1]).to_path_buf(),
|
async_std::task::spawn(async move {
|
||||||
)?;
|
while let Some(event) = events.recv().await {
|
||||||
|
receive_event(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
println!("Delta Chat Core is awaiting your commands.");
|
println!("Delta Chat Core is awaiting your commands.");
|
||||||
|
|
||||||
let ctx = Arc::new(RwLock::new(context));
|
|
||||||
|
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
.history_ignore_space(true)
|
.history_ignore_space(true)
|
||||||
.completion_type(CompletionType::List)
|
.completion_type(CompletionType::List)
|
||||||
.edit_mode(EditMode::Emacs)
|
.edit_mode(EditMode::Emacs)
|
||||||
.output_stream(OutputStreamType::Stdout)
|
.output_stream(OutputStreamType::Stdout)
|
||||||
.build();
|
.build();
|
||||||
let h = DcHelper {
|
let mut selected_chat = ChatId::default();
|
||||||
completer: FilenameCompleter::new(),
|
let (reader_s, reader_r) = async_std::sync::channel(100);
|
||||||
highlighter: MatchingBracketHighlighter::new(),
|
let input_loop = async_std::task::spawn_blocking(move || {
|
||||||
hinter: HistoryHinter {},
|
let h = DcHelper {
|
||||||
};
|
completer: FilenameCompleter::new(),
|
||||||
let mut rl = Editor::with_config(config);
|
highlighter: MatchingBracketHighlighter::new(),
|
||||||
rl.set_helper(Some(h));
|
hinter: HistoryHinter {},
|
||||||
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
};
|
||||||
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
let mut rl = Editor::with_config(config);
|
||||||
if rl.load_history(".dc-history.txt").is_err() {
|
rl.set_helper(Some(h));
|
||||||
println!("No previous history.");
|
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
||||||
}
|
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
||||||
|
if rl.load_history(".dc-history.txt").is_err() {
|
||||||
|
println!("No previous history.");
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let p = "> ";
|
let p = "> ";
|
||||||
let readline = rl.readline(&p);
|
let readline = rl.readline(&p);
|
||||||
match readline {
|
|
||||||
Ok(line) => {
|
match readline {
|
||||||
// TODO: ignore "set mail_pw"
|
Ok(line) => {
|
||||||
rl.add_history_entry(line.as_str());
|
// TODO: ignore "set mail_pw"
|
||||||
let ctx = ctx.clone();
|
rl.add_history_entry(line.as_str());
|
||||||
match handle_cmd(line.trim(), ctx) {
|
async_std::task::block_on(reader_s.send(line));
|
||||||
Ok(ExitResult::Continue) => {}
|
}
|
||||||
Ok(ExitResult::Exit) => break,
|
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||||
Err(err) => println!("Error: {}", err),
|
println!("Exiting...");
|
||||||
|
drop(reader_s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Error: {}", err);
|
||||||
|
drop(reader_s);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
}
|
||||||
println!("Exiting...");
|
|
||||||
break;
|
rl.save_history(".dc-history.txt")?;
|
||||||
}
|
println!("history saved");
|
||||||
Err(err) => {
|
Ok::<_, Error>(())
|
||||||
println!("Error: {}", err);
|
});
|
||||||
break;
|
|
||||||
}
|
while let Ok(line) = reader_r.recv().await {
|
||||||
|
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
|
||||||
|
Ok(ExitResult::Continue) => {}
|
||||||
|
Ok(ExitResult::Exit) => break,
|
||||||
|
Err(err) => println!("Error: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rl.save_history(".dc-history.txt")?;
|
context.stop_io().await;
|
||||||
println!("history saved");
|
input_loop.await?;
|
||||||
{
|
|
||||||
stop_threads(&ctx.read().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -434,43 +351,29 @@ enum ExitResult {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
|
async fn handle_cmd(
|
||||||
|
line: &str,
|
||||||
|
ctx: Context,
|
||||||
|
selected_chat: &mut ChatId,
|
||||||
|
) -> Result<ExitResult, Error> {
|
||||||
let mut args = line.splitn(2, ' ');
|
let mut args = line.splitn(2, ' ');
|
||||||
let arg0 = args.next().unwrap_or_default();
|
let arg0 = args.next().unwrap_or_default();
|
||||||
let arg1 = args.next().unwrap_or_default();
|
let arg1 = args.next().unwrap_or_default();
|
||||||
|
|
||||||
match arg0 {
|
match arg0 {
|
||||||
"connect" => {
|
"connect" => {
|
||||||
start_threads(ctx);
|
ctx.start_io().await;
|
||||||
}
|
}
|
||||||
"disconnect" => {
|
"disconnect" => {
|
||||||
stop_threads(&ctx.read().unwrap());
|
ctx.stop_io().await;
|
||||||
}
|
|
||||||
"smtp-jobs" => {
|
|
||||||
if HANDLE.clone().lock().unwrap().is_some() {
|
|
||||||
println!("smtp-jobs are already running in a thread.",);
|
|
||||||
} else {
|
|
||||||
perform_smtp_jobs(&ctx.read().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"imap-jobs" => {
|
|
||||||
if HANDLE.clone().lock().unwrap().is_some() {
|
|
||||||
println!("inbox-jobs are already running in a thread.");
|
|
||||||
} else {
|
|
||||||
perform_inbox_jobs(&ctx.read().unwrap());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"configure" => {
|
"configure" => {
|
||||||
start_threads(ctx.clone());
|
ctx.configure().await?;
|
||||||
configure(&ctx.read().unwrap());
|
|
||||||
}
|
}
|
||||||
"oauth2" => {
|
"oauth2" => {
|
||||||
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
|
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
|
||||||
let oauth2_url = dc_get_oauth2_url(
|
let oauth2_url =
|
||||||
&ctx.read().unwrap(),
|
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
|
||||||
&addr,
|
|
||||||
"chat.delta:/com.b44t.messenger",
|
|
||||||
);
|
|
||||||
if oauth2_url.is_none() {
|
if oauth2_url.is_none() {
|
||||||
println!("OAuth2 not available for {}.", &addr);
|
println!("OAuth2 not available for {}.", &addr);
|
||||||
} else {
|
} else {
|
||||||
@@ -485,9 +388,9 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
|
|||||||
print!("\x1b[1;1H\x1b[2J");
|
print!("\x1b[1;1H\x1b[2J");
|
||||||
}
|
}
|
||||||
"getqr" | "getbadqr" => {
|
"getqr" | "getbadqr" => {
|
||||||
start_threads(ctx.clone());
|
ctx.start_io().await;
|
||||||
if let Some(mut qr) =
|
if let Some(mut qr) =
|
||||||
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default())
|
dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await
|
||||||
{
|
{
|
||||||
if !qr.is_empty() {
|
if !qr.is_empty() {
|
||||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||||
@@ -504,23 +407,23 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"joinqr" => {
|
"joinqr" => {
|
||||||
start_threads(ctx.clone());
|
ctx.start_io().await;
|
||||||
if !arg0.is_empty() {
|
if !arg0.is_empty() {
|
||||||
dc_join_securejoin(&ctx.read().unwrap(), arg1);
|
dc_join_securejoin(&ctx, arg1).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||||
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
|
_ => cmdline(ctx.clone(), line, selected_chat).await?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ExitResult::Continue)
|
Ok(ExitResult::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() -> Result<(), failure::Error> {
|
fn main() -> Result<(), Error> {
|
||||||
let _ = pretty_env_logger::try_init();
|
let _ = pretty_env_logger::try_init();
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args = std::env::args().collect();
|
||||||
main_0(args)?;
|
async_std::task::block_on(async move { start(args).await })?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,100 @@
|
|||||||
extern crate deltachat;
|
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::{thread, time};
|
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
use deltachat::chat;
|
use deltachat::chat;
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::configure::*;
|
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
use deltachat::job::{
|
use deltachat::message::Message;
|
||||||
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
|
|
||||||
perform_smtp_jobs,
|
|
||||||
};
|
|
||||||
use deltachat::Event;
|
use deltachat::Event;
|
||||||
|
|
||||||
fn cb(_ctx: &Context, event: Event) -> usize {
|
fn cb(event: Event) {
|
||||||
print!("[{:?}]", event);
|
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::ConfigureProgress(progress) => {
|
Event::ConfigureProgress(progress) => {
|
||||||
print!(" progress: {}\n", progress);
|
log::info!("progress: {}", progress);
|
||||||
0
|
|
||||||
}
|
}
|
||||||
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
Event::Info(msg) => {
|
||||||
print!(" {}\n", msg);
|
log::info!("{}", msg);
|
||||||
0
|
|
||||||
}
|
}
|
||||||
_ => {
|
Event::Warning(msg) => {
|
||||||
print!("\n");
|
log::warn!("{}", msg);
|
||||||
0
|
}
|
||||||
|
Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
||||||
|
log::error!("{}", msg);
|
||||||
|
}
|
||||||
|
event => {
|
||||||
|
log::info!("{:?}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
pretty_env_logger::try_init_timed().ok();
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
println!("creating database {:?}", dbfile);
|
log::info!("creating database {:?}", dbfile);
|
||||||
let ctx =
|
let ctx = Context::new("FakeOs".into(), dbfile.into())
|
||||||
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
|
.await
|
||||||
let running = Arc::new(RwLock::new(true));
|
.expect("Failed to create context");
|
||||||
let info = ctx.get_info();
|
let info = ctx.get_info().await;
|
||||||
let duration = time::Duration::from_millis(4000);
|
log::info!("info: {:#?}", info);
|
||||||
println!("info: {:#?}", info);
|
|
||||||
|
|
||||||
let ctx = Arc::new(ctx);
|
let events = ctx.get_event_emitter();
|
||||||
let ctx1 = ctx.clone();
|
let events_spawn = async_std::task::spawn(async move {
|
||||||
let r1 = running.clone();
|
while let Some(event) = events.recv().await {
|
||||||
let t1 = thread::spawn(move || {
|
cb(event);
|
||||||
while *r1.read().unwrap() {
|
|
||||||
perform_inbox_jobs(&ctx1);
|
|
||||||
if *r1.read().unwrap() {
|
|
||||||
perform_inbox_fetch(&ctx1);
|
|
||||||
|
|
||||||
if *r1.read().unwrap() {
|
|
||||||
perform_inbox_idle(&ctx1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let ctx1 = ctx.clone();
|
log::info!("configuring");
|
||||||
let r1 = running.clone();
|
|
||||||
let t2 = thread::spawn(move || {
|
|
||||||
while *r1.read().unwrap() {
|
|
||||||
perform_smtp_jobs(&ctx1);
|
|
||||||
if *r1.read().unwrap() {
|
|
||||||
perform_smtp_idle(&ctx1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("configuring");
|
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
let args = std::env::args().collect::<Vec<String>>();
|
||||||
assert_eq!(args.len(), 2, "missing password");
|
assert_eq!(args.len(), 3, "requires email password");
|
||||||
let pw = args[1].clone();
|
let email = args[1].clone();
|
||||||
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
|
let pw = args[2].clone();
|
||||||
|
ctx.set_config(config::Config::Addr, Some(&email))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
ctx.set_config(config::Config::MailPw, Some(&pw))
|
||||||
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
|
|
||||||
configure(&ctx);
|
|
||||||
|
|
||||||
thread::sleep(duration);
|
ctx.configure().await.unwrap();
|
||||||
|
|
||||||
println!("sending a message");
|
log::info!("------ RUN ------");
|
||||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
|
ctx.start_io().await;
|
||||||
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
|
log::info!("--- SENDING A MESSAGE ---");
|
||||||
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
|
|
||||||
|
|
||||||
println!("fetching chats..");
|
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
||||||
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
|
||||||
|
|
||||||
for i in 0..chats.len() {
|
for i in 0..1 {
|
||||||
let summary = chats.get_summary(&ctx, 0, None);
|
log::info!("sending message {}", i);
|
||||||
let text1 = summary.get_text1();
|
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {}nth message!", i))
|
||||||
let text2 = summary.get_text2();
|
.await
|
||||||
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::sleep(duration);
|
// wait for the message to be sent out
|
||||||
|
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
println!("stopping threads");
|
log::info!("fetching chats..");
|
||||||
|
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
||||||
|
|
||||||
*running.write().unwrap() = false;
|
for i in 0..chats.len() {
|
||||||
deltachat::job::interrupt_inbox_idle(&ctx, true);
|
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
|
||||||
deltachat::job::interrupt_smtp_idle(&ctx);
|
.await
|
||||||
|
.unwrap();
|
||||||
|
log::info!("[{}] msg: {:?}", i, msg);
|
||||||
|
}
|
||||||
|
|
||||||
println!("joining");
|
log::info!("stopping");
|
||||||
t1.join().unwrap();
|
ctx.stop_io().await;
|
||||||
t2.join().unwrap();
|
log::info!("closing");
|
||||||
|
drop(ctx);
|
||||||
println!("closing");
|
events_spawn.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
0.600.1
|
0.900.0 (DRAFT)
|
||||||
|
---------------
|
||||||
|
|
||||||
|
- refactored internals to use plugin-approach
|
||||||
|
|
||||||
|
- introduced PerAccount and Global hooks that plugins can implement
|
||||||
|
|
||||||
|
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
|
||||||
|
|
||||||
|
- introduced two documented examples for an echo and a group-membership
|
||||||
|
tracking plugin.
|
||||||
|
|
||||||
|
0.800.0
|
||||||
|
-------
|
||||||
|
|
||||||
|
- use latest core 1.25.0
|
||||||
|
|
||||||
|
- refine tests and some internal changes to core bindings
|
||||||
|
|
||||||
|
0.700.0
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
- lots of new Python APIs
|
||||||
|
|
||||||
|
- use rust core-beta23
|
||||||
|
|
||||||
- introduce automatic versioning via setuptools_scm,
|
- introduce automatic versioning via setuptools_scm,
|
||||||
based on py-X.Y.Z tags
|
based on py-X.Y.Z tags
|
||||||
|
|
||||||
|
|||||||
@@ -3,41 +3,98 @@ deltachat python bindings
|
|||||||
=========================
|
=========================
|
||||||
|
|
||||||
This package provides bindings to the deltachat-core_ Rust -library
|
This package provides bindings to the deltachat-core_ Rust -library
|
||||||
which provides imap/smtp/crypto handling as well as chat/group/messages
|
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
||||||
handling to Android, Desktop and IO user interfaces.
|
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||||
|
|
||||||
Installing pre-built packages (linux-only)
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
If you have a linux system you may install the ``deltachat`` binary "wheel" package
|
Installing bindings from source (Updated: 20-Jan-2020)
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
Install Rust and Cargo first. Deltachat needs a specific nightly
|
||||||
|
version, the easiest is probably to first install Rust stable from
|
||||||
|
rustup and then use this to install the correct nightly version.
|
||||||
|
|
||||||
|
Bootstrap Rust and Cargo by using rustup::
|
||||||
|
|
||||||
|
curl https://sh.rustup.rs -sSf | sh
|
||||||
|
|
||||||
|
Then GIT clone the deltachat-core-rust repo and get the actual
|
||||||
|
rust- and cargo-toolchain needed by deltachat::
|
||||||
|
|
||||||
|
git clone https://github.com/deltachat/deltachat-core-rust
|
||||||
|
cd deltachat-core-rust
|
||||||
|
rustup show
|
||||||
|
|
||||||
|
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||||
|
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||||
|
python3-venv` should give you a usable python installation.
|
||||||
|
|
||||||
|
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||||
|
virtual environment and activate it in your shell::
|
||||||
|
|
||||||
|
cd python
|
||||||
|
python3 -m venv venv # or: virtualenv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
You should now be able to build the python bindings using the supplied script::
|
||||||
|
|
||||||
|
./install_python_bindings.py
|
||||||
|
|
||||||
|
The installation might take a while, depending on your machine.
|
||||||
|
The bindings will be installed in release mode but with debug symbols.
|
||||||
|
The release mode is currently necessary because some tests generate RSA keys
|
||||||
|
which is prohibitively slow in non-release mode.
|
||||||
|
|
||||||
|
After successful binding installation you can install a few more
|
||||||
|
Python packages before running the tests::
|
||||||
|
|
||||||
|
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
|
||||||
|
pytest -v tests
|
||||||
|
|
||||||
|
|
||||||
|
running "live" tests with temporary accounts
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
If you want to run "liveconfig" functional tests you can set
|
||||||
|
``DCC_NEW_TMP_EMAIL`` to:
|
||||||
|
|
||||||
|
- a particular https-url that you can ask for from the delta
|
||||||
|
chat devs. This is implemented on the server side via
|
||||||
|
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
|
||||||
|
|
||||||
|
- or the path of a file that contains two lines, each describing
|
||||||
|
via "addr=... mail_pw=..." a test account login that will
|
||||||
|
be used for the live tests.
|
||||||
|
|
||||||
|
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
|
||||||
|
e-mail accounts and run through all functional "liveconfig" tests.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
without any "build-from-source" steps.
|
||||||
|
|
||||||
1. `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||||
then create a fresh python environment and activate it in your shell::
|
then create a fresh Python virtual environment and activate it in your shell::
|
||||||
|
|
||||||
virtualenv venv # or: python -m venv
|
virtualenv venv # or: python -m venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
Afterwards, invoking ``python`` or ``pip install`` will only
|
Afterwards, invoking ``python`` or ``pip install`` only
|
||||||
modify files in your ``venv`` directory and leave your system installation
|
modifies files in your ``venv`` directory and leaves
|
||||||
alone.
|
your system installation alone.
|
||||||
|
|
||||||
2. Install the wheel for linux::
|
|
||||||
|
|
||||||
pip install deltachat
|
|
||||||
|
|
||||||
Verify it worked by typing::
|
|
||||||
|
|
||||||
python -c "import deltachat"
|
|
||||||
|
|
||||||
|
|
||||||
Installing a wheel from a PR/branch
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
For Linux, we automatically build wheels for all github PR branches
|
For Linux, we automatically build wheels for all github PR branches
|
||||||
and push them to a python package index. To install the latest github ``master`` branch::
|
and push them to a python package index. To install the latest
|
||||||
|
github ``master`` branch::
|
||||||
|
|
||||||
pip install -i https://m.devpi.net/dc/master deltachat
|
pip install --pre -i https://m.devpi.net/dc/master deltachat
|
||||||
|
|
||||||
|
To verify it worked::
|
||||||
|
|
||||||
|
python -c "import deltachat"
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -46,65 +103,6 @@ and push them to a python package index. To install the latest github ``master``
|
|||||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||||
|
|
||||||
|
|
||||||
Installing bindings from source
|
|
||||||
===============================
|
|
||||||
|
|
||||||
If you can't use "binary" method above then you need to compile
|
|
||||||
to core deltachat library::
|
|
||||||
|
|
||||||
git clone https://github.com/deltachat/deltachat-core-rust
|
|
||||||
cd deltachat-core-rust
|
|
||||||
cd python
|
|
||||||
|
|
||||||
If you don't have one active, create and activate a python "virtualenv":
|
|
||||||
|
|
||||||
python virtualenv venv # or python -m venv
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
Afterwards ``which python`` tells you that it comes out of the "venv"
|
|
||||||
directory that contains all python install artifacts. Let's first
|
|
||||||
install test tools::
|
|
||||||
|
|
||||||
pip install pytest pytest-timeout pytest-rerunfailures requests
|
|
||||||
|
|
||||||
then cargo-build and install the deltachat bindings::
|
|
||||||
|
|
||||||
python install_python_bindings.py
|
|
||||||
|
|
||||||
The bindings will be installed in release mode but with debug symbols.
|
|
||||||
The release mode is necessary because some tests generate RSA keys
|
|
||||||
which is prohibitively slow in debug mode.
|
|
||||||
|
|
||||||
After successful binding installation you can finally run the tests::
|
|
||||||
|
|
||||||
pytest -v tests
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Some tests are sometimes failing/hanging because of
|
|
||||||
https://github.com/deltachat/deltachat-core-rust/issues/331
|
|
||||||
and
|
|
||||||
https://github.com/deltachat/deltachat-core-rust/issues/326
|
|
||||||
|
|
||||||
|
|
||||||
running "live" tests (experimental)
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
If you want to run "liveconfig" functional tests you can set
|
|
||||||
``DCC_PY_LIVECONFIG`` to:
|
|
||||||
|
|
||||||
- a particular https-url that you can ask for from the delta
|
|
||||||
chat devs.
|
|
||||||
|
|
||||||
- or the path of a file that contains two lines, each describing
|
|
||||||
via "addr=... mail_pw=..." a test account login that will
|
|
||||||
be used for the live tests.
|
|
||||||
|
|
||||||
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
|
|
||||||
e-mail accounts and run through all functional "liveconfig" tests.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Code examples
|
Code examples
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -115,15 +113,11 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
|
|||||||
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||||
|
|
||||||
|
|
||||||
Building manylinux1 wheels
|
Building manylinux based wheels
|
||||||
==========================
|
====================================
|
||||||
|
|
||||||
.. note::
|
Building portable manylinux wheels which come with libdeltachat.so
|
||||||
|
can be done with docker-tooling.
|
||||||
This section may not fully work.
|
|
||||||
|
|
||||||
Building portable manylinux1 wheels which come with libdeltachat.so
|
|
||||||
and all it's dependencies is easy using the provided docker tooling.
|
|
||||||
|
|
||||||
using docker pull / premade images
|
using docker pull / premade images
|
||||||
------------------------------------
|
------------------------------------
|
||||||
@@ -136,9 +130,9 @@ organization::
|
|||||||
|
|
||||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||||
|
|
||||||
$ bash ci_scripts/ci_run.sh
|
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||||
|
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||||
This command runs tests and build-wheel scripts in a docker container.
|
deltachat/coredeps ci_scripts/run_all.sh
|
||||||
|
|
||||||
|
|
||||||
Optionally build your own docker image
|
Optionally build your own docker image
|
||||||
|
|||||||
4
python/doc/_static/custom.css
vendored
4
python/doc/_static/custom.css
vendored
@@ -15,3 +15,7 @@ div.globaltoc {
|
|||||||
img.logo {
|
img.logo {
|
||||||
height: 120px;
|
height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
high level API reference
|
high level API reference
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This API is work in progress and may change in versions prior to 1.0.
|
|
||||||
|
|
||||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||||
other classes)
|
other classes)
|
||||||
- :class:`deltachat.contact.Contact`
|
- :class:`deltachat.contact.Contact`
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
C deltachat interface
|
|
||||||
=====================
|
|
||||||
|
|
||||||
See :doc:`lapi` for accessing many of the below functions
|
|
||||||
through the ``deltachat.capi.lib`` namespace.
|
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'deltachat'
|
project = u'deltachat'
|
||||||
copyright = u'2018, holger krekel and contributors'
|
copyright = u'2020, holger krekel and contributors'
|
||||||
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
|||||||
@@ -1,37 +1,60 @@
|
|||||||
|
|
||||||
|
|
||||||
examples
|
examples
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
||||||
Playing around on the commandline
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Once you have :doc:`installed deltachat bindings <install>`
|
Once you have :doc:`installed deltachat bindings <install>`
|
||||||
you can start playing from the python interpreter commandline.
|
you need email/password credentials for an IMAP/SMTP account.
|
||||||
For example you can type ``python`` and then::
|
Delta Chat developers and the CI system use a special URL to create
|
||||||
|
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
|
||||||
|
|
||||||
# instantiate and configure deltachat account
|
Receiving a Chat message from the command line
|
||||||
import deltachat
|
----------------------------------------------
|
||||||
ac = deltachat.Account("/tmp/db")
|
|
||||||
|
|
||||||
# start configuration activity and smtp/imap threads
|
Here is a simple bot that:
|
||||||
ac.start_threads()
|
|
||||||
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
|
|
||||||
|
|
||||||
# create a contact and send a message
|
- receives a message and sends back ("echoes") a message
|
||||||
contact = ac.create_contact("someother@email.address")
|
|
||||||
chat = ac.create_chat_by_contact(contact)
|
|
||||||
chat.send_text("hi from the python interpreter command line")
|
|
||||||
|
|
||||||
Checkout our :doc:`api` for the various high-level things you can do
|
- terminates the bot if the message `/quit` is sent
|
||||||
to send/receive messages, create contacts and chats.
|
|
||||||
|
|
||||||
|
.. include:: ../examples/echo_and_quit.py
|
||||||
|
:literal:
|
||||||
|
|
||||||
Looking at a real example
|
With this file in your working directory you can run the bot
|
||||||
|
by specifying a database path, an e-mail address and password of
|
||||||
|
a SMTP-IMAP account::
|
||||||
|
|
||||||
|
$ cd examples
|
||||||
|
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
|
||||||
|
|
||||||
|
While this process is running you can start sending chat messages
|
||||||
|
to `ADDRESS`.
|
||||||
|
|
||||||
|
Track member additions and removals in a group
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
Here is a simple bot that:
|
||||||
|
|
||||||
|
- echoes messages sent to it
|
||||||
|
|
||||||
|
- tracks if configuration completed
|
||||||
|
|
||||||
|
- tracks member additions and removals for all chat groups
|
||||||
|
|
||||||
|
.. include:: ../examples/group_tracking.py
|
||||||
|
:literal:
|
||||||
|
|
||||||
|
With this file in your working directory you can run the bot
|
||||||
|
by specifying a database path, an e-mail address and password of
|
||||||
|
a SMTP-IMAP account::
|
||||||
|
|
||||||
|
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
|
||||||
|
|
||||||
|
When this process is running you can start sending chat messages
|
||||||
|
to `ADDRESS`.
|
||||||
|
|
||||||
|
Writing bots for real
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
||||||
contains a real-life example of Python bindings usage.
|
contains a little framework for writing deltachat bots in Python.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
deltachat python bindings
|
deltachat python bindings
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
The ``deltachat`` Python package provides two bindings for the core Rust-library
|
The ``deltachat`` Python package provides two layers of bindings for the
|
||||||
of the https://delta.chat messaging ecosystem:
|
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||||
|
|
||||||
- :doc:`api` is a high level interface to deltachat-core which aims
|
- :doc:`api` is a high level interface to deltachat-core.
|
||||||
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
|
||||||
|
|
||||||
- :doc:`capi` is a lowlevel CFFI-binding to the previous
|
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
|
||||||
`deltachat-core C-API <https://c.delta.chat>`_ (so far the Rust library
|
|
||||||
replicates exactly the same C-level API).
|
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||||
|
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ getting started
|
|||||||
links
|
links
|
||||||
changelog
|
changelog
|
||||||
api
|
api
|
||||||
capi
|
|
||||||
lapi
|
lapi
|
||||||
|
plugins
|
||||||
|
|
||||||
..
|
..
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
38
python/doc/plugins.rst
Normal file
38
python/doc/plugins.rst
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
Implementing Plugin Hooks
|
||||||
|
==========================
|
||||||
|
|
||||||
|
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
|
||||||
|
for managing global and per-account plugin registration, and performing
|
||||||
|
hook calls. There are two kinds of plugins:
|
||||||
|
|
||||||
|
- Global plugins that are active for all accounts; they can implement
|
||||||
|
hooks at account-creation and account-shutdown time.
|
||||||
|
|
||||||
|
- Account plugins that are only active during the lifetime of a
|
||||||
|
single Account instance.
|
||||||
|
|
||||||
|
|
||||||
|
Registering a plugin
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. autofunction:: deltachat.register_global_plugin
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
.. automethod:: deltachat.account.Account.add_account_plugin
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
|
||||||
|
Per-Account Hook specifications
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.hookspec.PerAccount
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Global Hook specifications
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.hookspec.Global
|
||||||
|
:members:
|
||||||
|
|
||||||
33
python/examples/echo_and_quit.py
Normal file
33
python/examples/echo_and_quit.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
# content of echo_and_quit.py
|
||||||
|
|
||||||
|
from deltachat import account_hookimpl, run_cmdline
|
||||||
|
|
||||||
|
|
||||||
|
class EchoPlugin:
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
print("process_incoming message", message)
|
||||||
|
if message.text.strip() == "/quit":
|
||||||
|
message.account.shutdown()
|
||||||
|
else:
|
||||||
|
# unconditionally accept the chat
|
||||||
|
message.create_chat()
|
||||||
|
addr = message.get_sender_contact().addr
|
||||||
|
if message.is_system_message():
|
||||||
|
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))
|
||||||
|
else:
|
||||||
|
text = message.text
|
||||||
|
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_message_delivered(self, message):
|
||||||
|
print("ac_message_delivered", message)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
52
python/examples/group_tracking.py
Normal file
52
python/examples/group_tracking.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
# content of group_tracking.py
|
||||||
|
|
||||||
|
from deltachat import account_hookimpl, run_cmdline
|
||||||
|
|
||||||
|
|
||||||
|
class GroupTrackingPlugin:
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
print("process_incoming message", message)
|
||||||
|
if message.text.strip() == "/quit":
|
||||||
|
message.account.shutdown()
|
||||||
|
else:
|
||||||
|
# unconditionally accept the chat
|
||||||
|
message.create_chat()
|
||||||
|
addr = message.get_sender_contact().addr
|
||||||
|
text = message.text
|
||||||
|
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_outgoing_message(self, message):
|
||||||
|
print("ac_outgoing_message:", message)
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_configure_completed(self, success):
|
||||||
|
print("ac_configure_completed:", success)
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_chat_modified(self, chat):
|
||||||
|
print("ac_chat_modified:", chat.id, chat.get_name())
|
||||||
|
for member in chat.get_contacts():
|
||||||
|
print("chat member: {}".format(member.addr))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_member_added(self, chat, contact, message):
|
||||||
|
print("ac_member_added {} to chat {} from {}".format(
|
||||||
|
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||||
|
for member in chat.get_contacts():
|
||||||
|
print("chat member: {}".format(member.addr))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_member_removed(self, chat, contact, message):
|
||||||
|
print("ac_member_removed {} from chat {} by {}".format(
|
||||||
|
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
79
python/examples/test_examples.py
Normal file
79
python/examples/test_examples.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
import pytest
|
||||||
|
import py
|
||||||
|
import echo_and_quit
|
||||||
|
import group_tracking
|
||||||
|
from deltachat.events import FFIEventLogger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def datadir():
|
||||||
|
"""The py.path.local object of the test-data/ directory."""
|
||||||
|
for path in reversed(py.path.local(__file__).parts()):
|
||||||
|
datadir = path.join('test-data')
|
||||||
|
if datadir.isdir():
|
||||||
|
return datadir
|
||||||
|
else:
|
||||||
|
pytest.skip('test-data directory not found')
|
||||||
|
|
||||||
|
|
||||||
|
def test_echo_quit_plugin(acfactory, lp):
|
||||||
|
lp.sec("creating one echo_and_quit bot")
|
||||||
|
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||||
|
|
||||||
|
lp.sec("creating a temp account to contact the bot")
|
||||||
|
ac1 = acfactory.get_one_online_account()
|
||||||
|
|
||||||
|
lp.sec("sending a message to the bot")
|
||||||
|
bot_contact = ac1.create_contact(botproc.addr)
|
||||||
|
bot_chat = bot_contact.create_chat()
|
||||||
|
bot_chat.send_text("hello")
|
||||||
|
|
||||||
|
lp.sec("waiting for the reply message from the bot to arrive")
|
||||||
|
reply = ac1._evtracker.wait_next_incoming_message()
|
||||||
|
assert reply.chat == bot_chat
|
||||||
|
assert "hello" in reply.text
|
||||||
|
lp.sec("send quit sequence")
|
||||||
|
bot_chat.send_text("/quit")
|
||||||
|
botproc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_tracking_plugin(acfactory, lp):
|
||||||
|
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||||
|
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
|
||||||
|
|
||||||
|
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
|
||||||
|
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_configure_completed*
|
||||||
|
""")
|
||||||
|
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||||
|
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||||
|
|
||||||
|
lp.sec("creating bot test group with bot")
|
||||||
|
bot_contact = ac1.create_contact(botproc.addr)
|
||||||
|
ch = ac1.create_group_chat("bot test group")
|
||||||
|
ch.add_contact(bot_contact)
|
||||||
|
ch.send_text("hello")
|
||||||
|
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_chat_modified*bot test group*
|
||||||
|
""")
|
||||||
|
|
||||||
|
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||||
|
contact3 = ac1.create_contact(ac2.get_config("addr"))
|
||||||
|
ch.add_contact(contact3)
|
||||||
|
|
||||||
|
reply = ac1._evtracker.wait_next_incoming_message()
|
||||||
|
assert "hello" in reply.text
|
||||||
|
|
||||||
|
lp.sec("now looking at what the bot received")
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_member_added {}*
|
||||||
|
""".format(contact3.addr))
|
||||||
|
|
||||||
|
lp.sec("contact successfully added, now removing")
|
||||||
|
ch.remove_contact(contact3)
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_member_removed {}*
|
||||||
|
""".format(contact3.addr))
|
||||||
7
python/fail_test.py
Normal file
7
python/fail_test.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
from deltachat import capi
|
||||||
|
from deltachat.capi import ffi, lib
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
|
||||||
|
lib.dc_stop_io(ctx)
|
||||||
@@ -11,18 +11,16 @@ import sys
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
target = os.environ.get("DCC_RS_TARGET")
|
target = os.environ.get("DCC_RS_TARGET")
|
||||||
if target is None:
|
if target is None:
|
||||||
os.environ["DCC_RS_TARGET"] = target = "release"
|
os.environ["DCC_RS_TARGET"] = target = "debug"
|
||||||
if "DCC_RS_DEV" not in os.environ:
|
if "DCC_RS_DEV" not in os.environ:
|
||||||
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
os.environ["DCC_RS_DEV"] = dn
|
os.environ["DCC_RS_DEV"] = dn
|
||||||
|
|
||||||
# build the core library in release + debug mode because
|
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||||
# as of Nov 2019 rPGP generates RSA keys which take
|
if target == 'release':
|
||||||
# prohibitively long for non-release installs
|
cmd.append("--release")
|
||||||
os.environ["RUSTFLAGS"] = "-g"
|
print("running:", " ".join(cmd))
|
||||||
subprocess.check_call([
|
subprocess.check_call(cmd)
|
||||||
"cargo", "build", "-p", "deltachat_ffi", "--" + target
|
|
||||||
])
|
|
||||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||||
|
|
||||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
||||||
|
|||||||
@@ -13,14 +13,20 @@ def main():
|
|||||||
"root": "..",
|
"root": "..",
|
||||||
"relative_to": __file__,
|
"relative_to": __file__,
|
||||||
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
|
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
|
||||||
|
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
|
||||||
},
|
},
|
||||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||||
install_requires=['cffi>=1.0.0', 'six'],
|
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient'],
|
||||||
packages=setuptools.find_packages('src'),
|
packages=setuptools.find_packages('src'),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||||
|
entry_points = {
|
||||||
|
'pytest11': [
|
||||||
|
'deltachat.testplugin = deltachat.testplugin',
|
||||||
|
],
|
||||||
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from deltachat import capi, const
|
import sys
|
||||||
from deltachat.capi import ffi
|
|
||||||
from deltachat.account import Account # noqa
|
from . import capi, const, hookspec # noqa
|
||||||
|
from .capi import ffi # noqa
|
||||||
|
from .account import Account # noqa
|
||||||
|
from .message import Message # noqa
|
||||||
|
from .contact import Contact # noqa
|
||||||
|
from .chat import Chat # noqa
|
||||||
|
from .hookspec import account_hookimpl, global_hookimpl # noqa
|
||||||
|
from . import events
|
||||||
|
|
||||||
from pkg_resources import get_distribution, DistributionNotFound
|
from pkg_resources import get_distribution, DistributionNotFound
|
||||||
try:
|
try:
|
||||||
@@ -10,67 +17,72 @@ except DistributionNotFound:
|
|||||||
__version__ = "0.0.0.dev0-unknown"
|
__version__ = "0.0.0.dev0-unknown"
|
||||||
|
|
||||||
|
|
||||||
_DC_CALLBACK_MAP = {}
|
|
||||||
|
|
||||||
|
|
||||||
@capi.ffi.def_extern()
|
|
||||||
def py_dc_callback(ctx, evt, data1, data2):
|
|
||||||
"""The global event handler.
|
|
||||||
|
|
||||||
CFFI only allows us to set one global event handler, so this one
|
|
||||||
looks up the correct event handler for the given context.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0)
|
|
||||||
except AttributeError:
|
|
||||||
# we are in a deep in GC-free/interpreter shutdown land
|
|
||||||
# nothing much better to do here than:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# the following code relates to the deltachat/_build.py's helper
|
|
||||||
# function which provides us signature info of an event call
|
|
||||||
evt_name = get_dc_event_name(evt)
|
|
||||||
event_sig_types = capi.lib.dc_get_event_signature_types(evt)
|
|
||||||
if data1 and event_sig_types & 1:
|
|
||||||
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
|
|
||||||
if data2 and event_sig_types & 2:
|
|
||||||
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
|
|
||||||
try:
|
|
||||||
if isinstance(data2, bytes):
|
|
||||||
data2 = data2.decode("utf8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
# XXX ignoring the decode error is not quite correct but for now
|
|
||||||
# i don't want to hunt down encoding problems in the c lib
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
ret = callback(ctx, evt_name, data1, data2)
|
|
||||||
if ret is None:
|
|
||||||
ret = 0
|
|
||||||
assert isinstance(ret, int), repr(ret)
|
|
||||||
if event_sig_types & 4:
|
|
||||||
return ffi.cast('uintptr_t', ret)
|
|
||||||
elif event_sig_types & 8:
|
|
||||||
return ffi.cast('int', ret)
|
|
||||||
except: # noqa
|
|
||||||
raise
|
|
||||||
ret = 0
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def set_context_callback(dc_context, func):
|
|
||||||
_DC_CALLBACK_MAP[dc_context] = func
|
|
||||||
|
|
||||||
|
|
||||||
def clear_context_callback(dc_context):
|
|
||||||
try:
|
|
||||||
_DC_CALLBACK_MAP.pop(dc_context, None)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||||
if not _DC_EVENTNAME_MAP:
|
if not _DC_EVENTNAME_MAP:
|
||||||
for name, val in vars(const).items():
|
for name, val in vars(const).items():
|
||||||
if name.startswith("DC_EVENT_"):
|
if name.startswith("DC_EVENT_"):
|
||||||
_DC_EVENTNAME_MAP[val] = name
|
_DC_EVENTNAME_MAP[val] = name
|
||||||
return _DC_EVENTNAME_MAP[integer]
|
return _DC_EVENTNAME_MAP[integer]
|
||||||
|
|
||||||
|
|
||||||
|
def register_global_plugin(plugin):
|
||||||
|
""" Register a global plugin which implements one or more
|
||||||
|
of the :class:`deltachat.hookspec.Global` hooks.
|
||||||
|
"""
|
||||||
|
gm = hookspec.Global._get_plugin_manager()
|
||||||
|
gm.register(plugin)
|
||||||
|
gm.check_pending()
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_global_plugin(plugin):
|
||||||
|
gm = hookspec.Global._get_plugin_manager()
|
||||||
|
gm.unregister(plugin)
|
||||||
|
|
||||||
|
|
||||||
|
register_global_plugin(events)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmdline(argv=None, account_plugins=None):
|
||||||
|
""" Run a simple default command line app, registering the specified
|
||||||
|
account plugins. """
|
||||||
|
import argparse
|
||||||
|
if argv is None:
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
|
||||||
|
parser.add_argument("db", action="store", help="database file")
|
||||||
|
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
|
||||||
|
parser.add_argument("--email", action="store", help="email address")
|
||||||
|
parser.add_argument("--password", action="store", help="password")
|
||||||
|
|
||||||
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
|
ac = Account(args.db)
|
||||||
|
|
||||||
|
if args.show_ffi:
|
||||||
|
ac.set_config("displayname", "bot")
|
||||||
|
log = events.FFIEventLogger(ac)
|
||||||
|
ac.add_account_plugin(log)
|
||||||
|
|
||||||
|
for plugin in account_plugins or []:
|
||||||
|
print("adding plugin", plugin)
|
||||||
|
ac.add_account_plugin(plugin)
|
||||||
|
|
||||||
|
if not ac.is_configured():
|
||||||
|
assert args.email and args.password, (
|
||||||
|
"you must specify --email and --password once to configure this database/account"
|
||||||
|
)
|
||||||
|
ac.set_config("addr", args.email)
|
||||||
|
ac.set_config("mail_pw", args.password)
|
||||||
|
ac.set_config("mvbox_move", "0")
|
||||||
|
ac.set_config("mvbox_watch", "0")
|
||||||
|
ac.set_config("sentbox_watch", "0")
|
||||||
|
configtracker = ac.configure()
|
||||||
|
configtracker.wait_finish()
|
||||||
|
|
||||||
|
# start IO threads and configure if neccessary
|
||||||
|
ac.start_io()
|
||||||
|
|
||||||
|
print("{}: waiting for message".format(ac.get_config("addr")))
|
||||||
|
|
||||||
|
ac.wait_shutdown()
|
||||||
|
|||||||
@@ -1,79 +1,64 @@
|
|||||||
import distutils.ccompiler
|
import distutils.ccompiler
|
||||||
import distutils.log
|
import distutils.log
|
||||||
import distutils.sysconfig
|
import distutils.sysconfig
|
||||||
import tempfile
|
|
||||||
import platform
|
|
||||||
import os
|
import os
|
||||||
import cffi
|
import platform
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from os.path import dirname as dn
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
import types
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
|
from os.path import dirname as dn
|
||||||
|
|
||||||
|
import cffi
|
||||||
|
|
||||||
|
|
||||||
def ffibuilder():
|
def local_build_flags(projdir, target):
|
||||||
projdir = os.environ.get('DCC_RS_DEV')
|
"""Construct build flags for building against a checkout.
|
||||||
if not projdir:
|
|
||||||
p = dn(dn(dn(dn(abspath(__file__)))))
|
:param projdir: The root directory of the deltachat-core-rust project.
|
||||||
projdir = os.environ["DCC_RS_DEV"] = p
|
:param target: The rust build target, `debug` or `release`.
|
||||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
"""
|
||||||
if projdir:
|
flags = types.SimpleNamespace()
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
libs = ['resolv', 'dl']
|
flags.libs = ['resolv', 'dl']
|
||||||
extra_link_args = [
|
flags.extra_link_args = [
|
||||||
'-framework', 'CoreFoundation',
|
'-framework', 'CoreFoundation',
|
||||||
'-framework', 'CoreServices',
|
'-framework', 'CoreServices',
|
||||||
'-framework', 'Security',
|
'-framework', 'Security',
|
||||||
]
|
]
|
||||||
elif platform.system() == 'Linux':
|
elif platform.system() == 'Linux':
|
||||||
libs = ['rt', 'dl', 'm']
|
flags.libs = ['rt', 'dl', 'm']
|
||||||
extra_link_args = []
|
flags.extra_link_args = []
|
||||||
else:
|
|
||||||
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
|
||||||
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
|
||||||
if target_dir is None:
|
|
||||||
target_dir = os.path.join(projdir, 'target')
|
|
||||||
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
|
||||||
assert os.path.exists(objs[0]), objs
|
|
||||||
incs = [os.path.join(projdir, 'deltachat-ffi')]
|
|
||||||
else:
|
else:
|
||||||
libs = ['deltachat']
|
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
||||||
objs = []
|
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
||||||
incs = []
|
if target_dir is None:
|
||||||
extra_link_args = []
|
target_dir = os.path.join(projdir, 'target')
|
||||||
builder = cffi.FFI()
|
flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||||
builder.set_source(
|
assert os.path.exists(flags.objs[0]), flags.objs
|
||||||
'deltachat.capi',
|
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||||
"""
|
return flags
|
||||||
#include <deltachat.h>
|
|
||||||
const char * dupstring_helper(const char* string)
|
|
||||||
{
|
def system_build_flags():
|
||||||
return strdup(string);
|
"""Construct build flags for building against an installed libdeltachat."""
|
||||||
}
|
flags = types.SimpleNamespace()
|
||||||
int dc_get_event_signature_types(int e)
|
flags.libs = ['deltachat']
|
||||||
{
|
flags.objs = []
|
||||||
int result = 0;
|
flags.incs = []
|
||||||
if (DC_EVENT_DATA1_IS_STRING(e))
|
flags.extra_link_args = []
|
||||||
result |= 1;
|
|
||||||
if (DC_EVENT_DATA2_IS_STRING(e))
|
|
||||||
result |= 2;
|
def extract_functions(flags):
|
||||||
if (DC_EVENT_RETURNS_STRING(e))
|
"""Extract the function definitions from deltachat.h.
|
||||||
result |= 4;
|
|
||||||
if (DC_EVENT_RETURNS_INT(e))
|
This creates a .h file with a single `#include <deltachat.h>` line
|
||||||
result |= 8;
|
in it. It then runs the C preprocessor to create an output file
|
||||||
return result;
|
which contains all function definitions found in `deltachat.h`.
|
||||||
}
|
"""
|
||||||
""",
|
|
||||||
include_dirs=incs,
|
|
||||||
libraries=libs,
|
|
||||||
extra_objects=objs,
|
|
||||||
extra_link_args=extra_link_args,
|
|
||||||
)
|
|
||||||
builder.cdef("""
|
|
||||||
typedef int... time_t;
|
|
||||||
void free(void *ptr);
|
|
||||||
extern const char * dupstring_helper(const char* string);
|
|
||||||
extern int dc_get_event_signature_types(int);
|
|
||||||
""")
|
|
||||||
distutils.log.set_verbosity(distutils.log.INFO)
|
distutils.log.set_verbosity(distutils.log.INFO)
|
||||||
cc = distutils.ccompiler.new_compiler(force=True)
|
cc = distutils.ccompiler.new_compiler(force=True)
|
||||||
distutils.sysconfig.customize_compiler(cc)
|
distutils.sysconfig.customize_compiler(cc)
|
||||||
@@ -85,20 +70,133 @@ def ffibuilder():
|
|||||||
src_fp.write('#include <deltachat.h>')
|
src_fp.write('#include <deltachat.h>')
|
||||||
cc.preprocess(source=src_name,
|
cc.preprocess(source=src_name,
|
||||||
output_file=dst_name,
|
output_file=dst_name,
|
||||||
include_dirs=incs,
|
include_dirs=flags.incs,
|
||||||
macros=[('PY_CFFI', '1')])
|
macros=[('PY_CFFI', '1')])
|
||||||
with open(dst_name, "r") as dst_fp:
|
with open(dst_name, "r") as dst_fp:
|
||||||
builder.cdef(dst_fp.read())
|
return dst_fp.read()
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(tmpdir)
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
def find_header(flags):
|
||||||
|
"""Use the compiler to find the deltachat.h header location.
|
||||||
|
|
||||||
|
This uses a small utility in deltachat.h to find the location of
|
||||||
|
the header file location.
|
||||||
|
"""
|
||||||
|
distutils.log.set_verbosity(distutils.log.INFO)
|
||||||
|
cc = distutils.ccompiler.new_compiler(force=True)
|
||||||
|
distutils.sysconfig.customize_compiler(cc)
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
src_name = os.path.join(tmpdir, "where.c")
|
||||||
|
obj_name = os.path.join(tmpdir, "where.o")
|
||||||
|
dst_name = os.path.join(tmpdir, "where")
|
||||||
|
with open(src_name, "w") as src_fp:
|
||||||
|
src_fp.write(textwrap.dedent("""
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <deltachat.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
printf("%s", _dc_header_file_location());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
cwd = os.getcwd()
|
||||||
|
try:
|
||||||
|
os.chdir(tmpdir)
|
||||||
|
cc.compile(sources=["where.c"],
|
||||||
|
include_dirs=flags.incs,
|
||||||
|
macros=[("PY_CFFI_INC", "1")])
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
cc.link_executable(objects=[obj_name],
|
||||||
|
output_progname="where",
|
||||||
|
output_dir=tmpdir)
|
||||||
|
return subprocess.check_output(dst_name)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_defines(flags):
|
||||||
|
"""Extract the required #DEFINEs from deltachat.h.
|
||||||
|
|
||||||
|
Since #DEFINEs are interpreted by the C preprocessor we can not
|
||||||
|
use the compiler to extract these and need to parse the header
|
||||||
|
file ourselves.
|
||||||
|
|
||||||
|
The defines are returned in a string that can be passed to CFFIs
|
||||||
|
cdef() method.
|
||||||
|
"""
|
||||||
|
header = find_header(flags)
|
||||||
|
defines_re = re.compile(r"""
|
||||||
|
\#define\s+ # The start of a define.
|
||||||
|
( # Begin capturing group which captures the define name.
|
||||||
|
(?: # A nested group which is not captured, this allows us
|
||||||
|
# to build the list of prefixes to extract without
|
||||||
|
# creation another capture group.
|
||||||
|
DC_EVENT
|
||||||
|
| DC_QR
|
||||||
|
| DC_MSG
|
||||||
|
| DC_LP
|
||||||
|
| DC_EMPTY
|
||||||
|
| DC_CERTCK
|
||||||
|
| DC_STATE
|
||||||
|
| DC_STR
|
||||||
|
| DC_CONTACT_ID
|
||||||
|
| DC_GCL
|
||||||
|
| DC_CHAT
|
||||||
|
| DC_PROVIDER
|
||||||
|
| DC_KEY_GEN
|
||||||
|
) # End of prefix matching
|
||||||
|
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
||||||
|
) # Close the capturing group, this contains
|
||||||
|
# the entire name e.g. DC_MSG_TEXT.
|
||||||
|
\s+\S+ # Ensure there is whitespace followed by a value.
|
||||||
|
""", re.VERBOSE)
|
||||||
|
defines = []
|
||||||
|
with open(header) as fp:
|
||||||
|
for line in fp:
|
||||||
|
match = defines_re.match(line)
|
||||||
|
if match:
|
||||||
|
defines.append(match.group(1))
|
||||||
|
return '\n'.join('#define {} ...'.format(d) for d in defines)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
flags = local_build_flags(projdir, target)
|
||||||
|
else:
|
||||||
|
flags = system_build_flags()
|
||||||
|
builder = cffi.FFI()
|
||||||
|
builder.set_source(
|
||||||
|
'deltachat.capi',
|
||||||
|
"""
|
||||||
|
#include <deltachat.h>
|
||||||
|
int dc_event_has_string_data(int e)
|
||||||
|
{
|
||||||
|
return DC_EVENT_DATA2_IS_STRING(e);
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
include_dirs=flags.incs,
|
||||||
|
libraries=flags.libs,
|
||||||
|
extra_objects=flags.objs,
|
||||||
|
extra_link_args=flags.extra_link_args,
|
||||||
|
)
|
||||||
builder.cdef("""
|
builder.cdef("""
|
||||||
extern "Python" uintptr_t py_dc_callback(
|
typedef int... time_t;
|
||||||
dc_context_t* context,
|
void free(void *ptr);
|
||||||
int event,
|
extern int dc_event_has_string_data(int);
|
||||||
uintptr_t data1,
|
|
||||||
uintptr_t data2);
|
|
||||||
""")
|
""")
|
||||||
|
function_defs = extract_functions(flags)
|
||||||
|
defines = extract_defines(flags)
|
||||||
|
builder.cdef(function_defs)
|
||||||
|
builder.cdef(defines)
|
||||||
return builder
|
return builder
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
""" Account class implementation. """
|
""" Account class implementation. """
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import atexit
|
from contextlib import contextmanager
|
||||||
import threading
|
from email.utils import parseaddr
|
||||||
|
from threading import Event
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from array import array
|
from array import array
|
||||||
try:
|
|
||||||
from queue import Queue, Empty
|
|
||||||
except ImportError:
|
|
||||||
from Queue import Queue, Empty
|
|
||||||
|
|
||||||
import deltachat
|
|
||||||
from . import const
|
from . import const
|
||||||
from .capi import ffi, lib
|
from .capi import ffi, lib
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
|
from .tracker import ImexTracker, ConfigureTracker
|
||||||
|
from . import hookspec
|
||||||
|
from .events import EventThread
|
||||||
|
|
||||||
|
|
||||||
|
class MissingCredentials(ValueError):
|
||||||
|
""" Account is missing `addr` and `mail_pw` config values. """
|
||||||
|
|
||||||
|
|
||||||
class Account(object):
|
class Account(object):
|
||||||
@@ -26,38 +26,53 @@ class Account(object):
|
|||||||
by the underlying deltachat core library. All public Account methods are
|
by the underlying deltachat core library. All public Account methods are
|
||||||
meant to be memory-safe and return memory-safe objects.
|
meant to be memory-safe and return memory-safe objects.
|
||||||
"""
|
"""
|
||||||
def __init__(self, db_path, logid=None, eventlogging=True, debug=True):
|
MissingCredentials = MissingCredentials
|
||||||
|
|
||||||
|
def __init__(self, db_path, os_name=None, logging=True):
|
||||||
""" initialize account object.
|
""" initialize account object.
|
||||||
|
|
||||||
:param db_path: a path to the account database. The database
|
:param db_path: a path to the account database. The database
|
||||||
will be created if it doesn't exist.
|
will be created if it doesn't exist.
|
||||||
:param logid: an optional logging prefix that should be used with
|
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||||
the default internal logging.
|
|
||||||
:param eventlogging: if False no eventlogging and no context callback will be configured
|
|
||||||
:param debug: turn on debug logging for events.
|
|
||||||
"""
|
"""
|
||||||
self._dc_context = ffi.gc(
|
# initialize per-account plugin system
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
self._pm = hookspec.PerAccount._make_plugin_manager()
|
||||||
_destroy_dc_context,
|
self._logging = logging
|
||||||
)
|
|
||||||
if eventlogging:
|
|
||||||
self._evlogger = EventLogger(self._dc_context, logid, debug)
|
|
||||||
deltachat.set_context_callback(self._dc_context, self._process_event)
|
|
||||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
|
||||||
else:
|
|
||||||
self._threads = IOThreads(self._dc_context)
|
|
||||||
|
|
||||||
|
self.add_account_plugin(self)
|
||||||
|
|
||||||
|
self.db_path = db_path
|
||||||
if hasattr(db_path, "encode"):
|
if hasattr(db_path, "encode"):
|
||||||
db_path = db_path.encode("utf8")
|
db_path = db_path.encode("utf8")
|
||||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
|
||||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
self._dc_context = ffi.gc(
|
||||||
|
lib.dc_context_new(as_dc_charpointer(os_name), db_path, ffi.NULL),
|
||||||
|
lib.dc_context_unref,
|
||||||
|
)
|
||||||
|
if self._dc_context == ffi.NULL:
|
||||||
|
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
|
||||||
|
|
||||||
|
self._shutdown_event = Event()
|
||||||
|
self._event_thread = EventThread(self)
|
||||||
self._configkeys = self.get_config("sys.config_keys").split()
|
self._configkeys = self.get_config("sys.config_keys").split()
|
||||||
self._imex_events = Queue()
|
hook = hookspec.Global._get_plugin_manager().hook
|
||||||
atexit.register(self.shutdown)
|
hook.dc_account_init(account=self)
|
||||||
|
|
||||||
|
def disable_logging(self):
|
||||||
|
""" disable logging. """
|
||||||
|
self._logging = False
|
||||||
|
|
||||||
|
def enable_logging(self):
|
||||||
|
""" re-enable logging. """
|
||||||
|
self._logging = True
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.shutdown()
|
# self.shutdown()
|
||||||
|
|
||||||
|
def log(self, msg):
|
||||||
|
if self._logging:
|
||||||
|
self._pm.hook.ac_log_line(message=msg)
|
||||||
|
|
||||||
def _check_config_key(self, name):
|
def _check_config_key(self, name):
|
||||||
if name not in self._configkeys:
|
if name not in self._configkeys:
|
||||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||||
@@ -117,16 +132,27 @@ class Account(object):
|
|||||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||||
return from_dc_charpointer(res)
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
def configure(self, **kwargs):
|
def _preconfigure_keypair(self, addr, public, secret):
|
||||||
""" set config values and configure this account.
|
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||||
|
|
||||||
|
In other words, you don't need this.
|
||||||
|
"""
|
||||||
|
res = lib.dc_preconfigure_keypair(self._dc_context,
|
||||||
|
as_dc_charpointer(addr),
|
||||||
|
as_dc_charpointer(public),
|
||||||
|
as_dc_charpointer(secret))
|
||||||
|
if res == 0:
|
||||||
|
raise Exception("Failed to set key")
|
||||||
|
|
||||||
|
def update_config(self, kwargs):
|
||||||
|
""" update config values.
|
||||||
|
|
||||||
:param kwargs: name=value config settings for this account.
|
:param kwargs: name=value config settings for this account.
|
||||||
values need to be unicode.
|
values need to be unicode.
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
for name, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.set_config(name, value)
|
self.set_config(key, str(value))
|
||||||
lib.dc_configure(self._dc_context)
|
|
||||||
|
|
||||||
def is_configured(self):
|
def is_configured(self):
|
||||||
""" determine if the account is configured already; an initial connection
|
""" determine if the account is configured already; an initial connection
|
||||||
@@ -134,7 +160,7 @@ class Account(object):
|
|||||||
|
|
||||||
:returns: True if account is configured.
|
:returns: True if account is configured.
|
||||||
"""
|
"""
|
||||||
return lib.dc_is_configured(self._dc_context)
|
return True if lib.dc_is_configured(self._dc_context) else False
|
||||||
|
|
||||||
def set_avatar(self, img_path):
|
def set_avatar(self, img_path):
|
||||||
"""Set self avatar.
|
"""Set self avatar.
|
||||||
@@ -164,11 +190,6 @@ class Account(object):
|
|||||||
raise ValueError("no flags set")
|
raise ValueError("no flags set")
|
||||||
lib.dc_empty_server(self._dc_context, flags)
|
lib.dc_empty_server(self._dc_context, flags)
|
||||||
|
|
||||||
def get_infostring(self):
|
|
||||||
""" return info of the configured account. """
|
|
||||||
self.check_is_configured()
|
|
||||||
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
|
||||||
|
|
||||||
def get_latest_backupfile(self, backupdir):
|
def get_latest_backupfile(self, backupdir):
|
||||||
""" return the latest backup file in a given directory.
|
""" return the latest backup file in a given directory.
|
||||||
"""
|
"""
|
||||||
@@ -190,23 +211,43 @@ class Account(object):
|
|||||||
|
|
||||||
:returns: :class:`deltachat.contact.Contact`
|
:returns: :class:`deltachat.contact.Contact`
|
||||||
"""
|
"""
|
||||||
self.check_is_configured()
|
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||||
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
|
||||||
|
|
||||||
def create_contact(self, email, name=None):
|
def create_contact(self, obj, name=None):
|
||||||
""" create a (new) Contact. If there already is a Contact
|
""" create a (new) Contact or return an existing one.
|
||||||
with that e-mail address, it is unblocked and its name is
|
|
||||||
updated.
|
|
||||||
|
|
||||||
:param email: email-address (text type)
|
Calling this method will always resulut in the same
|
||||||
:param name: display name for this contact (optional)
|
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.
|
||||||
|
|
||||||
|
:param obj: email-address, Account or Contact instance.
|
||||||
|
:param name: (optional) display name for this contact
|
||||||
:returns: :class:`deltachat.contact.Contact` instance.
|
:returns: :class:`deltachat.contact.Contact` instance.
|
||||||
"""
|
"""
|
||||||
|
if isinstance(obj, Account):
|
||||||
|
if not obj.is_configured():
|
||||||
|
raise ValueError("can only add addresses from configured accounts")
|
||||||
|
addr, displayname = obj.get_config("addr"), obj.get_config("displayname")
|
||||||
|
elif isinstance(obj, Contact):
|
||||||
|
if obj.account != self:
|
||||||
|
raise ValueError("account mismatch {}".format(obj))
|
||||||
|
addr, displayname = obj.addr, obj.name
|
||||||
|
elif isinstance(obj, str):
|
||||||
|
displayname, addr = parseaddr(obj)
|
||||||
|
else:
|
||||||
|
raise TypeError("don't know how to create chat for %r" % (obj, ))
|
||||||
|
|
||||||
|
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)
|
name = as_dc_charpointer(name)
|
||||||
email = as_dc_charpointer(email)
|
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||||
contact_id = lib.dc_create_contact(self._dc_context, name, email)
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
|
||||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
return Contact(self, contact_id)
|
||||||
return Contact(self._dc_context, contact_id)
|
|
||||||
|
|
||||||
def delete_contact(self, contact):
|
def delete_contact(self, contact):
|
||||||
""" delete a Contact.
|
""" delete a Contact.
|
||||||
@@ -215,10 +256,25 @@ class Account(object):
|
|||||||
:returns: True if deletion succeeded (contact was deleted)
|
:returns: True if deletion succeeded (contact was deleted)
|
||||||
"""
|
"""
|
||||||
contact_id = contact.id
|
contact_id = contact.id
|
||||||
assert contact._dc_context == self._dc_context
|
assert contact.account == self
|
||||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||||
|
|
||||||
|
def get_contact_by_addr(self, email):
|
||||||
|
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||||
|
_, addr = parseaddr(email)
|
||||||
|
addr = as_dc_charpointer(addr)
|
||||||
|
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||||
|
if contact_id:
|
||||||
|
return self.get_contact_by_id(contact_id)
|
||||||
|
|
||||||
|
def get_contact_by_id(self, contact_id):
|
||||||
|
""" return Contact instance or None.
|
||||||
|
:param contact_id: integer id of this contact.
|
||||||
|
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||||
|
"""
|
||||||
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||||
""" get a (filtered) list of contacts.
|
""" get a (filtered) list of contacts.
|
||||||
|
|
||||||
@@ -238,52 +294,39 @@ class Account(object):
|
|||||||
lib.dc_get_contacts(self._dc_context, flags, query),
|
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
|
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||||
|
|
||||||
def create_chat_by_contact(self, contact):
|
def get_fresh_messages(self):
|
||||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
""" yield all fresh messages from all chats. """
|
||||||
|
dc_array = ffi.gc(
|
||||||
|
lib.dc_get_fresh_msgs(self._dc_context),
|
||||||
|
lib.dc_array_unref
|
||||||
|
)
|
||||||
|
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||||
|
|
||||||
:param contact: chat_id (int) or contact object.
|
def create_chat(self, obj):
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||||
"""
|
return self.create_contact(obj).create_chat()
|
||||||
if hasattr(contact, "id"):
|
|
||||||
if contact._dc_context != self._dc_context:
|
|
||||||
raise ValueError("Contact belongs to a different Account")
|
|
||||||
contact_id = contact.id
|
|
||||||
else:
|
|
||||||
assert isinstance(contact, int)
|
|
||||||
contact_id = contact
|
|
||||||
chat_id = lib.dc_create_chat_by_contact_id(self._dc_context, contact_id)
|
|
||||||
return Chat(self, chat_id)
|
|
||||||
|
|
||||||
def create_chat_by_message(self, message):
|
def _create_chat_by_message_id(self, msg_id):
|
||||||
""" create or get an existing chat object for the
|
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
|
||||||
the specified message.
|
|
||||||
|
|
||||||
:param message: messsage id or message instance.
|
def create_group_chat(self, name, contacts=None, verified=False):
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
|
||||||
"""
|
|
||||||
if hasattr(message, "id"):
|
|
||||||
if self._dc_context != message._dc_context:
|
|
||||||
raise ValueError("Message belongs to a different Account")
|
|
||||||
msg_id = message.id
|
|
||||||
else:
|
|
||||||
assert isinstance(message, int)
|
|
||||||
msg_id = message
|
|
||||||
chat_id = lib.dc_create_chat_by_msg_id(self._dc_context, msg_id)
|
|
||||||
return Chat(self, chat_id)
|
|
||||||
|
|
||||||
def create_group_chat(self, name, verified=False):
|
|
||||||
""" create a new group chat object.
|
""" create a new group chat object.
|
||||||
|
|
||||||
Chats are unpromoted until the first message is sent.
|
Chats are unpromoted until the first message is sent.
|
||||||
|
|
||||||
|
:param contacts: list of contacts to add
|
||||||
:param verified: if true only verified contacts can be added.
|
:param verified: if true only verified contacts can be added.
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
"""
|
"""
|
||||||
bytes_name = name.encode("utf8")
|
bytes_name = name.encode("utf8")
|
||||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
||||||
return Chat(self, chat_id)
|
chat = Chat(self, chat_id)
|
||||||
|
if contacts is not None:
|
||||||
|
for contact in contacts:
|
||||||
|
chat.add_contact(contact)
|
||||||
|
return chat
|
||||||
|
|
||||||
def get_chats(self):
|
def get_chats(self):
|
||||||
""" return list of chats.
|
""" return list of chats.
|
||||||
@@ -356,45 +399,35 @@ class Account(object):
|
|||||||
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||||
|
|
||||||
def export_self_keys(self, path):
|
def export_self_keys(self, path):
|
||||||
""" export public and private keys to the specified directory. """
|
""" export public and private keys to the specified directory.
|
||||||
|
|
||||||
|
Note that the account does not have to be started.
|
||||||
|
"""
|
||||||
return self._export(path, imex_cmd=1)
|
return self._export(path, imex_cmd=1)
|
||||||
|
|
||||||
def export_all(self, path):
|
def export_all(self, path):
|
||||||
"""return new file containing a backup of all database state
|
"""return new file containing a backup of all database state
|
||||||
(chats, contacts, keys, media, ...). The file is created in the
|
(chats, contacts, keys, media, ...). The file is created in the
|
||||||
the `path` directory.
|
the `path` directory.
|
||||||
|
|
||||||
|
Note that the account does not have to be started.
|
||||||
"""
|
"""
|
||||||
export_files = self._export(path, 11)
|
export_files = self._export(path, 11)
|
||||||
if len(export_files) != 1:
|
if len(export_files) != 1:
|
||||||
raise RuntimeError("found more than one new file")
|
raise RuntimeError("found more than one new file")
|
||||||
return export_files[0]
|
return export_files[0]
|
||||||
|
|
||||||
def _imex_events_clear(self):
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
self._imex_events.get_nowait()
|
|
||||||
except Empty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _export(self, path, imex_cmd):
|
def _export(self, path, imex_cmd):
|
||||||
self._imex_events_clear()
|
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
if not self._threads.is_started():
|
return imex_tracker.wait_finish()
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
|
||||||
files_written = []
|
|
||||||
while True:
|
|
||||||
ev = self._imex_events.get()
|
|
||||||
if isinstance(ev, str):
|
|
||||||
files_written.append(ev)
|
|
||||||
elif isinstance(ev, bool):
|
|
||||||
if not ev:
|
|
||||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
|
||||||
return files_written
|
|
||||||
|
|
||||||
def import_self_keys(self, path):
|
def import_self_keys(self, path):
|
||||||
""" Import private keys found in the `path` directory.
|
""" Import private keys found in the `path` directory.
|
||||||
The last imported key is made the default keys unless its name
|
The last imported key is made the default keys unless its name
|
||||||
contains the string legacy. Public keys are not imported.
|
contains the string legacy. Public keys are not imported.
|
||||||
|
|
||||||
|
Note that the account does not have to be started.
|
||||||
"""
|
"""
|
||||||
self._import(path, imex_cmd=2)
|
self._import(path, imex_cmd=2)
|
||||||
|
|
||||||
@@ -407,12 +440,9 @@ class Account(object):
|
|||||||
self._import(path, imex_cmd=12)
|
self._import(path, imex_cmd=12)
|
||||||
|
|
||||||
def _import(self, path, imex_cmd):
|
def _import(self, path, imex_cmd):
|
||||||
self._imex_events_clear()
|
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
if not self._threads.is_started():
|
imex_tracker.wait_finish()
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
|
||||||
if not self._imex_events.get():
|
|
||||||
raise ValueError("import from path '{}' failed".format(path))
|
|
||||||
|
|
||||||
def initiate_key_transfer(self):
|
def initiate_key_transfer(self):
|
||||||
"""return setup code after a Autocrypt setup message
|
"""return setup code after a Autocrypt setup message
|
||||||
@@ -420,8 +450,8 @@ class Account(object):
|
|||||||
If sending out was unsuccessful, a RuntimeError is raised.
|
If sending out was unsuccessful, a RuntimeError is raised.
|
||||||
"""
|
"""
|
||||||
self.check_is_configured()
|
self.check_is_configured()
|
||||||
if not self._threads.is_started():
|
if not self.is_started():
|
||||||
raise RuntimeError("threads not running, can not send out")
|
raise RuntimeError("IO not running, can not send out")
|
||||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
res = lib.dc_initiate_key_transfer(self._dc_context)
|
||||||
if res == ffi.NULL:
|
if res == ffi.NULL:
|
||||||
raise RuntimeError("could not send out autocrypt setup message")
|
raise RuntimeError("could not send out autocrypt setup message")
|
||||||
@@ -477,63 +507,6 @@ class Account(object):
|
|||||||
raise ValueError("could not join group")
|
raise ValueError("could not join group")
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def stop_ongoing(self):
|
|
||||||
lib.dc_stop_ongoing_process(self._dc_context)
|
|
||||||
|
|
||||||
#
|
|
||||||
# meta API for start/stop and event based processing
|
|
||||||
#
|
|
||||||
|
|
||||||
def wait_next_incoming_message(self):
|
|
||||||
""" wait for and return next incoming message. """
|
|
||||||
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
return self.get_message_by_id(ev[2])
|
|
||||||
|
|
||||||
def start_threads(self, mvbox=False, sentbox=False):
|
|
||||||
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
|
||||||
|
|
||||||
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
if not self.is_configured():
|
|
||||||
self.configure()
|
|
||||||
self._threads.start(mvbox=mvbox, sentbox=sentbox)
|
|
||||||
|
|
||||||
def stop_threads(self, wait=True):
|
|
||||||
""" stop IMAP/SMTP threads. """
|
|
||||||
if self._threads.is_started():
|
|
||||||
self.stop_ongoing()
|
|
||||||
self._threads.stop(wait=wait)
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
""" stop threads and close and remove underlying dc_context and callbacks. """
|
|
||||||
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
|
||||||
# print("SHUTDOWN", self)
|
|
||||||
self.stop_threads(wait=False)
|
|
||||||
lib.dc_close(self._dc_context)
|
|
||||||
self.stop_threads(wait=wait) # to wait for threads
|
|
||||||
deltachat.clear_context_callback(self._dc_context)
|
|
||||||
del self._dc_context
|
|
||||||
atexit.unregister(self.shutdown)
|
|
||||||
|
|
||||||
def _process_event(self, ctx, evt_name, data1, data2):
|
|
||||||
assert ctx == self._dc_context
|
|
||||||
if hasattr(self, "_evlogger"):
|
|
||||||
self._evlogger(evt_name, data1, data2)
|
|
||||||
method = getattr(self, "on_" + evt_name.lower(), None)
|
|
||||||
if method is not None:
|
|
||||||
method(data1, data2)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def on_dc_event_imex_progress(self, data1, data2):
|
|
||||||
if data1 == 1000:
|
|
||||||
self._imex_events.put(True)
|
|
||||||
elif data1 == 0:
|
|
||||||
self._imex_events.put(False)
|
|
||||||
|
|
||||||
def on_dc_event_imex_file_written(self, data1, data2):
|
|
||||||
self._imex_events.put(data1)
|
|
||||||
|
|
||||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||||
"""set a new location. It effects all chats where we currently
|
"""set a new location. It effects all chats where we currently
|
||||||
have enabled location streaming.
|
have enabled location streaming.
|
||||||
@@ -548,160 +521,104 @@ class Account(object):
|
|||||||
if dc_res == 0:
|
if dc_res == 0:
|
||||||
raise ValueError("no chat is streaming locations")
|
raise ValueError("no chat is streaming locations")
|
||||||
|
|
||||||
|
#
|
||||||
|
# meta API for start/stop and event based processing
|
||||||
|
#
|
||||||
|
|
||||||
class IOThreads:
|
def add_account_plugin(self, plugin, name=None):
|
||||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
""" add an account plugin which implements one or more of
|
||||||
self._dc_context = dc_context
|
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||||
self._thread_quitflag = False
|
"""
|
||||||
self._name2thread = {}
|
self._pm.register(plugin, name=name)
|
||||||
self._log_event = log_event
|
self._pm.check_pending()
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
def remove_account_plugin(self, plugin, name=None):
|
||||||
|
""" remove an account plugin. """
|
||||||
|
self._pm.unregister(plugin, name=name)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def temp_plugin(self, plugin):
|
||||||
|
""" run a with-block with the given plugin temporarily registered. """
|
||||||
|
self._pm.register(plugin)
|
||||||
|
yield plugin
|
||||||
|
self._pm.unregister(plugin)
|
||||||
|
|
||||||
|
def stop_ongoing(self):
|
||||||
|
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||||
|
lib.dc_stop_ongoing_process(self._dc_context)
|
||||||
|
|
||||||
|
def start_io(self):
|
||||||
|
""" start this account's IO scheduling (Rust-core async scheduler)
|
||||||
|
|
||||||
|
If this account is not configured an Exception is raised.
|
||||||
|
You need to call account.configure() and account.wait_configure_finish()
|
||||||
|
before.
|
||||||
|
|
||||||
|
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
|
||||||
|
account is started.
|
||||||
|
|
||||||
|
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
|
||||||
|
:raises ConfigureFailed: if the account could not be configured.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if not self.is_configured():
|
||||||
|
raise ValueError("account not configured, cannot start io")
|
||||||
|
lib.dc_start_io(self._dc_context)
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
""" 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()
|
||||||
|
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)
|
||||||
|
self.add_account_plugin(configtracker)
|
||||||
|
lib.dc_configure(self._dc_context)
|
||||||
|
return configtracker
|
||||||
|
|
||||||
def is_started(self):
|
def is_started(self):
|
||||||
return len(self._name2thread) > 0
|
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
|
||||||
|
|
||||||
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
|
def wait_shutdown(self):
|
||||||
assert not self.is_started()
|
""" wait until shutdown of this account has completed. """
|
||||||
if imap:
|
self._shutdown_event.wait()
|
||||||
self._start_one_thread("inbox", self.imap_thread_run)
|
|
||||||
if mvbox:
|
|
||||||
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
|
||||||
if sentbox:
|
|
||||||
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
|
||||||
if smtp:
|
|
||||||
self._start_one_thread("smtp", self.smtp_thread_run)
|
|
||||||
|
|
||||||
def _start_one_thread(self, name, func):
|
def stop_io(self):
|
||||||
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
""" stop core IO scheduler if it is running. """
|
||||||
t.setDaemon(1)
|
self.log("stop_ongoing")
|
||||||
t.start()
|
self.stop_ongoing()
|
||||||
|
|
||||||
def stop(self, wait=False):
|
if bool(lib.dc_is_io_running(self._dc_context)):
|
||||||
self._thread_quitflag = True
|
self.log("dc_stop_io (stop core IO scheduler)")
|
||||||
lib.dc_interrupt_imap_idle(self._dc_context)
|
lib.dc_stop_io(self._dc_context)
|
||||||
lib.dc_interrupt_smtp_idle(self._dc_context)
|
else:
|
||||||
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
self.log("stop_scheduler called on non-running context")
|
||||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
|
||||||
if wait:
|
|
||||||
for name, thread in self._name2thread.items():
|
|
||||||
thread.join()
|
|
||||||
|
|
||||||
def imap_thread_run(self):
|
def shutdown(self):
|
||||||
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
|
""" shutdown and destroy account (stop callback thread, close and remove
|
||||||
while not self._thread_quitflag:
|
underlying dc_context)."""
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
if self._dc_context is None:
|
||||||
if not self._thread_quitflag:
|
|
||||||
lib.dc_perform_imap_fetch(self._dc_context)
|
|
||||||
if not self._thread_quitflag:
|
|
||||||
lib.dc_perform_imap_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
|
|
||||||
|
|
||||||
def mvbox_thread_run(self):
|
|
||||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
|
||||||
while not self._thread_quitflag:
|
|
||||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
|
||||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
|
||||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
|
||||||
|
|
||||||
def sentbox_thread_run(self):
|
|
||||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
|
||||||
while not self._thread_quitflag:
|
|
||||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
|
||||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
|
||||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
|
||||||
|
|
||||||
def smtp_thread_run(self):
|
|
||||||
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
|
||||||
while not self._thread_quitflag:
|
|
||||||
lib.dc_perform_smtp_jobs(self._dc_context)
|
|
||||||
lib.dc_perform_smtp_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
|
||||||
|
|
||||||
|
|
||||||
class EventLogger:
|
|
||||||
_loglock = threading.RLock()
|
|
||||||
|
|
||||||
def __init__(self, dc_context, logid=None, debug=True):
|
|
||||||
self._dc_context = dc_context
|
|
||||||
self._event_queue = Queue()
|
|
||||||
self._debug = debug
|
|
||||||
if logid is None:
|
|
||||||
logid = str(self._dc_context).strip(">").split()[-1]
|
|
||||||
self.logid = logid
|
|
||||||
self._timeout = None
|
|
||||||
self.init_time = time.time()
|
|
||||||
|
|
||||||
def __call__(self, evt_name, data1, data2):
|
|
||||||
self._log_event(evt_name, data1, data2)
|
|
||||||
self._event_queue.put((evt_name, data1, data2))
|
|
||||||
|
|
||||||
def set_timeout(self, timeout):
|
|
||||||
self._timeout = timeout
|
|
||||||
|
|
||||||
def consume_events(self, check_error=True):
|
|
||||||
while not self._event_queue.empty():
|
|
||||||
self.get()
|
|
||||||
|
|
||||||
def get(self, timeout=None, check_error=True):
|
|
||||||
timeout = timeout or self._timeout
|
|
||||||
ev = self._event_queue.get(timeout=timeout)
|
|
||||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
|
||||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def ensure_event_not_queued(self, event_name_regex):
|
|
||||||
__tracebackhide__ = True
|
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
ev = self._event_queue.get(False)
|
|
||||||
except Empty:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
|
||||||
|
|
||||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
|
||||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
|
||||||
while 1:
|
|
||||||
ev = self.get(timeout=timeout, check_error=check_error)
|
|
||||||
if rex.match(ev[0]):
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def get_info_matching(self, regex):
|
|
||||||
rex = re.compile("(?:{}).*".format(regex))
|
|
||||||
while 1:
|
|
||||||
ev = self.get_matching("DC_EVENT_INFO")
|
|
||||||
if rex.match(ev[2]):
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def _log_event(self, evt_name, data1, data2):
|
|
||||||
# don't show events that are anyway empty impls now
|
|
||||||
if evt_name == "DC_EVENT_GET_STRING":
|
|
||||||
return
|
return
|
||||||
if self._debug:
|
|
||||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
|
||||||
self._log(evpart)
|
|
||||||
|
|
||||||
def _log(self, msg):
|
self.stop_io()
|
||||||
t = threading.currentThread()
|
|
||||||
tname = getattr(t, "name", t)
|
|
||||||
if tname == "MainThread":
|
|
||||||
tname = "MAIN"
|
|
||||||
with self._loglock:
|
|
||||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
|
||||||
|
|
||||||
|
self.log("remove dc_context references")
|
||||||
|
# the dc_context_unref triggers get_next_event to return ffi.NULL
|
||||||
|
# which in turns makes the event thread finish execution
|
||||||
|
self._dc_context = None
|
||||||
|
|
||||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
self.log("wait for event thread to finish")
|
||||||
# destructor for dc_context
|
self._event_thread.wait()
|
||||||
dc_context_unref(dc_context)
|
|
||||||
try:
|
self._shutdown_event.set()
|
||||||
deltachat.clear_context_callback(dc_context)
|
|
||||||
except (TypeError, AttributeError):
|
hook = hookspec.Global._get_plugin_manager().hook
|
||||||
# we are deep into Python Interpreter shutdown,
|
hook.dc_account_after_shutdown(account=self)
|
||||||
# so no need to clear the callback context mapping.
|
self.log("shutdown finished")
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ScannedQRCode:
|
class ScannedQRCode:
|
||||||
|
|||||||
@@ -18,24 +18,25 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, account, id):
|
def __init__(self, account, id):
|
||||||
|
from .account import Account
|
||||||
|
assert isinstance(account, Account), repr(account)
|
||||||
self.account = account
|
self.account = account
|
||||||
self._dc_context = account._dc_context
|
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.id == getattr(other, "id", None) and \
|
return self.id == getattr(other, "id", None) and \
|
||||||
self._dc_context == getattr(other, "_dc_context", None)
|
self.account._dc_context == other.account._dc_context
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not (self == other)
|
return not (self == other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
|
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _dc_chat(self):
|
def _dc_chat(self):
|
||||||
return ffi.gc(
|
return ffi.gc(
|
||||||
lib.dc_get_chat(self._dc_context, self.id),
|
lib.dc_get_chat(self.account._dc_context, self.id),
|
||||||
lib.dc_chat_unref
|
lib.dc_chat_unref
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,10 +48,20 @@ class Chat(object):
|
|||||||
- does not delete messages on server
|
- does not delete messages on server
|
||||||
- the chat or contact is not blocked, new message will arrive
|
- the chat or contact is not blocked, new message will arrive
|
||||||
"""
|
"""
|
||||||
lib.dc_delete_chat(self._dc_context, self.id)
|
lib.dc_delete_chat(self.account._dc_context, self.id)
|
||||||
|
|
||||||
# ------ chat status/metadata API ------------------------------
|
# ------ chat status/metadata API ------------------------------
|
||||||
|
|
||||||
|
def is_group(self):
|
||||||
|
""" return true if this chat is a group chat.
|
||||||
|
|
||||||
|
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||||
|
"""
|
||||||
|
return lib.dc_chat_get_type(self._dc_chat) in (
|
||||||
|
const.DC_CHAT_TYPE_GROUP,
|
||||||
|
const.DC_CHAT_TYPE_VERIFIED_GROUP
|
||||||
|
)
|
||||||
|
|
||||||
def is_deaddrop(self):
|
def is_deaddrop(self):
|
||||||
""" return true if this chat is a deaddrop chat.
|
""" return true if this chat is a deaddrop chat.
|
||||||
|
|
||||||
@@ -58,6 +69,13 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return self.id == const.DC_CHAT_ID_DEADDROP
|
return self.id == const.DC_CHAT_ID_DEADDROP
|
||||||
|
|
||||||
|
def is_muted(self):
|
||||||
|
""" return true if this chat is muted.
|
||||||
|
|
||||||
|
:returns: True if chat is muted, False otherwise.
|
||||||
|
"""
|
||||||
|
return lib.dc_chat_is_muted(self._dc_chat)
|
||||||
|
|
||||||
def is_promoted(self):
|
def is_promoted(self):
|
||||||
""" return True if this chat is promoted, i.e.
|
""" return True if this chat is promoted, i.e.
|
||||||
the member contacts are aware of their membership,
|
the member contacts are aware of their membership,
|
||||||
@@ -84,14 +102,45 @@ class Chat(object):
|
|||||||
def set_name(self, name):
|
def set_name(self, name):
|
||||||
""" set name of this chat.
|
""" set name of this chat.
|
||||||
|
|
||||||
:param: name as a unicode string.
|
:param name: as a unicode string.
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
name = as_dc_charpointer(name)
|
name = as_dc_charpointer(name)
|
||||||
return lib.dc_set_chat_name(self._dc_context, self.id, name)
|
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
|
||||||
|
|
||||||
|
def mute(self, duration=None):
|
||||||
|
""" mutes the chat
|
||||||
|
|
||||||
|
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if duration is None:
|
||||||
|
mute_duration = -1
|
||||||
|
else:
|
||||||
|
mute_duration = duration
|
||||||
|
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
|
||||||
|
if not bool(ret):
|
||||||
|
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||||
|
|
||||||
|
def unmute(self):
|
||||||
|
""" unmutes the chat
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, 0)
|
||||||
|
if not bool(ret):
|
||||||
|
raise ValueError("Failed to unmute chat")
|
||||||
|
|
||||||
|
def get_mute_duration(self):
|
||||||
|
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||||
|
|
||||||
|
:param duration:
|
||||||
|
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
|
||||||
|
"""
|
||||||
|
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
""" return type of this chat.
|
""" (deprecated) return type of this chat.
|
||||||
|
|
||||||
:returns: one of const.DC_CHAT_TYPE_*
|
:returns: one of const.DC_CHAT_TYPE_*
|
||||||
"""
|
"""
|
||||||
@@ -104,7 +153,7 @@ class Chat(object):
|
|||||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
||||||
where account.join_with_qrcode(qr) needs to be called.
|
where account.join_with_qrcode(qr) needs to be called.
|
||||||
"""
|
"""
|
||||||
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
|
res = lib.dc_get_securejoin_qr(self.account._dc_context, self.id)
|
||||||
return from_dc_charpointer(res)
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
# ------ chat messaging API ------------------------------
|
# ------ chat messaging API ------------------------------
|
||||||
@@ -126,7 +175,7 @@ class Chat(object):
|
|||||||
assert msg.id != 0
|
assert msg.id != 0
|
||||||
# get a fresh copy of dc_msg, the core needs it
|
# get a fresh copy of dc_msg, the core needs it
|
||||||
msg = Message.from_db(self.account, msg.id)
|
msg = Message.from_db(self.account, msg.id)
|
||||||
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
# modify message in place to avoid bad state for the caller
|
# modify message in place to avoid bad state for the caller
|
||||||
@@ -141,7 +190,7 @@ class Chat(object):
|
|||||||
:returns: the resulting :class:`deltachat.message.Message` instance
|
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||||
"""
|
"""
|
||||||
msg = as_dc_charpointer(text)
|
msg = as_dc_charpointer(text)
|
||||||
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg)
|
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
|
||||||
if msg_id == 0:
|
if msg_id == 0:
|
||||||
raise ValueError("message could not be send, does chat exist?")
|
raise ValueError("message could not be send, does chat exist?")
|
||||||
return Message.from_db(self.account, msg_id)
|
return Message.from_db(self.account, msg_id)
|
||||||
@@ -156,7 +205,7 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
msg = Message.new_empty(self.account, view_type="file")
|
msg = Message.new_empty(self.account, view_type="file")
|
||||||
msg.set_file(path, mime_type)
|
msg.set_file(path, mime_type)
|
||||||
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
return Message.from_db(self.account, sent_id)
|
return Message.from_db(self.account, sent_id)
|
||||||
@@ -171,7 +220,7 @@ class Chat(object):
|
|||||||
mime_type = mimetypes.guess_type(path)[0]
|
mime_type = mimetypes.guess_type(path)[0]
|
||||||
msg = Message.new_empty(self.account, view_type="image")
|
msg = Message.new_empty(self.account, view_type="image")
|
||||||
msg.set_file(path, mime_type)
|
msg.set_file(path, mime_type)
|
||||||
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
return Message.from_db(self.account, sent_id)
|
return Message.from_db(self.account, sent_id)
|
||||||
@@ -182,7 +231,7 @@ class Chat(object):
|
|||||||
:param msg: the message to be prepared.
|
:param msg: the message to be prepared.
|
||||||
:returns: :class:`deltachat.message.Message` instance.
|
:returns: :class:`deltachat.message.Message` instance.
|
||||||
"""
|
"""
|
||||||
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
|
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||||
if msg_id == 0:
|
if msg_id == 0:
|
||||||
raise ValueError("message could not be prepared")
|
raise ValueError("message could not be prepared")
|
||||||
# invalidate passed in message which is not safe to use anymore
|
# invalidate passed in message which is not safe to use anymore
|
||||||
@@ -218,7 +267,7 @@ class Chat(object):
|
|||||||
msg = Message.from_db(self.account, message.id)
|
msg = Message.from_db(self.account, message.id)
|
||||||
|
|
||||||
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
||||||
sent_id = lib.dc_send_msg(self._dc_context, 0, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
assert sent_id == msg.id
|
assert sent_id == msg.id
|
||||||
@@ -232,9 +281,9 @@ class Chat(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
if message is None:
|
if message is None:
|
||||||
lib.dc_set_draft(self._dc_context, self.id, ffi.NULL)
|
lib.dc_set_draft(self.account._dc_context, self.id, ffi.NULL)
|
||||||
else:
|
else:
|
||||||
lib.dc_set_draft(self._dc_context, self.id, message._dc_msg)
|
lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
|
||||||
|
|
||||||
def get_draft(self):
|
def get_draft(self):
|
||||||
""" get draft message for this chat.
|
""" get draft message for this chat.
|
||||||
@@ -242,7 +291,7 @@ class Chat(object):
|
|||||||
:param message: a :class:`Message` instance
|
:param message: a :class:`Message` instance
|
||||||
:returns: Message object or None (if no draft available)
|
:returns: Message object or None (if no draft available)
|
||||||
"""
|
"""
|
||||||
x = lib.dc_get_draft(self._dc_context, self.id)
|
x = lib.dc_get_draft(self.account._dc_context, self.id)
|
||||||
if x == ffi.NULL:
|
if x == ffi.NULL:
|
||||||
return None
|
return None
|
||||||
dc_msg = ffi.gc(x, lib.dc_msg_unref)
|
dc_msg = ffi.gc(x, lib.dc_msg_unref)
|
||||||
@@ -254,7 +303,7 @@ class Chat(object):
|
|||||||
:returns: list of :class:`deltachat.message.Message` objects for this chat.
|
:returns: list of :class:`deltachat.message.Message` objects for this chat.
|
||||||
"""
|
"""
|
||||||
dc_array = ffi.gc(
|
dc_array = ffi.gc(
|
||||||
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0),
|
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
|
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
|
||||||
@@ -264,60 +313,69 @@ class Chat(object):
|
|||||||
|
|
||||||
:returns: number of fresh messages
|
:returns: number of fresh messages
|
||||||
"""
|
"""
|
||||||
return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id)
|
return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
|
||||||
|
|
||||||
def mark_noticed(self):
|
def mark_noticed(self):
|
||||||
""" mark all messages in this chat as noticed.
|
""" mark all messages in this chat as noticed.
|
||||||
|
|
||||||
Noticed messages are no longer fresh.
|
Noticed messages are no longer fresh.
|
||||||
"""
|
"""
|
||||||
return lib.dc_marknoticed_chat(self._dc_context, self.id)
|
return lib.dc_marknoticed_chat(self.account._dc_context, self.id)
|
||||||
|
|
||||||
def get_summary(self):
|
def get_summary(self):
|
||||||
""" return dictionary with summary information. """
|
""" return dictionary with summary information. """
|
||||||
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
|
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
|
||||||
s = from_dc_charpointer(dc_res)
|
s = from_dc_charpointer(dc_res)
|
||||||
return json.loads(s)
|
return json.loads(s)
|
||||||
|
|
||||||
# ------ group management API ------------------------------
|
# ------ group management API ------------------------------
|
||||||
|
|
||||||
def add_contact(self, contact):
|
def add_contact(self, obj):
|
||||||
""" add a contact to this chat.
|
""" add a contact to this chat.
|
||||||
|
|
||||||
:params: contact object.
|
:params obj: Contact, Account or e-mail address.
|
||||||
:raises ValueError: if contact could not be added
|
:raises ValueError: if contact could not be added
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id)
|
contact = self.account.create_contact(obj)
|
||||||
|
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
|
||||||
if ret != 1:
|
if ret != 1:
|
||||||
raise ValueError("could not add contact {!r} to chat".format(contact))
|
raise ValueError("could not add contact {!r} to chat".format(contact))
|
||||||
|
return contact
|
||||||
|
|
||||||
def remove_contact(self, contact):
|
def remove_contact(self, obj):
|
||||||
""" remove a contact from this chat.
|
""" remove a contact from this chat.
|
||||||
|
|
||||||
:params: contact object.
|
:params obj: Contact, Account or e-mail address.
|
||||||
:raises ValueError: if contact could not be removed
|
:raises ValueError: if contact could not be removed
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id)
|
contact = self.account.create_contact(obj)
|
||||||
|
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
|
||||||
if ret != 1:
|
if ret != 1:
|
||||||
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
||||||
|
|
||||||
def get_contacts(self):
|
def get_contacts(self):
|
||||||
""" get all contacts for this chat.
|
""" get all contacts for this chat.
|
||||||
:params: contact object.
|
|
||||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
dc_array = ffi.gc(
|
dc_array = ffi.gc(
|
||||||
lib.dc_get_chat_contacts(self._dc_context, self.id),
|
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(
|
return list(iter_array(
|
||||||
dc_array, lambda id: Contact(self._dc_context, id))
|
dc_array, lambda id: Contact(self.account, id))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def num_contacts(self):
|
||||||
|
""" return number of contacts in this chat. """
|
||||||
|
dc_array = ffi.gc(
|
||||||
|
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
||||||
|
lib.dc_array_unref
|
||||||
|
)
|
||||||
|
return lib.dc_array_get_cnt(dc_array)
|
||||||
|
|
||||||
def set_profile_image(self, img_path):
|
def set_profile_image(self, img_path):
|
||||||
"""Set group profile image.
|
"""Set group profile image.
|
||||||
|
|
||||||
@@ -330,7 +388,7 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
assert os.path.exists(img_path), img_path
|
assert os.path.exists(img_path), img_path
|
||||||
p = as_dc_charpointer(img_path)
|
p = as_dc_charpointer(img_path)
|
||||||
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p)
|
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p)
|
||||||
if res != 1:
|
if res != 1:
|
||||||
raise ValueError("Setting Profile Image {!r} failed".format(p))
|
raise ValueError("Setting Profile Image {!r} failed".format(p))
|
||||||
|
|
||||||
@@ -343,7 +401,7 @@ class Chat(object):
|
|||||||
:raises ValueError: if profile image could not be reset
|
:raises ValueError: if profile image could not be reset
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL)
|
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, ffi.NULL)
|
||||||
if res != 1:
|
if res != 1:
|
||||||
raise ValueError("Removing Profile Image failed")
|
raise ValueError("Removing Profile Image failed")
|
||||||
|
|
||||||
@@ -367,32 +425,26 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_color(self._dc_chat)
|
return lib.dc_chat_get_color(self._dc_chat)
|
||||||
|
|
||||||
def get_subtitle(self):
|
|
||||||
"""return the subtitle of the chat
|
|
||||||
:returns: the subtitle
|
|
||||||
"""
|
|
||||||
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
|
||||||
|
|
||||||
# ------ location streaming API ------------------------------
|
# ------ location streaming API ------------------------------
|
||||||
|
|
||||||
def is_sending_locations(self):
|
def is_sending_locations(self):
|
||||||
"""return True if this chat has location-sending enabled currently.
|
"""return True if this chat has location-sending enabled currently.
|
||||||
:returns: True if location sending is enabled.
|
:returns: True if location sending is enabled.
|
||||||
"""
|
"""
|
||||||
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
|
return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
|
||||||
|
|
||||||
def is_archived(self):
|
def is_archived(self):
|
||||||
"""return True if this chat is archived.
|
"""return True if this chat is archived.
|
||||||
:returns: True if archived.
|
:returns: True if archived.
|
||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_archived(self._dc_chat)
|
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
|
||||||
|
|
||||||
def enable_sending_locations(self, seconds):
|
def enable_sending_locations(self, seconds):
|
||||||
"""enable sending locations for this chat.
|
"""enable sending locations for this chat.
|
||||||
|
|
||||||
all subsequent messages will carry a location with them.
|
all subsequent messages will carry a location with them.
|
||||||
"""
|
"""
|
||||||
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
|
lib.dc_send_locations_to_chat(self.account._dc_context, self.id, seconds)
|
||||||
|
|
||||||
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
||||||
"""return list of locations for the given contact in the given timespan.
|
"""return list of locations for the given contact in the given timespan.
|
||||||
@@ -416,7 +468,7 @@ class Chat(object):
|
|||||||
else:
|
else:
|
||||||
contact_id = contact.id
|
contact_id = contact.id
|
||||||
|
|
||||||
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
|
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
|
||||||
return [
|
return [
|
||||||
Location(
|
Location(
|
||||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||||
|
|||||||
@@ -1,193 +1,7 @@
|
|||||||
import sys
|
from .capi import lib
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from os.path import dirname, abspath
|
|
||||||
from os.path import join as joinpath
|
|
||||||
|
|
||||||
# the following const are generated from deltachat.h
|
|
||||||
# this works well when you in a git-checkout
|
|
||||||
# run "python deltachat/const.py" to regenerate events
|
|
||||||
# begin const generated
|
|
||||||
DC_GCL_ARCHIVED_ONLY = 0x01
|
|
||||||
DC_GCL_NO_SPECIALS = 0x02
|
|
||||||
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
|
||||||
DC_GCL_VERIFIED_ONLY = 0x01
|
|
||||||
DC_GCL_ADD_SELF = 0x02
|
|
||||||
DC_QR_ASK_VERIFYCONTACT = 200
|
|
||||||
DC_QR_ASK_VERIFYGROUP = 202
|
|
||||||
DC_QR_FPR_OK = 210
|
|
||||||
DC_QR_FPR_MISMATCH = 220
|
|
||||||
DC_QR_FPR_WITHOUT_ADDR = 230
|
|
||||||
DC_QR_ADDR = 320
|
|
||||||
DC_QR_TEXT = 330
|
|
||||||
DC_QR_URL = 332
|
|
||||||
DC_QR_ERROR = 400
|
|
||||||
DC_CHAT_ID_DEADDROP = 1
|
|
||||||
DC_CHAT_ID_TRASH = 3
|
|
||||||
DC_CHAT_ID_MSGS_IN_CREATION = 4
|
|
||||||
DC_CHAT_ID_STARRED = 5
|
|
||||||
DC_CHAT_ID_ARCHIVED_LINK = 6
|
|
||||||
DC_CHAT_ID_ALLDONE_HINT = 7
|
|
||||||
DC_CHAT_ID_LAST_SPECIAL = 9
|
|
||||||
DC_CHAT_TYPE_UNDEFINED = 0
|
|
||||||
DC_CHAT_TYPE_SINGLE = 100
|
|
||||||
DC_CHAT_TYPE_GROUP = 120
|
|
||||||
DC_CHAT_TYPE_VERIFIED_GROUP = 130
|
|
||||||
DC_MSG_ID_MARKER1 = 1
|
|
||||||
DC_MSG_ID_DAYMARKER = 9
|
|
||||||
DC_MSG_ID_LAST_SPECIAL = 9
|
|
||||||
DC_STATE_UNDEFINED = 0
|
|
||||||
DC_STATE_IN_FRESH = 10
|
|
||||||
DC_STATE_IN_NOTICED = 13
|
|
||||||
DC_STATE_IN_SEEN = 16
|
|
||||||
DC_STATE_OUT_PREPARING = 18
|
|
||||||
DC_STATE_OUT_DRAFT = 19
|
|
||||||
DC_STATE_OUT_PENDING = 20
|
|
||||||
DC_STATE_OUT_FAILED = 24
|
|
||||||
DC_STATE_OUT_DELIVERED = 26
|
|
||||||
DC_STATE_OUT_MDN_RCVD = 28
|
|
||||||
DC_CONTACT_ID_SELF = 1
|
|
||||||
DC_CONTACT_ID_INFO = 2
|
|
||||||
DC_CONTACT_ID_DEVICE = 5
|
|
||||||
DC_CONTACT_ID_LAST_SPECIAL = 9
|
|
||||||
DC_MSG_TEXT = 10
|
|
||||||
DC_MSG_IMAGE = 20
|
|
||||||
DC_MSG_GIF = 21
|
|
||||||
DC_MSG_STICKER = 23
|
|
||||||
DC_MSG_AUDIO = 40
|
|
||||||
DC_MSG_VOICE = 41
|
|
||||||
DC_MSG_VIDEO = 50
|
|
||||||
DC_MSG_FILE = 60
|
|
||||||
DC_LP_AUTH_OAUTH2 = 0x2
|
|
||||||
DC_LP_AUTH_NORMAL = 0x4
|
|
||||||
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
|
|
||||||
DC_LP_IMAP_SOCKET_SSL = 0x200
|
|
||||||
DC_LP_IMAP_SOCKET_PLAIN = 0x400
|
|
||||||
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
|
|
||||||
DC_LP_SMTP_SOCKET_SSL = 0x20000
|
|
||||||
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
|
|
||||||
DC_CERTCK_AUTO = 0
|
|
||||||
DC_CERTCK_STRICT = 1
|
|
||||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
|
|
||||||
DC_EMPTY_MVBOX = 0x01
|
|
||||||
DC_EMPTY_INBOX = 0x02
|
|
||||||
DC_EVENT_INFO = 100
|
|
||||||
DC_EVENT_SMTP_CONNECTED = 101
|
|
||||||
DC_EVENT_IMAP_CONNECTED = 102
|
|
||||||
DC_EVENT_SMTP_MESSAGE_SENT = 103
|
|
||||||
DC_EVENT_IMAP_MESSAGE_DELETED = 104
|
|
||||||
DC_EVENT_IMAP_MESSAGE_MOVED = 105
|
|
||||||
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
|
|
||||||
DC_EVENT_NEW_BLOB_FILE = 150
|
|
||||||
DC_EVENT_DELETED_BLOB_FILE = 151
|
|
||||||
DC_EVENT_WARNING = 300
|
|
||||||
DC_EVENT_ERROR = 400
|
|
||||||
DC_EVENT_ERROR_NETWORK = 401
|
|
||||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410
|
|
||||||
DC_EVENT_MSGS_CHANGED = 2000
|
|
||||||
DC_EVENT_INCOMING_MSG = 2005
|
|
||||||
DC_EVENT_MSG_DELIVERED = 2010
|
|
||||||
DC_EVENT_MSG_FAILED = 2012
|
|
||||||
DC_EVENT_MSG_READ = 2015
|
|
||||||
DC_EVENT_CHAT_MODIFIED = 2020
|
|
||||||
DC_EVENT_CONTACTS_CHANGED = 2030
|
|
||||||
DC_EVENT_LOCATION_CHANGED = 2035
|
|
||||||
DC_EVENT_CONFIGURE_PROGRESS = 2041
|
|
||||||
DC_EVENT_IMEX_PROGRESS = 2051
|
|
||||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
|
||||||
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
|
||||||
DC_EVENT_FILE_COPIED = 2055
|
|
||||||
DC_EVENT_IS_OFFLINE = 2081
|
|
||||||
DC_EVENT_GET_STRING = 2091
|
|
||||||
DC_STR_SELFNOTINGRP = 21
|
|
||||||
DC_PROVIDER_STATUS_OK = 1
|
|
||||||
DC_PROVIDER_STATUS_PREPARATION = 2
|
|
||||||
DC_PROVIDER_STATUS_BROKEN = 3
|
|
||||||
DC_STR_NOMESSAGES = 1
|
|
||||||
DC_STR_SELF = 2
|
|
||||||
DC_STR_DRAFT = 3
|
|
||||||
DC_STR_MEMBER = 4
|
|
||||||
DC_STR_CONTACT = 6
|
|
||||||
DC_STR_VOICEMESSAGE = 7
|
|
||||||
DC_STR_DEADDROP = 8
|
|
||||||
DC_STR_IMAGE = 9
|
|
||||||
DC_STR_VIDEO = 10
|
|
||||||
DC_STR_AUDIO = 11
|
|
||||||
DC_STR_FILE = 12
|
|
||||||
DC_STR_STATUSLINE = 13
|
|
||||||
DC_STR_NEWGROUPDRAFT = 14
|
|
||||||
DC_STR_MSGGRPNAME = 15
|
|
||||||
DC_STR_MSGGRPIMGCHANGED = 16
|
|
||||||
DC_STR_MSGADDMEMBER = 17
|
|
||||||
DC_STR_MSGDELMEMBER = 18
|
|
||||||
DC_STR_MSGGROUPLEFT = 19
|
|
||||||
DC_STR_GIF = 23
|
|
||||||
DC_STR_ENCRYPTEDMSG = 24
|
|
||||||
DC_STR_E2E_AVAILABLE = 25
|
|
||||||
DC_STR_ENCR_TRANSP = 27
|
|
||||||
DC_STR_ENCR_NONE = 28
|
|
||||||
DC_STR_CANTDECRYPT_MSG_BODY = 29
|
|
||||||
DC_STR_FINGERPRINTS = 30
|
|
||||||
DC_STR_READRCPT = 31
|
|
||||||
DC_STR_READRCPT_MAILBODY = 32
|
|
||||||
DC_STR_MSGGRPIMGDELETED = 33
|
|
||||||
DC_STR_E2E_PREFERRED = 34
|
|
||||||
DC_STR_CONTACT_VERIFIED = 35
|
|
||||||
DC_STR_CONTACT_NOT_VERIFIED = 36
|
|
||||||
DC_STR_CONTACT_SETUP_CHANGED = 37
|
|
||||||
DC_STR_ARCHIVEDCHATS = 40
|
|
||||||
DC_STR_STARREDMSGS = 41
|
|
||||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
|
||||||
DC_STR_AC_SETUP_MSG_BODY = 43
|
|
||||||
DC_STR_SELFTALK_SUBTITLE = 50
|
|
||||||
DC_STR_CANNOT_LOGIN = 60
|
|
||||||
DC_STR_SERVER_RESPONSE = 61
|
|
||||||
DC_STR_MSGACTIONBYUSER = 62
|
|
||||||
DC_STR_MSGACTIONBYME = 63
|
|
||||||
DC_STR_MSGLOCATIONENABLED = 64
|
|
||||||
DC_STR_MSGLOCATIONDISABLED = 65
|
|
||||||
DC_STR_LOCATION = 66
|
|
||||||
DC_STR_STICKER = 67
|
|
||||||
DC_STR_DEVICE_MESSAGES = 68
|
|
||||||
DC_STR_COUNT = 68
|
|
||||||
# end const generated
|
|
||||||
|
|
||||||
|
|
||||||
def read_event_defines(f):
|
for name in dir(lib):
|
||||||
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
|
if name.startswith("DC_"):
|
||||||
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER)_\S+)\s+([x\d]+).*')
|
globals()[name] = getattr(lib, name)
|
||||||
for line in f:
|
del name
|
||||||
m = rex.match(line)
|
|
||||||
if m:
|
|
||||||
yield m.groups()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
here = abspath(__file__).rstrip("oc")
|
|
||||||
here_dir = dirname(here)
|
|
||||||
if len(sys.argv) >= 2:
|
|
||||||
deltah = sys.argv[1]
|
|
||||||
else:
|
|
||||||
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
|
|
||||||
assert os.path.exists(deltah)
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
skip_to_end = False
|
|
||||||
for orig_line in open(here):
|
|
||||||
if skip_to_end:
|
|
||||||
if not orig_line.startswith("# end const"):
|
|
||||||
continue
|
|
||||||
skip_to_end = False
|
|
||||||
lines.append(orig_line)
|
|
||||||
if orig_line.startswith("# begin const"):
|
|
||||||
with open(deltah) as f:
|
|
||||||
for name, item in read_event_defines(f):
|
|
||||||
lines.append("{} = {}\n".format(name, item))
|
|
||||||
skip_to_end = True
|
|
||||||
|
|
||||||
tmpname = here + ".tmp"
|
|
||||||
with open(tmpname, "w") as f:
|
|
||||||
f.write("".join(lines))
|
|
||||||
os.rename(tmpname, here)
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
from . import props
|
from . import props
|
||||||
from .cutil import from_dc_charpointer
|
from .cutil import from_dc_charpointer
|
||||||
from .capi import lib, ffi
|
from .capi import lib, ffi
|
||||||
|
from .chat import Chat
|
||||||
|
from . import const
|
||||||
|
|
||||||
|
|
||||||
class Contact(object):
|
class Contact(object):
|
||||||
@@ -10,23 +12,25 @@ class Contact(object):
|
|||||||
|
|
||||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, dc_context, id):
|
def __init__(self, account, id):
|
||||||
self._dc_context = dc_context
|
from .account import Account
|
||||||
|
assert isinstance(account, Account), repr(account)
|
||||||
|
self.account = account
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self._dc_context == other._dc_context and self.id == other.id
|
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not (self == other)
|
return not (self == other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _dc_contact(self):
|
def _dc_contact(self):
|
||||||
return ffi.gc(
|
return ffi.gc(
|
||||||
lib.dc_get_contact(self._dc_context, self.id),
|
lib.dc_get_contact(self.account._dc_context, self.id),
|
||||||
lib.dc_contact_unref
|
lib.dc_contact_unref
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,10 +40,13 @@ class Contact(object):
|
|||||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def display_name(self):
|
def name(self):
|
||||||
""" display name for this contact. """
|
""" display name for this contact. """
|
||||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||||
|
|
||||||
|
# deprecated alias
|
||||||
|
display_name = name
|
||||||
|
|
||||||
def is_blocked(self):
|
def is_blocked(self):
|
||||||
""" Return True if the contact is blocked. """
|
""" Return True if the contact is blocked. """
|
||||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||||
@@ -57,3 +64,17 @@ class Contact(object):
|
|||||||
if dc_res == ffi.NULL:
|
if dc_res == ffi.NULL:
|
||||||
return None
|
return None
|
||||||
return from_dc_charpointer(dc_res)
|
return from_dc_charpointer(dc_res)
|
||||||
|
|
||||||
|
def create_chat(self):
|
||||||
|
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||||
|
|
||||||
|
:param contact: chat_id (int) or contact object.
|
||||||
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
|
"""
|
||||||
|
dc_context = self.account._dc_context
|
||||||
|
chat_id = lib.dc_create_chat_by_contact_id(dc_context, self.id)
|
||||||
|
assert chat_id > const.DC_CHAT_ID_LAST_SPECIAL, chat_id
|
||||||
|
return Chat(self.account, chat_id)
|
||||||
|
|
||||||
|
# deprecated name
|
||||||
|
get_chat = create_chat
|
||||||
|
|||||||
213
python/src/deltachat/direct_imap.py
Normal file
213
python/src/deltachat/direct_imap.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
Internal Python-level IMAP handling used by the testplugin
|
||||||
|
and for cleaning up inbox/mvbox for each test function run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import email
|
||||||
|
import ssl
|
||||||
|
import pathlib
|
||||||
|
from imapclient import IMAPClient
|
||||||
|
from imapclient.exceptions import IMAPClientError
|
||||||
|
import deltachat
|
||||||
|
|
||||||
|
|
||||||
|
SEEN = b'\\Seen'
|
||||||
|
DELETED = b'\\Deleted'
|
||||||
|
FLAGS = b'FLAGS'
|
||||||
|
FETCH = b'FETCH'
|
||||||
|
ALL = "1:*"
|
||||||
|
|
||||||
|
|
||||||
|
@deltachat.global_hookimpl
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@deltachat.global_hookimpl
|
||||||
|
def dc_account_after_shutdown(account):
|
||||||
|
""" shutdown the imap connection if there is one. """
|
||||||
|
imap = getattr(account, "direct_imap", None)
|
||||||
|
if imap is not None:
|
||||||
|
imap.shutdown()
|
||||||
|
del account.direct_imap
|
||||||
|
|
||||||
|
|
||||||
|
class DirectImap:
|
||||||
|
def __init__(self, account):
|
||||||
|
self.account = account
|
||||||
|
self.logid = account.get_config("displayname") or id(account)
|
||||||
|
self._idling = False
|
||||||
|
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")
|
||||||
|
user = self.account.get_config("addr")
|
||||||
|
pw = self.account.get_config("mail_pw")
|
||||||
|
self.conn = IMAPClient(host, ssl_context=ssl_context)
|
||||||
|
self.conn.login(user, pw)
|
||||||
|
|
||||||
|
self.select_folder("INBOX")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
try:
|
||||||
|
self.conn.idle_done()
|
||||||
|
except (OSError, IMAPClientError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.conn.logout()
|
||||||
|
except (OSError, IMAPClientError):
|
||||||
|
print("Could not logout direct_imap conn")
|
||||||
|
|
||||||
|
def select_folder(self, foldername):
|
||||||
|
assert not self._idling
|
||||||
|
return self.conn.select_folder(foldername)
|
||||||
|
|
||||||
|
def select_config_folder(self, config_name):
|
||||||
|
""" Return info about selected folder if it is
|
||||||
|
configured, otherwise None. """
|
||||||
|
if "_" not in config_name:
|
||||||
|
config_name = "configured_{}_folder".format(config_name)
|
||||||
|
foldername = self.account.get_config(config_name)
|
||||||
|
if foldername:
|
||||||
|
return self.select_folder(foldername)
|
||||||
|
|
||||||
|
def list_folders(self):
|
||||||
|
""" return list of all existing folder names"""
|
||||||
|
assert not self._idling
|
||||||
|
folders = []
|
||||||
|
for meta, sep, foldername in self.conn.list_folders():
|
||||||
|
folders.append(foldername)
|
||||||
|
return folders
|
||||||
|
|
||||||
|
def delete(self, range, expunge=True):
|
||||||
|
""" delete a range of messages (imap-syntax).
|
||||||
|
If expunge is true, perform the expunge-operation
|
||||||
|
to make sure the messages are really gone and not
|
||||||
|
just flagged as deleted.
|
||||||
|
"""
|
||||||
|
self.conn.set_flags(range, [DELETED])
|
||||||
|
if expunge:
|
||||||
|
self.conn.expunge()
|
||||||
|
|
||||||
|
def get_all_messages(self):
|
||||||
|
assert not self._idling
|
||||||
|
return self.conn.fetch(ALL, [FLAGS])
|
||||||
|
|
||||||
|
def get_unread_messages(self):
|
||||||
|
assert not self._idling
|
||||||
|
res = self.conn.fetch(ALL, [FLAGS])
|
||||||
|
return [uid for uid in res
|
||||||
|
if SEEN not in res[uid][FLAGS]]
|
||||||
|
|
||||||
|
def mark_all_read(self):
|
||||||
|
messages = self.get_unread_messages()
|
||||||
|
if messages:
|
||||||
|
res = self.conn.set_flags(messages, [SEEN])
|
||||||
|
print("marked seen:", messages, res)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def log(*args, **kwargs):
|
||||||
|
kwargs["file"] = stream
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
empty_folders = []
|
||||||
|
for imapfolder in self.list_folders():
|
||||||
|
self.select_folder(imapfolder)
|
||||||
|
messages = list(self.get_all_messages())
|
||||||
|
if not messages:
|
||||||
|
empty_folders.append(imapfolder)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log("---------", imapfolder, len(messages), "messages ---------")
|
||||||
|
# get message content without auto-marking it as seen
|
||||||
|
# fetching 'RFC822' would mark it as seen.
|
||||||
|
requested = [b'BODY.PEEK[HEADER]', FLAGS]
|
||||||
|
for uid, data in self.conn.fetch(messages, requested).items():
|
||||||
|
body_bytes = data[b'BODY[HEADER]']
|
||||||
|
flags = data[FLAGS]
|
||||||
|
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
fn = path.joinpath(str(uid))
|
||||||
|
fn.write_bytes(body_bytes)
|
||||||
|
log("Message", uid, fn)
|
||||||
|
email_message = email.message_from_bytes(body_bytes)
|
||||||
|
log("Message", uid, flags, "Message-Id:", email_message.get("Message-Id"))
|
||||||
|
|
||||||
|
if empty_folders:
|
||||||
|
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||||
|
|
||||||
|
print(stream.getvalue(), file=logfile)
|
||||||
|
|
||||||
|
def idle_start(self):
|
||||||
|
""" switch this connection to idle mode. non-blocking. """
|
||||||
|
assert not self._idling
|
||||||
|
res = self.conn.idle()
|
||||||
|
self._idling = True
|
||||||
|
return res
|
||||||
|
|
||||||
|
def idle_check(self, terminate=False):
|
||||||
|
""" (blocking) wait for next idle message from server. """
|
||||||
|
assert self._idling
|
||||||
|
self.account.log("imap-direct: calling idle_check")
|
||||||
|
res = self.conn.idle_check(timeout=30)
|
||||||
|
if len(res) == 0:
|
||||||
|
raise TimeoutError
|
||||||
|
if terminate:
|
||||||
|
self.idle_done()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def idle_wait_for_seen(self):
|
||||||
|
""" Return first message with SEEN flag
|
||||||
|
from a running idle-stream REtiurn.
|
||||||
|
"""
|
||||||
|
while 1:
|
||||||
|
for item in self.idle_check():
|
||||||
|
if item[1] == FETCH:
|
||||||
|
if item[2][0] == FLAGS:
|
||||||
|
if SEEN in item[2][1]:
|
||||||
|
return item[0]
|
||||||
|
|
||||||
|
def idle_done(self):
|
||||||
|
""" send idle-done to server if we are currently in idle mode. """
|
||||||
|
if self._idling:
|
||||||
|
res = self.conn.idle_done()
|
||||||
|
self._idling = False
|
||||||
|
return res
|
||||||
219
python/src/deltachat/events.py
Normal file
219
python/src/deltachat/events.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from queue import Queue, Empty
|
||||||
|
|
||||||
|
import deltachat
|
||||||
|
from .hookspec import account_hookimpl
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from .capi import ffi, lib
|
||||||
|
from .message import map_system_message
|
||||||
|
from .cutil import from_dc_charpointer
|
||||||
|
|
||||||
|
|
||||||
|
class FFIEvent:
|
||||||
|
def __init__(self, name, data1, data2):
|
||||||
|
self.name = name
|
||||||
|
self.data1 = data1
|
||||||
|
self.data2 = data2
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class FFIEventLogger:
|
||||||
|
""" If you register an instance of this logger with an Account
|
||||||
|
you'll get all ffi-events printed.
|
||||||
|
"""
|
||||||
|
# to prevent garbled logging
|
||||||
|
_loglock = threading.RLock()
|
||||||
|
|
||||||
|
def __init__(self, account):
|
||||||
|
self.account = account
|
||||||
|
self.logid = self.account.get_config("displayname")
|
||||||
|
self.init_time = time.time()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
self.account.log(str(ffi_event))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_log_line(self, message):
|
||||||
|
t = threading.currentThread()
|
||||||
|
tname = getattr(t, "name", t)
|
||||||
|
if tname == "MainThread":
|
||||||
|
tname = "MAIN"
|
||||||
|
elapsed = time.time() - self.init_time
|
||||||
|
locname = tname
|
||||||
|
if self.logid:
|
||||||
|
locname += "-" + self.logid
|
||||||
|
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
||||||
|
with self._loglock:
|
||||||
|
print(s, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FFIEventTracker:
|
||||||
|
def __init__(self, account, timeout=None):
|
||||||
|
self.account = account
|
||||||
|
self._timeout = timeout
|
||||||
|
self._event_queue = Queue()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
self._event_queue.put(ffi_event)
|
||||||
|
|
||||||
|
def set_timeout(self, timeout):
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def consume_events(self, check_error=True):
|
||||||
|
while not self._event_queue.empty():
|
||||||
|
self.get(check_error=check_error)
|
||||||
|
|
||||||
|
def get(self, timeout=None, check_error=True):
|
||||||
|
timeout = timeout if timeout is not None else self._timeout
|
||||||
|
ev = self._event_queue.get(timeout=timeout)
|
||||||
|
if check_error and ev.name == "DC_EVENT_ERROR":
|
||||||
|
raise ValueError("unexpected event: {}".format(ev))
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def iter_events(self, timeout=None, check_error=True):
|
||||||
|
while 1:
|
||||||
|
yield self.get(timeout=timeout, check_error=check_error)
|
||||||
|
|
||||||
|
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
for ev in self.iter_events(timeout=timeout, check_error=check_error):
|
||||||
|
if rex.match(ev.name):
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def get_info_matching(self, regex):
|
||||||
|
rex = re.compile("(?:{}).*".format(regex))
|
||||||
|
while 1:
|
||||||
|
ev = self.get_matching("DC_EVENT_INFO")
|
||||||
|
if rex.match(ev.data2):
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def ensure_event_not_queued(self, event_name_regex):
|
||||||
|
__tracebackhide__ = True
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
ev = self._event_queue.get(False)
|
||||||
|
except Empty:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
assert not rex.match(ev.name), "event found {}".format(ev)
|
||||||
|
|
||||||
|
def wait_securejoin_inviter_progress(self, target):
|
||||||
|
while 1:
|
||||||
|
event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||||
|
if event.data2 >= target:
|
||||||
|
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
|
||||||
|
break
|
||||||
|
|
||||||
|
def wait_next_incoming_message(self):
|
||||||
|
""" wait for and return next incoming message. """
|
||||||
|
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
|
||||||
|
return self.account.get_message_by_id(ev.data2)
|
||||||
|
|
||||||
|
def wait_next_messages_changed(self):
|
||||||
|
""" wait for and return next message-changed message or None
|
||||||
|
if the event contains no msgid"""
|
||||||
|
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
if ev.data2 > 0:
|
||||||
|
return self.account.get_message_by_id(ev.data2)
|
||||||
|
|
||||||
|
def wait_msg_delivered(self, msg):
|
||||||
|
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
assert ev.data1 == msg.chat.id
|
||||||
|
assert ev.data2 == msg.id
|
||||||
|
assert msg.is_out_delivered()
|
||||||
|
|
||||||
|
|
||||||
|
class EventThread(threading.Thread):
|
||||||
|
""" Event Thread for an account.
|
||||||
|
|
||||||
|
With each Account init this callback thread is started.
|
||||||
|
"""
|
||||||
|
def __init__(self, account):
|
||||||
|
self.account = account
|
||||||
|
super(EventThread, self).__init__(name="events")
|
||||||
|
self.setDaemon(True)
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def log_execution(self, message):
|
||||||
|
self.account.log(message + " START")
|
||||||
|
yield
|
||||||
|
self.account.log(message + " FINISHED")
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
if self == threading.current_thread():
|
||||||
|
# we are in the callback thread and thus cannot
|
||||||
|
# wait for the thread-loop to finish.
|
||||||
|
return
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" get and run events until shutdown. """
|
||||||
|
with self.log_execution("EVENT THREAD"):
|
||||||
|
self._inner_run()
|
||||||
|
|
||||||
|
def _inner_run(self):
|
||||||
|
event_emitter = ffi.gc(
|
||||||
|
lib.dc_get_event_emitter(self.account._dc_context),
|
||||||
|
lib.dc_event_emitter_unref,
|
||||||
|
)
|
||||||
|
while 1:
|
||||||
|
event = lib.dc_get_next_event(event_emitter)
|
||||||
|
if event == ffi.NULL:
|
||||||
|
break
|
||||||
|
evt = lib.dc_event_get_id(event)
|
||||||
|
data1 = lib.dc_event_get_data1_int(event)
|
||||||
|
# the following code relates to the deltachat/_build.py's helper
|
||||||
|
# function which provides us signature info of an event call
|
||||||
|
evt_name = deltachat.get_dc_event_name(evt)
|
||||||
|
if lib.dc_event_has_string_data(evt):
|
||||||
|
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||||
|
else:
|
||||||
|
data2 = lib.dc_event_get_data2_int(event)
|
||||||
|
|
||||||
|
lib.dc_event_unref(event)
|
||||||
|
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
||||||
|
try:
|
||||||
|
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
|
||||||
|
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||||
|
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
|
||||||
|
hook = getattr(self.account._pm.hook, name)
|
||||||
|
hook(**kwargs)
|
||||||
|
except Exception:
|
||||||
|
if self.account._dc_context is not None:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _map_ffi_event(self, ffi_event):
|
||||||
|
name = ffi_event.name
|
||||||
|
account = self.account
|
||||||
|
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||||
|
data1 = ffi_event.data1
|
||||||
|
if data1 == 0 or data1 == 1000:
|
||||||
|
success = data1 == 1000
|
||||||
|
yield "ac_configure_completed", dict(success=success)
|
||||||
|
elif name == "DC_EVENT_INCOMING_MSG":
|
||||||
|
msg = account.get_message_by_id(ffi_event.data2)
|
||||||
|
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||||
|
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||||
|
if ffi_event.data2 != 0:
|
||||||
|
msg = account.get_message_by_id(ffi_event.data2)
|
||||||
|
if msg.is_outgoing():
|
||||||
|
res = map_system_message(msg)
|
||||||
|
if res and res[0].startswith("ac_member"):
|
||||||
|
yield res
|
||||||
|
yield "ac_outgoing_message", dict(message=msg)
|
||||||
|
elif msg.is_in_fresh():
|
||||||
|
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||||
|
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||||
|
msg = account.get_message_by_id(ffi_event.data2)
|
||||||
|
yield "ac_message_delivered", dict(message=msg)
|
||||||
|
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||||
|
chat = account.get_chat_by_id(ffi_event.data1)
|
||||||
|
yield "ac_chat_modified", dict(chat=chat)
|
||||||
101
python/src/deltachat/hookspec.py
Normal file
101
python/src/deltachat/hookspec.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
||||||
|
|
||||||
|
import pluggy
|
||||||
|
|
||||||
|
|
||||||
|
account_spec_name = "deltachat-account"
|
||||||
|
account_hookspec = pluggy.HookspecMarker(account_spec_name)
|
||||||
|
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
|
||||||
|
|
||||||
|
global_spec_name = "deltachat-global"
|
||||||
|
global_hookspec = pluggy.HookspecMarker(global_spec_name)
|
||||||
|
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
|
||||||
|
|
||||||
|
|
||||||
|
class PerAccount:
|
||||||
|
""" per-Account-instance hook specifications.
|
||||||
|
|
||||||
|
All hooks are executed in a dedicated Event thread.
|
||||||
|
Hooks are not allowed to block/last long as this
|
||||||
|
blocks overall event processing on the python side.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def _make_plugin_manager(cls):
|
||||||
|
pm = pluggy.PluginManager(account_spec_name)
|
||||||
|
pm.add_hookspecs(cls)
|
||||||
|
return pm
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
""" process a CFFI low level events for a given account.
|
||||||
|
|
||||||
|
ffi_event has "name", "data1", "data2" values as specified
|
||||||
|
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||||
|
|
||||||
|
DANGER: this hook is executed from the callback invoked by core.
|
||||||
|
Hook implementations need to be short running and can typically
|
||||||
|
not call back into core because this would easily cause recursion issues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_log_line(self, message):
|
||||||
|
""" log a message related to the account. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_configure_completed(self, success):
|
||||||
|
""" Called after a configure process completed. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
""" Called on any incoming message (to deaddrop or chat). """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_outgoing_message(self, message):
|
||||||
|
""" Called on each outgoing message (both system and "normal")."""
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_message_delivered(self, message):
|
||||||
|
""" Called when an outgoing message has been delivered to SMTP. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_chat_modified(self, chat):
|
||||||
|
""" Chat was created or modified regarding membership, avatar, title. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_member_added(self, chat, contact, message):
|
||||||
|
""" Called for each contact added to an accepted chat. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_member_removed(self, chat, contact, message):
|
||||||
|
""" Called for each contact removed from a chat. """
|
||||||
|
|
||||||
|
|
||||||
|
class Global:
|
||||||
|
""" global hook specifications using a per-process singleton
|
||||||
|
plugin manager instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_plugin_manager = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_plugin_manager(cls):
|
||||||
|
if cls._plugin_manager is None:
|
||||||
|
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
|
||||||
|
pm.add_hookspecs(cls)
|
||||||
|
return cls._plugin_manager
|
||||||
|
|
||||||
|
@global_hookspec
|
||||||
|
def dc_account_init(self, account):
|
||||||
|
""" called when `Account::__init__()` function starts executing. """
|
||||||
|
|
||||||
|
@global_hookspec
|
||||||
|
def dc_account_extra_configure(self, account):
|
||||||
|
""" Called when account configuration successfully finished.
|
||||||
|
|
||||||
|
This hook can be used to perform extra work before
|
||||||
|
ac_configure_completed is called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@global_hookspec
|
||||||
|
def dc_account_after_shutdown(self, account):
|
||||||
|
""" Called after the account has been shutdown. """
|
||||||
@@ -16,8 +16,7 @@ class Message(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, account, dc_msg):
|
def __init__(self, account, dc_msg):
|
||||||
self.account = account
|
self.account = account
|
||||||
self._dc_context = account._dc_context
|
assert isinstance(self.account._dc_context, ffi.CData)
|
||||||
assert isinstance(self._dc_context, ffi.CData)
|
|
||||||
assert isinstance(dc_msg, ffi.CData)
|
assert isinstance(dc_msg, ffi.CData)
|
||||||
assert dc_msg != ffi.NULL
|
assert dc_msg != ffi.NULL
|
||||||
self._dc_msg = dc_msg
|
self._dc_msg = dc_msg
|
||||||
@@ -28,7 +27,11 @@ class Message(object):
|
|||||||
return self.account == other.account and self.id == other.id
|
return self.account == other.account and self.id == other.id
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
|
c = self.get_sender_contact()
|
||||||
|
typ = "outgoing" if self.is_outgoing() else "incoming"
|
||||||
|
return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
|
||||||
|
typ, self.is_system_message(), repr(self.text[:10]),
|
||||||
|
self.id, c.id, c.addr, self.chat.id, self.chat.get_name())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_db(cls, account, id):
|
def from_db(cls, account, id):
|
||||||
@@ -50,6 +53,20 @@ class Message(object):
|
|||||||
lib.dc_msg_unref
|
lib.dc_msg_unref
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def create_chat(self):
|
||||||
|
""" create or get an existing chat (group) object for this message.
|
||||||
|
|
||||||
|
If the message is a deaddrop contact request
|
||||||
|
the sender will become an accepted contact.
|
||||||
|
|
||||||
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
|
"""
|
||||||
|
from .chat import Chat
|
||||||
|
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
|
||||||
|
ctx = self.account._dc_context
|
||||||
|
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
|
||||||
|
return Chat(self.account, chat_id)
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def text(self):
|
def text(self):
|
||||||
"""unicode text of this messages (might be empty if not a text message). """
|
"""unicode text of this messages (might be empty if not a text message). """
|
||||||
@@ -81,6 +98,10 @@ class Message(object):
|
|||||||
"""mime type of the file (if it exists)"""
|
"""mime type of the file (if it exists)"""
|
||||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||||
|
|
||||||
|
def is_system_message(self):
|
||||||
|
""" return True if this message is a system/info message. """
|
||||||
|
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||||
|
|
||||||
def is_setup_message(self):
|
def is_setup_message(self):
|
||||||
""" return True if this message is a setup message. """
|
""" return True if this message is a setup message. """
|
||||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||||
@@ -102,12 +123,12 @@ class Message(object):
|
|||||||
|
|
||||||
The text is multiline and may contain eg. the raw text of the message.
|
The text is multiline and may contain eg. the raw text of the message.
|
||||||
"""
|
"""
|
||||||
return from_dc_charpointer(lib.dc_get_msg_info(self._dc_context, self.id))
|
return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id))
|
||||||
|
|
||||||
def continue_key_transfer(self, setup_code):
|
def continue_key_transfer(self, setup_code):
|
||||||
""" extract key and use it as primary key for this account. """
|
""" extract key and use it as primary key for this account. """
|
||||||
res = lib.dc_continue_key_transfer(
|
res = lib.dc_continue_key_transfer(
|
||||||
self._dc_context,
|
self.account._dc_context,
|
||||||
self.id,
|
self.id,
|
||||||
as_dc_charpointer(setup_code)
|
as_dc_charpointer(setup_code)
|
||||||
)
|
)
|
||||||
@@ -142,7 +163,7 @@ class Message(object):
|
|||||||
:returns: email-mime message object (with headers only, no body).
|
:returns: email-mime message object (with headers only, no body).
|
||||||
"""
|
"""
|
||||||
import email.parser
|
import email.parser
|
||||||
mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id)
|
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
|
||||||
if mime_headers:
|
if mime_headers:
|
||||||
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
||||||
if isinstance(s, bytes):
|
if isinstance(s, bytes):
|
||||||
@@ -159,6 +180,13 @@ class Message(object):
|
|||||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||||
return Chat(self.account, chat_id)
|
return Chat(self.account, chat_id)
|
||||||
|
|
||||||
|
def get_sender_chat(self):
|
||||||
|
"""return the 1:1 chat with the sender of this message.
|
||||||
|
|
||||||
|
:returns: :class:`deltachat.chat.Chat` instance
|
||||||
|
"""
|
||||||
|
return self.get_sender_contact().get_chat()
|
||||||
|
|
||||||
def get_sender_contact(self):
|
def get_sender_contact(self):
|
||||||
"""return the contact of who wrote the message.
|
"""return the contact of who wrote the message.
|
||||||
|
|
||||||
@@ -166,7 +194,7 @@ class Message(object):
|
|||||||
"""
|
"""
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||||
return Contact(self._dc_context, contact_id)
|
return Contact(self.account, contact_id)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Message State query methods
|
# Message State query methods
|
||||||
@@ -178,7 +206,7 @@ class Message(object):
|
|||||||
else:
|
else:
|
||||||
# load message from db to get a fresh/current state
|
# load message from db to get a fresh/current state
|
||||||
dc_msg = ffi.gc(
|
dc_msg = ffi.gc(
|
||||||
lib.dc_get_msg(self._dc_context, self.id),
|
lib.dc_get_msg(self.account._dc_context, self.id),
|
||||||
lib.dc_msg_unref
|
lib.dc_msg_unref
|
||||||
)
|
)
|
||||||
return lib.dc_msg_get_state(dc_msg)
|
return lib.dc_msg_get_state(dc_msg)
|
||||||
@@ -207,6 +235,13 @@ class Message(object):
|
|||||||
"""
|
"""
|
||||||
return self._msgstate == const.DC_STATE_IN_SEEN
|
return self._msgstate == const.DC_STATE_IN_SEEN
|
||||||
|
|
||||||
|
def is_outgoing(self):
|
||||||
|
"""Return True if Message is outgoing. """
|
||||||
|
return self._msgstate in (
|
||||||
|
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING,
|
||||||
|
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD,
|
||||||
|
const.DC_STATE_OUT_DELIVERED)
|
||||||
|
|
||||||
def is_out_preparing(self):
|
def is_out_preparing(self):
|
||||||
"""Return True if Message is outgoing, but its file is being prepared.
|
"""Return True if Message is outgoing, but its file is being prepared.
|
||||||
"""
|
"""
|
||||||
@@ -269,6 +304,10 @@ class Message(object):
|
|||||||
""" return True if it's a file message. """
|
""" return True if it's a file message. """
|
||||||
return self._view_type == const.DC_MSG_FILE
|
return self._view_type == const.DC_MSG_FILE
|
||||||
|
|
||||||
|
def mark_seen(self):
|
||||||
|
""" mark this message as seen. """
|
||||||
|
self.account.mark_seen_messages([self.id])
|
||||||
|
|
||||||
|
|
||||||
# some code for handling DC_MSG_* view types
|
# some code for handling DC_MSG_* view types
|
||||||
|
|
||||||
@@ -288,3 +327,29 @@ def get_viewtype_code_from_name(view_type_name):
|
|||||||
return code
|
return code
|
||||||
raise ValueError("message typecode not found for {!r}, "
|
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.values())))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# some helper code for turning system messages into hook events
|
||||||
|
#
|
||||||
|
|
||||||
|
def map_system_message(msg):
|
||||||
|
if msg.is_system_message():
|
||||||
|
res = parse_system_add_remove(msg.text)
|
||||||
|
if res:
|
||||||
|
contact = msg.account.get_contact_by_addr(res[1])
|
||||||
|
if contact:
|
||||||
|
d = dict(chat=msg.chat, contact=contact, message=msg)
|
||||||
|
return "ac_member_" + res[0], d
|
||||||
|
|
||||||
|
|
||||||
|
def parse_system_add_remove(text):
|
||||||
|
# Member Me (x@y) removed by a@b.
|
||||||
|
# Member x@y removed by a@b
|
||||||
|
text = text.lower()
|
||||||
|
parts = text.split()
|
||||||
|
if parts[0] == "member":
|
||||||
|
if parts[2] in ("removed", "added"):
|
||||||
|
return parts[2], parts[1]
|
||||||
|
if parts[3] in ("removed", "added"):
|
||||||
|
return parts[3], parts[2].strip("()")
|
||||||
|
|||||||
@@ -11,27 +11,18 @@ class ProviderNotFoundError(Exception):
|
|||||||
class Provider(object):
|
class Provider(object):
|
||||||
"""Provider information.
|
"""Provider information.
|
||||||
|
|
||||||
:param domain: The domain to get the provider info for, this is
|
:param domain: The email to get the provider info for.
|
||||||
normally the part following the `@` of the domain.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, domain):
|
def __init__(self, account, addr):
|
||||||
provider = ffi.gc(
|
provider = ffi.gc(
|
||||||
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
|
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||||
lib.dc_provider_unref,
|
lib.dc_provider_unref,
|
||||||
)
|
)
|
||||||
if provider == ffi.NULL:
|
if provider == ffi.NULL:
|
||||||
raise ProviderNotFoundError("Provider not found")
|
raise ProviderNotFoundError("Provider not found")
|
||||||
self._provider = provider
|
self._provider = provider
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_email(cls, email):
|
|
||||||
"""Create provider info from an email address.
|
|
||||||
|
|
||||||
:param email: Email address to get provider info for.
|
|
||||||
"""
|
|
||||||
return cls(email.split('@')[-1])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def overview_page(self):
|
def overview_page(self):
|
||||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||||
@@ -39,21 +30,10 @@ class Provider(object):
|
|||||||
lib.dc_provider_get_overview_page(self._provider))
|
lib.dc_provider_get_overview_page(self._provider))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def get_before_login_hints(self):
|
||||||
"""The name of the provider."""
|
"""Should be shown to the user on login."""
|
||||||
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def markdown(self):
|
|
||||||
"""Content of the information page, formatted as markdown."""
|
|
||||||
return from_dc_charpointer(
|
return from_dc_charpointer(
|
||||||
lib.dc_provider_get_markdown(self._provider))
|
lib.dc_provider_get_before_login_hints(self._provider))
|
||||||
|
|
||||||
@property
|
|
||||||
def status_date(self):
|
|
||||||
"""The date the provider info was last updated, as a string."""
|
|
||||||
return from_dc_charpointer(
|
|
||||||
lib.dc_provider_get_status_date(self._provider))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
|
|||||||
507
python/src/deltachat/testplugin.py
Normal file
507
python/src/deltachat/testplugin.py
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import subprocess
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import fnmatch
|
||||||
|
import time
|
||||||
|
import weakref
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from . import Account, const
|
||||||
|
from .capi import lib
|
||||||
|
from .events import FFIEventLogger, FFIEventTracker
|
||||||
|
from _pytest._code import Source
|
||||||
|
from deltachat import direct_imap
|
||||||
|
|
||||||
|
import deltachat
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption(
|
||||||
|
"--liveconfig", action="store", default=None,
|
||||||
|
help="a file with >=2 lines where each line "
|
||||||
|
"contains NAME=VALUE config settings for one account"
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--ignored", action="store_true",
|
||||||
|
help="Also run tests marked with the ignored marker",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
cfg = config.getoption('--liveconfig')
|
||||||
|
if not cfg:
|
||||||
|
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||||
|
if cfg:
|
||||||
|
config.option.liveconfig = cfg
|
||||||
|
|
||||||
|
# Make sure we don't get garbled output because threads keep running
|
||||||
|
# collect all ever created accounts in a weakref-set (so we don't
|
||||||
|
# keep objects unneccessarily alive) and enable/disable logging
|
||||||
|
# for each pytest test phase # (setup/call/teardown).
|
||||||
|
# Additionally make the acfactory use a logging/no-logging default.
|
||||||
|
|
||||||
|
class LoggingAspect:
|
||||||
|
def __init__(self):
|
||||||
|
self._accounts = weakref.WeakSet()
|
||||||
|
|
||||||
|
@deltachat.global_hookimpl
|
||||||
|
def dc_account_init(self, account):
|
||||||
|
self._accounts.add(account)
|
||||||
|
|
||||||
|
def disable_logging(self, item):
|
||||||
|
for acc in self._accounts:
|
||||||
|
acc.disable_logging()
|
||||||
|
acfactory = item.funcargs.get("acfactory")
|
||||||
|
if acfactory:
|
||||||
|
acfactory.set_logging_default(False)
|
||||||
|
|
||||||
|
def enable_logging(self, item):
|
||||||
|
for acc in self._accounts:
|
||||||
|
acc.enable_logging()
|
||||||
|
acfactory = item.funcargs.get("acfactory")
|
||||||
|
if acfactory:
|
||||||
|
acfactory.set_logging_default(True)
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_setup(self, item):
|
||||||
|
if item.get_closest_marker("ignored"):
|
||||||
|
if not item.config.getvalue("ignored"):
|
||||||
|
pytest.skip("use --ignored to run this test")
|
||||||
|
self.enable_logging(item)
|
||||||
|
yield
|
||||||
|
self.disable_logging(item)
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_pyfunc_call(self, pyfuncitem):
|
||||||
|
self.enable_logging(pyfuncitem)
|
||||||
|
yield
|
||||||
|
self.disable_logging(pyfuncitem)
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_teardown(self, item):
|
||||||
|
self.enable_logging(item)
|
||||||
|
yield
|
||||||
|
self.disable_logging(item)
|
||||||
|
|
||||||
|
la = LoggingAspect()
|
||||||
|
config.pluginmanager.register(la)
|
||||||
|
deltachat.register_global_plugin(la)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config, startdir):
|
||||||
|
summary = []
|
||||||
|
|
||||||
|
t = tempfile.mktemp()
|
||||||
|
try:
|
||||||
|
ac = Account(t)
|
||||||
|
info = ac.get_info()
|
||||||
|
ac.shutdown()
|
||||||
|
finally:
|
||||||
|
os.remove(t)
|
||||||
|
summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format(
|
||||||
|
info['deltachat_core_version'],
|
||||||
|
info['sqlite_version'],
|
||||||
|
info['journal_mode'],
|
||||||
|
)])
|
||||||
|
|
||||||
|
cfg = config.option.liveconfig
|
||||||
|
if cfg:
|
||||||
|
if "?" in cfg:
|
||||||
|
url, token = cfg.split("?", 1)
|
||||||
|
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
|
||||||
|
else:
|
||||||
|
summary.append('Liveconfig file: {}'.format(cfg))
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
class SessionLiveConfigFromFile:
|
||||||
|
def __init__(self, fn):
|
||||||
|
self.fn = fn
|
||||||
|
self.configlist = []
|
||||||
|
for line in open(fn):
|
||||||
|
if line.strip() and not line.strip().startswith('#'):
|
||||||
|
d = {}
|
||||||
|
for part in line.split():
|
||||||
|
name, value = part.split("=")
|
||||||
|
d[name] = value
|
||||||
|
self.configlist.append(d)
|
||||||
|
|
||||||
|
def get(self, index):
|
||||||
|
return self.configlist[index]
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return bool(self.configlist)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionLiveConfigFromURL:
|
||||||
|
def __init__(self, url):
|
||||||
|
self.configlist = []
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def get(self, index):
|
||||||
|
try:
|
||||||
|
return self.configlist[index]
|
||||||
|
except IndexError:
|
||||||
|
assert index == len(self.configlist), index
|
||||||
|
res = requests.post(self.url)
|
||||||
|
if res.status_code != 200:
|
||||||
|
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||||
|
d = res.json()
|
||||||
|
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||||
|
self.configlist.append(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return bool(self.configlist)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def session_liveconfig(request):
|
||||||
|
liveconfig_opt = request.config.option.liveconfig
|
||||||
|
if liveconfig_opt:
|
||||||
|
if liveconfig_opt.startswith("http"):
|
||||||
|
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||||
|
else:
|
||||||
|
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data(request):
|
||||||
|
class Data:
|
||||||
|
def __init__(self):
|
||||||
|
# trying to find test data heuristically
|
||||||
|
# because we are run from a dev-setup with pytest direct,
|
||||||
|
# through tox, and then maybe also from deltachat-binding
|
||||||
|
# users like "deltabot".
|
||||||
|
self.paths = [os.path.normpath(x) for x in [
|
||||||
|
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data")
|
||||||
|
]]
|
||||||
|
|
||||||
|
def get_path(self, bn):
|
||||||
|
""" return path of file or None if it doesn't exist. """
|
||||||
|
for path in self.paths:
|
||||||
|
fn = os.path.join(path, *bn.split("/"))
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return fn
|
||||||
|
print("WARNING: path does not exist: {!r}".format(fn))
|
||||||
|
|
||||||
|
def read_path(self, bn, mode="r"):
|
||||||
|
fn = self.get_path(bn)
|
||||||
|
if fn is not None:
|
||||||
|
with open(fn, mode) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
return Data()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||||
|
|
||||||
|
class AccountMaker:
|
||||||
|
def __init__(self):
|
||||||
|
self.live_count = 0
|
||||||
|
self.offline_count = 0
|
||||||
|
self._finalizers = []
|
||||||
|
self._accounts = []
|
||||||
|
self.init_time = time.time()
|
||||||
|
self._generated_keys = ["alice", "bob", "charlie",
|
||||||
|
"dom", "elena", "fiona"]
|
||||||
|
self.set_logging_default(False)
|
||||||
|
deltachat.register_global_plugin(direct_imap)
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
while self._finalizers:
|
||||||
|
fin = self._finalizers.pop()
|
||||||
|
fin()
|
||||||
|
|
||||||
|
while self._accounts:
|
||||||
|
acc = self._accounts.pop()
|
||||||
|
acc.shutdown()
|
||||||
|
acc.disable_logging()
|
||||||
|
deltachat.unregister_global_plugin(direct_imap)
|
||||||
|
|
||||||
|
def make_account(self, path, logid, quiet=False):
|
||||||
|
ac = Account(path, logging=self._logging)
|
||||||
|
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||||
|
ac.addr = ac.get_self_contact().addr
|
||||||
|
ac.set_config("displayname", logid)
|
||||||
|
if not quiet:
|
||||||
|
ac.add_account_plugin(FFIEventLogger(ac))
|
||||||
|
self._accounts.append(ac)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def set_logging_default(self, logging):
|
||||||
|
self._logging = bool(logging)
|
||||||
|
|
||||||
|
def get_unconfigured_account(self):
|
||||||
|
self.offline_count += 1
|
||||||
|
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||||
|
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||||
|
ac._evtracker.init_time = self.init_time
|
||||||
|
ac._evtracker.set_timeout(2)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def _preconfigure_key(self, account, addr):
|
||||||
|
# Only set a key if we haven't used it yet for another account.
|
||||||
|
if self._generated_keys:
|
||||||
|
keyname = self._generated_keys.pop(0)
|
||||||
|
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
|
||||||
|
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
|
||||||
|
if fname_pub and fname_sec:
|
||||||
|
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||||
|
|
||||||
|
def get_configured_offline_account(self):
|
||||||
|
ac = self.get_unconfigured_account()
|
||||||
|
|
||||||
|
# do a pseudo-configured account
|
||||||
|
addr = "addr{}@offline.org".format(self.offline_count)
|
||||||
|
ac.set_config("addr", addr)
|
||||||
|
self._preconfigure_key(ac, addr)
|
||||||
|
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||||
|
ac.set_config("mail_pw", "123")
|
||||||
|
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||||
|
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def get_online_config(self, pre_generated_key=True, quiet=False):
|
||||||
|
if not session_liveconfig:
|
||||||
|
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||||
|
configdict = session_liveconfig.get(self.live_count)
|
||||||
|
self.live_count += 1
|
||||||
|
if "e2ee_enabled" not in configdict:
|
||||||
|
configdict["e2ee_enabled"] = "1"
|
||||||
|
|
||||||
|
# Enable strict certificate checks for online accounts
|
||||||
|
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||||
|
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||||
|
|
||||||
|
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||||
|
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
|
||||||
|
if pre_generated_key:
|
||||||
|
self._preconfigure_key(ac, configdict['addr'])
|
||||||
|
ac._evtracker.init_time = self.init_time
|
||||||
|
ac._evtracker.set_timeout(30)
|
||||||
|
return ac, dict(configdict)
|
||||||
|
|
||||||
|
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||||
|
pre_generated_key=True, quiet=False, config={}):
|
||||||
|
ac, configdict = self.get_online_config(
|
||||||
|
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||||
|
configdict.update(config)
|
||||||
|
configdict["mvbox_watch"] = str(int(mvbox))
|
||||||
|
configdict["mvbox_move"] = str(int(move))
|
||||||
|
configdict["sentbox_watch"] = str(int(sentbox))
|
||||||
|
ac.update_config(configdict)
|
||||||
|
ac._configtracker = ac.configure()
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||||
|
ac1 = self.get_online_configuring_account(
|
||||||
|
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||||
|
self.wait_configure_and_start_io()
|
||||||
|
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()
|
||||||
|
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()
|
||||||
|
for acc in accounts:
|
||||||
|
acc.add_account_plugin(FFIEventLogger(acc))
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
def clone_online_account(self, account, pre_generated_key=True):
|
||||||
|
self.live_count += 1
|
||||||
|
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||||
|
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||||
|
if pre_generated_key:
|
||||||
|
self._preconfigure_key(ac, account.get_config("addr"))
|
||||||
|
ac._evtracker.init_time = self.init_time
|
||||||
|
ac._evtracker.set_timeout(30)
|
||||||
|
ac.update_config(dict(
|
||||||
|
addr=account.get_config("addr"),
|
||||||
|
mail_pw=account.get_config("mail_pw"),
|
||||||
|
mvbox_watch=account.get_config("mvbox_watch"),
|
||||||
|
mvbox_move=account.get_config("mvbox_move"),
|
||||||
|
sentbox_watch=account.get_config("sentbox_watch"),
|
||||||
|
))
|
||||||
|
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 run_bot_process(self, module, ffi=True):
|
||||||
|
fn = module.__file__
|
||||||
|
|
||||||
|
bot_ac, bot_cfg = self.get_online_config()
|
||||||
|
|
||||||
|
# Avoid starting ac so we don't interfere with the bot operating on
|
||||||
|
# the same database.
|
||||||
|
self._accounts.remove(bot_ac)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
sys.executable,
|
||||||
|
"-u",
|
||||||
|
fn,
|
||||||
|
"--email", bot_cfg["addr"],
|
||||||
|
"--password", bot_cfg["mail_pw"],
|
||||||
|
bot_ac.db_path,
|
||||||
|
]
|
||||||
|
if ffi:
|
||||||
|
args.insert(-1, "--show-ffi")
|
||||||
|
print("$", " ".join(args))
|
||||||
|
popen = subprocess.Popen(
|
||||||
|
args=args,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
|
||||||
|
bufsize=0, # line buffering
|
||||||
|
close_fds=True, # close all FDs other than 0/1/2
|
||||||
|
universal_newlines=True # give back text
|
||||||
|
)
|
||||||
|
bot = BotProcess(popen, bot_cfg)
|
||||||
|
self._finalizers.append(bot.kill)
|
||||||
|
return bot
|
||||||
|
|
||||||
|
def dump_imap_summary(self, logfile):
|
||||||
|
for ac in self._accounts:
|
||||||
|
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):
|
||||||
|
ac2.create_chat(ac1)
|
||||||
|
return ac1.create_chat(ac2)
|
||||||
|
|
||||||
|
def introduce_each_other(self, accounts, sending=True):
|
||||||
|
to_wait = []
|
||||||
|
for i, acc in enumerate(accounts):
|
||||||
|
for acc2 in accounts[i + 1:]:
|
||||||
|
chat = self.get_accepted_chat(acc, acc2)
|
||||||
|
if sending:
|
||||||
|
chat.send_text("hi")
|
||||||
|
to_wait.append(acc2)
|
||||||
|
acc2.create_chat(acc).send_text("hi back")
|
||||||
|
to_wait.append(acc)
|
||||||
|
for acc in to_wait:
|
||||||
|
acc._evtracker.wait_next_incoming_message()
|
||||||
|
|
||||||
|
am = AccountMaker()
|
||||||
|
request.addfinalizer(am.finalize)
|
||||||
|
yield am
|
||||||
|
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||||
|
logfile = io.StringIO()
|
||||||
|
am.dump_imap_summary(logfile=logfile)
|
||||||
|
print(logfile.getvalue())
|
||||||
|
# request.node.add_report_section("call", "imap-server-state", s)
|
||||||
|
|
||||||
|
|
||||||
|
class BotProcess:
|
||||||
|
def __init__(self, popen, bot_cfg):
|
||||||
|
self.popen = popen
|
||||||
|
self.addr = bot_cfg["addr"]
|
||||||
|
|
||||||
|
# we read stdout as quickly as we can in a thread and make
|
||||||
|
# the (unicode) lines available for readers through a queue.
|
||||||
|
self.stdout_queue = queue.Queue()
|
||||||
|
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||||
|
t.setDaemon(1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _run_stdout_thread(self):
|
||||||
|
try:
|
||||||
|
while 1:
|
||||||
|
line = self.popen.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
line = line.strip()
|
||||||
|
self.stdout_queue.put(line)
|
||||||
|
print("bot-stdout: ", line)
|
||||||
|
finally:
|
||||||
|
self.stdout_queue.put(None)
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
self.popen.kill()
|
||||||
|
|
||||||
|
def wait(self, timeout=30):
|
||||||
|
self.popen.wait(timeout=timeout)
|
||||||
|
|
||||||
|
def fnmatch_lines(self, pattern_lines):
|
||||||
|
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()]
|
||||||
|
for next_pattern in patterns:
|
||||||
|
print("+++FNMATCH:", next_pattern)
|
||||||
|
ignored = []
|
||||||
|
while 1:
|
||||||
|
line = self.stdout_queue.get(timeout=15)
|
||||||
|
if line is None:
|
||||||
|
if ignored:
|
||||||
|
print("BOT stdout terminated after these lines")
|
||||||
|
for line in ignored:
|
||||||
|
print(line)
|
||||||
|
raise IOError("BOT stdout-thread terminated")
|
||||||
|
if fnmatch.fnmatch(line, next_pattern):
|
||||||
|
print("+++MATCHED:", line)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("+++IGN:", line)
|
||||||
|
ignored.append(line)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_db_path(tmpdir):
|
||||||
|
return tmpdir.join("test.db").strpath
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def lp():
|
||||||
|
class Printer:
|
||||||
|
def sec(self, msg):
|
||||||
|
print()
|
||||||
|
print("=" * 10, msg, "=" * 10)
|
||||||
|
|
||||||
|
def step(self, msg):
|
||||||
|
print("-" * 5, "step " + msg, "-" * 5)
|
||||||
|
|
||||||
|
return Printer()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
# execute all other hooks to obtain the report object
|
||||||
|
outcome = yield
|
||||||
|
rep = outcome.get_result()
|
||||||
|
|
||||||
|
# set a report attribute for each phase of a call, which can
|
||||||
|
# be "setup", "call", "teardown"
|
||||||
|
|
||||||
|
setattr(item, "rep_" + rep.when, rep)
|
||||||
90
python/src/deltachat/tracker.py
Normal file
90
python/src/deltachat/tracker.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
|
from .hookspec import account_hookimpl, Global
|
||||||
|
|
||||||
|
|
||||||
|
class ImexFailed(RuntimeError):
|
||||||
|
""" Exception for signalling that import/export operations failed."""
|
||||||
|
|
||||||
|
|
||||||
|
class ImexTracker:
|
||||||
|
def __init__(self):
|
||||||
|
self._imex_events = Queue()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
|
||||||
|
self._imex_events.put(ffi_event.data1)
|
||||||
|
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||||
|
self._imex_events.put(ffi_event.data2)
|
||||||
|
|
||||||
|
def wait_finish(self, progress_timeout=60):
|
||||||
|
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||||
|
files_written = []
|
||||||
|
while True:
|
||||||
|
ev = self._imex_events.get(timeout=progress_timeout)
|
||||||
|
if isinstance(ev, str):
|
||||||
|
files_written.append(ev)
|
||||||
|
elif ev == 0:
|
||||||
|
raise ImexFailed("export failed, exp-files: {}".format(files_written))
|
||||||
|
elif ev == 1000:
|
||||||
|
return files_written
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigureFailed(RuntimeError):
|
||||||
|
""" Exception for signalling that configuration failed."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigureTracker:
|
||||||
|
ConfigureFailed = ConfigureFailed
|
||||||
|
|
||||||
|
def __init__(self, account):
|
||||||
|
self.account = account
|
||||||
|
self._configure_events = Queue()
|
||||||
|
self._smtp_finished = Event()
|
||||||
|
self._imap_finished = Event()
|
||||||
|
self._ffi_events = []
|
||||||
|
self._progress = Queue()
|
||||||
|
self._gm = Global._get_plugin_manager()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
self._ffi_events.append(ffi_event)
|
||||||
|
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
|
||||||
|
self._smtp_finished.set()
|
||||||
|
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
|
||||||
|
self._imap_finished.set()
|
||||||
|
elif ffi_event.name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||||
|
self._progress.put(ffi_event.data1)
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_configure_completed(self, success):
|
||||||
|
if success:
|
||||||
|
self._gm.hook.dc_account_extra_configure(account=self.account)
|
||||||
|
self._configure_events.put(success)
|
||||||
|
self.account.remove_account_plugin(self)
|
||||||
|
|
||||||
|
def wait_smtp_connected(self):
|
||||||
|
""" wait until smtp is configured. """
|
||||||
|
self._smtp_finished.wait()
|
||||||
|
|
||||||
|
def wait_imap_connected(self):
|
||||||
|
""" wait until smtp is configured. """
|
||||||
|
self._imap_finished.wait()
|
||||||
|
|
||||||
|
def wait_progress(self, data1=None):
|
||||||
|
while 1:
|
||||||
|
evdata = self._progress.get()
|
||||||
|
if data1 is None or evdata == data1:
|
||||||
|
break
|
||||||
|
|
||||||
|
def wait_finish(self):
|
||||||
|
""" wait until configure is completed.
|
||||||
|
|
||||||
|
Raise Exception if Configure failed
|
||||||
|
"""
|
||||||
|
if not self._configure_events.get():
|
||||||
|
content = "\n".join(map(str, self._ffi_events))
|
||||||
|
raise ConfigureFailed(content)
|
||||||
@@ -10,4 +10,6 @@ if __name__ == "__main__":
|
|||||||
for relpath in os.listdir(workspacedir):
|
for relpath in os.listdir(workspacedir):
|
||||||
if relpath.startswith("deltachat"):
|
if relpath.startswith("deltachat"):
|
||||||
p = os.path.join(workspacedir, relpath)
|
p = os.path.join(workspacedir, relpath)
|
||||||
subprocess.check_call(["auditwheel", "repair", p, "-w", workspacedir])
|
subprocess.check_call(
|
||||||
|
["auditwheel", "repair", p, "-w", workspacedir,
|
||||||
|
"--plat", "manylinux2014_x86_64"])
|
||||||
|
|||||||
@@ -1,273 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from deltachat import Account
|
|
||||||
from deltachat import const
|
|
||||||
from deltachat.capi import lib
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
|
||||||
parser.addoption(
|
|
||||||
"--liveconfig", action="store", default=None,
|
|
||||||
help="a file with >=2 lines where each line "
|
|
||||||
"contains NAME=VALUE config settings for one account"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
|
||||||
cfg = config.getoption('--liveconfig')
|
|
||||||
if not cfg:
|
|
||||||
cfg = os.getenv('DCC_PY_LIVECONFIG')
|
|
||||||
if cfg:
|
|
||||||
config.option.liveconfig = cfg
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header(config, startdir):
|
|
||||||
summary = []
|
|
||||||
|
|
||||||
t = tempfile.mktemp()
|
|
||||||
try:
|
|
||||||
ac = Account(t, eventlogging=False)
|
|
||||||
info = ac.get_info()
|
|
||||||
ac.shutdown()
|
|
||||||
finally:
|
|
||||||
os.remove(t)
|
|
||||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
|
||||||
info['deltachat_core_version'],
|
|
||||||
info['sqlite_version'],
|
|
||||||
)])
|
|
||||||
|
|
||||||
cfg = config.option.liveconfig
|
|
||||||
if cfg:
|
|
||||||
if "#" in cfg:
|
|
||||||
url, token = cfg.split("#", 1)
|
|
||||||
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
|
|
||||||
else:
|
|
||||||
summary.append('Liveconfig file: {}'.format(cfg))
|
|
||||||
return summary
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def data():
|
|
||||||
class Data:
|
|
||||||
def __init__(self):
|
|
||||||
self.path = os.path.join(os.path.dirname(__file__), "data")
|
|
||||||
|
|
||||||
def get_path(self, bn):
|
|
||||||
fn = os.path.join(self.path, bn)
|
|
||||||
assert os.path.exists(fn)
|
|
||||||
return fn
|
|
||||||
return Data()
|
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromFile:
|
|
||||||
def __init__(self, fn):
|
|
||||||
self.fn = fn
|
|
||||||
self.configlist = []
|
|
||||||
for line in open(fn):
|
|
||||||
if line.strip() and not line.strip().startswith('#'):
|
|
||||||
d = {}
|
|
||||||
for part in line.split():
|
|
||||||
name, value = part.split("=")
|
|
||||||
d[name] = value
|
|
||||||
self.configlist.append(d)
|
|
||||||
|
|
||||||
def get(self, index):
|
|
||||||
return self.configlist[index]
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
return bool(self.configlist)
|
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromURL:
|
|
||||||
def __init__(self, url, create_token):
|
|
||||||
self.configlist = []
|
|
||||||
self.url = url
|
|
||||||
self.create_token = create_token
|
|
||||||
|
|
||||||
def get(self, index):
|
|
||||||
try:
|
|
||||||
return self.configlist[index]
|
|
||||||
except IndexError:
|
|
||||||
assert index == len(self.configlist), index
|
|
||||||
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
|
|
||||||
if res.status_code != 200:
|
|
||||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
|
||||||
d = res.json()
|
|
||||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
|
||||||
self.configlist.append(config)
|
|
||||||
return config
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
return bool(self.configlist)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def session_liveconfig(request):
|
|
||||||
liveconfig_opt = request.config.option.liveconfig
|
|
||||||
if liveconfig_opt:
|
|
||||||
if liveconfig_opt.startswith("http"):
|
|
||||||
url, create_token = liveconfig_opt.split("#", 1)
|
|
||||||
return SessionLiveConfigFromURL(url, create_token)
|
|
||||||
else:
|
|
||||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
|
||||||
|
|
||||||
class AccountMaker:
|
|
||||||
def __init__(self):
|
|
||||||
self.live_count = 0
|
|
||||||
self.offline_count = 0
|
|
||||||
self._finalizers = []
|
|
||||||
self.init_time = time.time()
|
|
||||||
|
|
||||||
def finalize(self):
|
|
||||||
while self._finalizers:
|
|
||||||
fin = self._finalizers.pop()
|
|
||||||
fin()
|
|
||||||
|
|
||||||
def make_account(self, path, logid):
|
|
||||||
ac = Account(path, logid=logid)
|
|
||||||
self._finalizers.append(ac.shutdown)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
def get_unconfigured_account(self):
|
|
||||||
self.offline_count += 1
|
|
||||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
|
||||||
ac._evlogger.init_time = self.init_time
|
|
||||||
ac._evlogger.set_timeout(2)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
def get_configured_offline_account(self):
|
|
||||||
ac = self.get_unconfigured_account()
|
|
||||||
|
|
||||||
# do a pseudo-configured account
|
|
||||||
addr = "addr{}@offline.org".format(self.offline_count)
|
|
||||||
ac.set_config("addr", addr)
|
|
||||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
|
||||||
ac.set_config("mail_pw", "123")
|
|
||||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
|
||||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
|
||||||
return ac
|
|
||||||
|
|
||||||
def peek_online_config(self):
|
|
||||||
if not session_liveconfig:
|
|
||||||
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
|
|
||||||
return session_liveconfig.get(self.live_count)
|
|
||||||
|
|
||||||
def get_online_config(self):
|
|
||||||
if not session_liveconfig:
|
|
||||||
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
|
|
||||||
configdict = session_liveconfig.get(self.live_count)
|
|
||||||
self.live_count += 1
|
|
||||||
if "e2ee_enabled" not in configdict:
|
|
||||||
configdict["e2ee_enabled"] = "1"
|
|
||||||
|
|
||||||
# Enable strict certificate checks for online accounts
|
|
||||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
|
||||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
|
||||||
|
|
||||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
|
||||||
ac._evlogger.init_time = self.init_time
|
|
||||||
ac._evlogger.set_timeout(30)
|
|
||||||
return ac, dict(configdict)
|
|
||||||
|
|
||||||
def get_online_configuring_account(self, mvbox=False, sentbox=False):
|
|
||||||
ac, configdict = self.get_online_config()
|
|
||||||
ac.configure(**configdict)
|
|
||||||
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
def get_one_online_account(self):
|
|
||||||
ac1 = self.get_online_configuring_account()
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
return ac1
|
|
||||||
|
|
||||||
def get_two_online_accounts(self):
|
|
||||||
ac1 = self.get_online_configuring_account()
|
|
||||||
ac2 = self.get_online_configuring_account()
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac2)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
return ac1, ac2
|
|
||||||
|
|
||||||
def clone_online_account(self, account):
|
|
||||||
self.live_count += 1
|
|
||||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
|
||||||
ac._evlogger.init_time = self.init_time
|
|
||||||
ac._evlogger.set_timeout(30)
|
|
||||||
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
|
||||||
ac.start_threads()
|
|
||||||
return ac
|
|
||||||
|
|
||||||
am = AccountMaker()
|
|
||||||
request.addfinalizer(am.finalize)
|
|
||||||
return am
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def tmp_db_path(tmpdir):
|
|
||||||
return tmpdir.join("test.db").strpath
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def lp():
|
|
||||||
class Printer:
|
|
||||||
def sec(self, msg):
|
|
||||||
print()
|
|
||||||
print("=" * 10, msg, "=" * 10)
|
|
||||||
|
|
||||||
def step(self, msg):
|
|
||||||
print("-" * 5, "step " + msg, "-" * 5)
|
|
||||||
return Printer()
|
|
||||||
|
|
||||||
|
|
||||||
def wait_configuration_progress(account, min_target, max_target=1001):
|
|
||||||
min_target = min(min_target, max_target)
|
|
||||||
while 1:
|
|
||||||
evt_name, data1, data2 = \
|
|
||||||
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
|
||||||
if data1 >= min_target and data1 <= max_target:
|
|
||||||
print("** CONFIG PROGRESS {}".format(min_target), account)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def wait_securejoin_inviter_progress(account, target):
|
|
||||||
while 1:
|
|
||||||
evt_name, data1, data2 = \
|
|
||||||
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
|
||||||
if data2 >= target:
|
|
||||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def wait_successful_IMAP_SMTP_connection(account):
|
|
||||||
imap_ok = smtp_ok = False
|
|
||||||
while not imap_ok or not smtp_ok:
|
|
||||||
evt_name, data1, data2 = \
|
|
||||||
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
|
|
||||||
if evt_name == "DC_EVENT_IMAP_CONNECTED":
|
|
||||||
imap_ok = True
|
|
||||||
print("** IMAP OK", account)
|
|
||||||
if evt_name == "DC_EVENT_SMTP_CONNECTED":
|
|
||||||
smtp_ok = True
|
|
||||||
print("** SMTP OK", account)
|
|
||||||
print("** IMAP and SMTP logins successful", account)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_msgs_changed(account, chat_id, msg_id=None):
|
|
||||||
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[1] == chat_id
|
|
||||||
if msg_id is not None:
|
|
||||||
assert ev[2] == msg_id
|
|
||||||
return ev[2]
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.7 KiB |
1
python/tests/data/key
Symbolic link
1
python/tests/data/key
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../test-data/key
|
||||||
135
python/tests/stress_test_db.py
Normal file
135
python/tests/stress_test_db.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
from queue import Queue, Empty
|
||||||
|
|
||||||
|
import deltachat
|
||||||
|
|
||||||
|
|
||||||
|
def test_db_busy_error(acfactory, tmpdir):
|
||||||
|
starttime = time.time()
|
||||||
|
log_lock = threading.RLock()
|
||||||
|
|
||||||
|
def log(string):
|
||||||
|
with log_lock:
|
||||||
|
print("%3.2f %s" % (time.time() - starttime, string))
|
||||||
|
|
||||||
|
# make a number of accounts
|
||||||
|
accounts = acfactory.get_many_online_accounts(3, quiet=True)
|
||||||
|
log("created %s accounts" % len(accounts))
|
||||||
|
|
||||||
|
# put a bigfile into each account
|
||||||
|
for acc in accounts:
|
||||||
|
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
|
||||||
|
with open(acc.bigfile, "wb") as f:
|
||||||
|
f.write(b"01234567890"*1000_000)
|
||||||
|
log("created %s bigfiles" % len(accounts))
|
||||||
|
|
||||||
|
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
|
||||||
|
chat = accounts[0].create_group_chat("stress-group")
|
||||||
|
for addr in contact_addrs[1:]:
|
||||||
|
chat.add_contact(chat.account.create_contact(addr))
|
||||||
|
|
||||||
|
# setup auto-responder bots which report back failures/actions
|
||||||
|
report_queue = Queue()
|
||||||
|
|
||||||
|
def report_func(replier, report_type, *report_args):
|
||||||
|
report_queue.put((replier, report_type, report_args))
|
||||||
|
|
||||||
|
# each replier receives all events and sends report events to receive_queue
|
||||||
|
repliers = []
|
||||||
|
for acc in accounts:
|
||||||
|
replier = AutoReplier(acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func)
|
||||||
|
acc.add_account_plugin(replier)
|
||||||
|
repliers.append(replier)
|
||||||
|
|
||||||
|
# kick off message sending
|
||||||
|
# after which repliers will reply to each other
|
||||||
|
chat.send_text("hello")
|
||||||
|
|
||||||
|
alive_count = len(accounts)
|
||||||
|
while alive_count > 0:
|
||||||
|
try:
|
||||||
|
replier, report_type, report_args = report_queue.get(timeout=10)
|
||||||
|
except Empty:
|
||||||
|
log("timeout waiting for next event")
|
||||||
|
pytest.fail("timeout exceeded")
|
||||||
|
if report_type == ReportType.exit:
|
||||||
|
replier.log("EXIT")
|
||||||
|
elif report_type == ReportType.ffi_error:
|
||||||
|
replier.log("ERROR: {}".format(report_args[0]))
|
||||||
|
elif report_type == ReportType.message_echo:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError("{} unknown report type {}, args={}".format(
|
||||||
|
addr, report_type, report_args
|
||||||
|
))
|
||||||
|
alive_count -= 1
|
||||||
|
replier.log("shutting down")
|
||||||
|
replier.account.shutdown()
|
||||||
|
replier.log("shut down complete, remaining={}".format(alive_count))
|
||||||
|
|
||||||
|
|
||||||
|
class ReportType:
|
||||||
|
exit = "exit"
|
||||||
|
ffi_error = "ffi-error"
|
||||||
|
message_echo = "message-echo"
|
||||||
|
|
||||||
|
|
||||||
|
class AutoReplier:
|
||||||
|
def __init__(self, account, log, num_send, num_bigfiles, report_func):
|
||||||
|
self.account = account
|
||||||
|
self._log = log
|
||||||
|
self.report_func = report_func
|
||||||
|
self.num_send = num_send
|
||||||
|
self.num_bigfiles = num_bigfiles
|
||||||
|
self.current_sent = 0
|
||||||
|
self.addr = self.account.get_self_contact().addr
|
||||||
|
|
||||||
|
self._thread = threading.Thread(
|
||||||
|
name="Stats{}".format(self.account),
|
||||||
|
target=self.thread_stats
|
||||||
|
)
|
||||||
|
self._thread.setDaemon(True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
self._log("{} {}".format(self.addr, message))
|
||||||
|
|
||||||
|
def thread_stats(self):
|
||||||
|
# XXX later use, for now we just quit
|
||||||
|
return
|
||||||
|
while 1:
|
||||||
|
time.sleep(1.0)
|
||||||
|
break
|
||||||
|
|
||||||
|
@deltachat.account_hookimpl
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
if self.current_sent >= self.num_send:
|
||||||
|
self.report_func(self, ReportType.exit)
|
||||||
|
return
|
||||||
|
message.create_chat()
|
||||||
|
message.mark_seen()
|
||||||
|
self.log("incoming message: {}".format(message))
|
||||||
|
|
||||||
|
self.current_sent += 1
|
||||||
|
# we are still alive, let's send a reply
|
||||||
|
if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0:
|
||||||
|
message.chat.send_text("send big file as reply to: {}".format(message.text))
|
||||||
|
msg = message.chat.send_file(self.account.bigfile)
|
||||||
|
else:
|
||||||
|
msg = message.chat.send_text("got message id {}, small text reply".format(message.id))
|
||||||
|
assert msg.text
|
||||||
|
self.log("message-sent: {}".format(msg))
|
||||||
|
self.report_func(self, ReportType.message_echo)
|
||||||
|
if self.current_sent >= self.num_send:
|
||||||
|
self.report_func(self, ReportType.exit)
|
||||||
|
return
|
||||||
|
|
||||||
|
@deltachat.account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
self.log(ffi_event)
|
||||||
|
if ffi_event.name == "DC_EVENT_ERROR":
|
||||||
|
self.report_func(self, ReportType.ffi_error, ffi_event)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,37 @@ import shutil
|
|||||||
import pytest
|
import pytest
|
||||||
from filecmp import cmp
|
from filecmp import cmp
|
||||||
|
|
||||||
from conftest import wait_configuration_progress, wait_msgs_changed
|
|
||||||
from deltachat import const
|
from deltachat import const
|
||||||
|
|
||||||
|
|
||||||
|
def wait_msg_delivered(account, msg_list):
|
||||||
|
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """
|
||||||
|
msg_list = list(msg_list)
|
||||||
|
while msg_list:
|
||||||
|
ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
msg_list.remove((ev.data1, ev.data2))
|
||||||
|
|
||||||
|
|
||||||
|
def wait_msgs_changed(account, msgs_list):
|
||||||
|
""" wait for one or more MSGS_CHANGED events to match msgs_list contents. """
|
||||||
|
account.log("waiting for msgs_list={}".format(msgs_list))
|
||||||
|
msgs_list = list(msgs_list)
|
||||||
|
while msgs_list:
|
||||||
|
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
for i, (data1, data2) in enumerate(msgs_list):
|
||||||
|
if ev.data1 == data1:
|
||||||
|
if data2 is None or ev.data2 == data2:
|
||||||
|
del msgs_list[i]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
account.log("waiting mismatch data1={} data2={}".format(data1, data2))
|
||||||
|
return ev.data1, ev.data2
|
||||||
|
|
||||||
|
|
||||||
class TestOnlineInCreation:
|
class TestOnlineInCreation:
|
||||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||||
ac2 = acfactory.get_online_configuring_account()
|
chat = ac1.create_chat(ac2)
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
|
|
||||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
|
||||||
chat = ac1.create_chat_by_contact(c2)
|
|
||||||
|
|
||||||
lp.sec("Creating in-creation file outside of blobdir")
|
lp.sec("Creating in-creation file outside of blobdir")
|
||||||
assert tmpdir.strpath != ac1.get_blobdir()
|
assert tmpdir.strpath != ac1.get_blobdir()
|
||||||
@@ -27,13 +45,8 @@ class TestOnlineInCreation:
|
|||||||
chat.prepare_message_file(src.strpath)
|
chat.prepare_message_file(src.strpath)
|
||||||
|
|
||||||
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||||
ac2 = acfactory.get_online_configuring_account()
|
chat = ac1.create_chat(ac2)
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
|
|
||||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
|
||||||
chat = ac1.create_chat_by_contact(c2)
|
|
||||||
|
|
||||||
lp.sec("Creating file outside of blobdir")
|
lp.sec("Creating file outside of blobdir")
|
||||||
assert tmpdir.strpath != ac1.get_blobdir()
|
assert tmpdir.strpath != ac1.get_blobdir()
|
||||||
@@ -45,15 +58,10 @@ class TestOnlineInCreation:
|
|||||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||||
|
|
||||||
def test_forward_increation(self, acfactory, data, lp):
|
def test_forward_increation(self, acfactory, data, lp):
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||||
ac2 = acfactory.get_online_configuring_account()
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
|
|
||||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
chat = ac1.create_chat(ac2)
|
||||||
chat = ac1.create_chat_by_contact(c2)
|
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
|
||||||
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
wait_msgs_changed(ac1, 0, 0) # why no chat id?
|
|
||||||
|
|
||||||
lp.sec("create a message with a file in creation")
|
lp.sec("create a message with a file in creation")
|
||||||
orig = data.get_path("d.png")
|
orig = data.get_path("d.png")
|
||||||
@@ -62,19 +70,16 @@ class TestOnlineInCreation:
|
|||||||
fp.write("preparing")
|
fp.write("preparing")
|
||||||
prepared_original = chat.prepare_message_file(path)
|
prepared_original = chat.prepare_message_file(path)
|
||||||
assert prepared_original.is_out_preparing()
|
assert prepared_original.is_out_preparing()
|
||||||
wait_msgs_changed(ac1, chat.id, prepared_original.id)
|
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
|
||||||
|
|
||||||
lp.sec("forward the message while still in creation")
|
lp.sec("forward the message while still in creation")
|
||||||
chat2 = ac1.create_group_chat("newgroup")
|
chat2 = ac1.create_group_chat("newgroup")
|
||||||
chat2.add_contact(c2)
|
chat2.add_contact(ac2)
|
||||||
wait_msgs_changed(ac1, 0, 0) # why not chat id?
|
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
|
||||||
ac1.forward_messages([prepared_original], chat2)
|
ac1.forward_messages([prepared_original], chat2)
|
||||||
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
|
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
|
||||||
# is the one caused by forwarding
|
# is the one caused by forwarding
|
||||||
forwarded_id = wait_msgs_changed(ac1, chat2.id)
|
_, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||||
if forwarded_id == 0:
|
|
||||||
forwarded_id = wait_msgs_changed(ac1, chat2.id)
|
|
||||||
assert forwarded_id
|
|
||||||
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
||||||
assert forwarded_msg.is_out_preparing()
|
assert forwarded_msg.is_out_preparing()
|
||||||
|
|
||||||
@@ -83,30 +88,28 @@ class TestOnlineInCreation:
|
|||||||
shutil.copyfile(orig, path)
|
shutil.copyfile(orig, path)
|
||||||
chat.send_prepared(prepared_original)
|
chat.send_prepared(prepared_original)
|
||||||
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
|
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
|
||||||
wait_msgs_changed(ac1, chat.id, prepared_original.id)
|
|
||||||
|
|
||||||
lp.sec("expect the forwarded message to be sent now too")
|
lp.sec("check that both forwarded and original message are proper.")
|
||||||
wait_msgs_changed(ac1, chat2.id, forwarded_id)
|
wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
|
||||||
|
|
||||||
fwd_msg = ac1.get_message_by_id(forwarded_id)
|
fwd_msg = ac1.get_message_by_id(forwarded_id)
|
||||||
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
||||||
|
|
||||||
lp.sec("wait for the messages to be delivered to SMTP")
|
lp.sec("wait for both messages to be delivered to SMTP")
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
wait_msg_delivered(ac1, [
|
||||||
assert ev[1] == chat.id
|
(chat2.id, forwarded_id),
|
||||||
assert ev[2] == prepared_original.id
|
(chat.id, prepared_original.id)
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
])
|
||||||
assert ev[1] == chat2.id
|
|
||||||
assert ev[2] == forwarded_id
|
|
||||||
|
|
||||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||||
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
received_original = ac2.get_message_by_id(ev1[2])
|
received_original = ac2.get_message_by_id(ev1.data2)
|
||||||
assert cmp(received_original.filename, orig, shallow=False)
|
assert cmp(received_original.filename, orig, shallow=False)
|
||||||
|
|
||||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||||
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
assert ev2[1] != ev1[1]
|
assert ev2.data1 != ev1.data1
|
||||||
received_copy = ac2.get_message_by_id(ev2[2])
|
received_copy = ac2.get_message_by_id(ev2.data2)
|
||||||
assert cmp(received_copy.filename, orig, shallow=False)
|
assert cmp(received_copy.filename, orig, shallow=False)
|
||||||
|
|||||||
@@ -1,72 +1,52 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
|
||||||
|
from queue import Queue
|
||||||
|
from deltachat import capi, cutil, const
|
||||||
|
from deltachat import register_global_plugin
|
||||||
|
from deltachat.hookspec import global_hookimpl
|
||||||
from deltachat.capi import ffi
|
from deltachat.capi import ffi
|
||||||
from deltachat.capi import lib
|
from deltachat.capi import lib
|
||||||
from deltachat.account import EventLogger
|
# from deltachat.account import EventLogger
|
||||||
|
|
||||||
|
|
||||||
def test_empty_context():
|
def test_empty_context():
|
||||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||||
capi.lib.dc_close(ctx)
|
capi.lib.dc_context_unref(ctx)
|
||||||
|
|
||||||
|
|
||||||
def test_callback_None2int():
|
def test_dc_close_events(tmpdir, acfactory):
|
||||||
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
set_context_callback(ctx, lambda *args: None)
|
|
||||||
capi.lib.dc_close(ctx)
|
|
||||||
clear_context_callback(ctx)
|
|
||||||
|
|
||||||
|
# register after_shutdown function
|
||||||
|
shutdowns = Queue()
|
||||||
|
|
||||||
def test_dc_close_events(tmpdir):
|
class ShutdownPlugin:
|
||||||
ctx = ffi.gc(
|
@global_hookimpl
|
||||||
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
def dc_account_after_shutdown(self, account):
|
||||||
lib.dc_context_unref,
|
assert account._dc_context is None
|
||||||
)
|
shutdowns.put(account)
|
||||||
evlog = EventLogger(ctx)
|
register_global_plugin(ShutdownPlugin())
|
||||||
evlog.set_timeout(5)
|
assert hasattr(ac1, "_dc_context")
|
||||||
set_context_callback(
|
ac1.shutdown()
|
||||||
ctx,
|
shutdowns.get(timeout=2)
|
||||||
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
|
|
||||||
)
|
|
||||||
p = tmpdir.join("hello.db")
|
|
||||||
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
|
|
||||||
capi.lib.dc_close(ctx)
|
|
||||||
|
|
||||||
def find(info_string):
|
|
||||||
while 1:
|
|
||||||
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
|
||||||
data2 = ev[2]
|
|
||||||
if info_string in data2:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print("skipping event", *ev)
|
|
||||||
|
|
||||||
find("disconnecting inbox-thread")
|
|
||||||
find("disconnecting sentbox-thread")
|
|
||||||
find("disconnecting mvbox-thread")
|
|
||||||
find("disconnecting SMTP")
|
|
||||||
find("Database closed")
|
|
||||||
|
|
||||||
|
|
||||||
def test_wrong_db(tmpdir):
|
def test_wrong_db(tmpdir):
|
||||||
dc_context = ffi.gc(
|
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
p = tmpdir.join("hello.db")
|
p = tmpdir.join("hello.db")
|
||||||
# write an invalid database file
|
# write an invalid database file
|
||||||
p.write("x123" * 10)
|
p.write("x123" * 10)
|
||||||
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
|
|
||||||
|
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_blobdir(tmpdir):
|
def test_empty_blobdir(tmpdir):
|
||||||
|
db_fname = tmpdir.join("hello.db")
|
||||||
# Apparently some client code expects this to be the same as passing NULL.
|
# Apparently some client code expects this to be the same as passing NULL.
|
||||||
ctx = ffi.gc(
|
ctx = ffi.gc(
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
|
||||||
lib.dc_context_unref,
|
lib.dc_context_unref,
|
||||||
)
|
)
|
||||||
db_fname = tmpdir.join("hello.db")
|
assert ctx != ffi.NULL
|
||||||
assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"")
|
|
||||||
|
|
||||||
|
|
||||||
def test_event_defines():
|
def test_event_defines():
|
||||||
@@ -75,24 +55,27 @@ def test_event_defines():
|
|||||||
|
|
||||||
|
|
||||||
def test_sig():
|
def test_sig():
|
||||||
sig = capi.lib.dc_get_event_signature_types
|
sig = capi.lib.dc_event_has_string_data
|
||||||
assert sig(const.DC_EVENT_INFO) == 2
|
assert not sig(const.DC_EVENT_MSGS_CHANGED)
|
||||||
assert sig(const.DC_EVENT_WARNING) == 2
|
assert sig(const.DC_EVENT_INFO)
|
||||||
assert sig(const.DC_EVENT_ERROR) == 2
|
assert sig(const.DC_EVENT_WARNING)
|
||||||
assert sig(const.DC_EVENT_SMTP_CONNECTED) == 2
|
assert sig(const.DC_EVENT_ERROR)
|
||||||
assert sig(const.DC_EVENT_IMAP_CONNECTED) == 2
|
assert sig(const.DC_EVENT_SMTP_CONNECTED)
|
||||||
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) == 2
|
assert sig(const.DC_EVENT_IMAP_CONNECTED)
|
||||||
|
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
|
||||||
|
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
|
||||||
|
|
||||||
|
|
||||||
def test_markseen_invalid_message_ids(acfactory):
|
def test_markseen_invalid_message_ids(acfactory):
|
||||||
ac1 = acfactory.get_configured_offline_account()
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
|
|
||||||
chat = ac1.create_chat_by_contact(contact1)
|
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||||
|
chat = contact1.create_chat()
|
||||||
chat.send_text("one messae")
|
chat.send_text("one messae")
|
||||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
msg_ids = [9]
|
msg_ids = [9]
|
||||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||||
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||||
|
|
||||||
|
|
||||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||||
@@ -102,56 +85,20 @@ def test_get_special_message_id_returns_empty_message(acfactory):
|
|||||||
assert msg.id == 0
|
assert msg.id == 0
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info():
|
|
||||||
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
|
|
||||||
assert cutil.from_dc_charpointer(
|
|
||||||
lib.dc_provider_get_overview_page(provider)
|
|
||||||
) == "https://providers.delta.chat/example.com"
|
|
||||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
|
|
||||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
|
|
||||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
|
|
||||||
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_none():
|
def test_provider_info_none():
|
||||||
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_info_closed():
|
|
||||||
ctx = ffi.gc(
|
ctx = ffi.gc(
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
|
||||||
lib.dc_context_unref,
|
lib.dc_context_unref,
|
||||||
)
|
)
|
||||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||||
assert 'deltachat_core_version' in info
|
|
||||||
assert 'database_dir' not in info
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_info_open(tmpdir):
|
def test_get_info_open(tmpdir):
|
||||||
|
db_fname = tmpdir.join("test.db")
|
||||||
ctx = ffi.gc(
|
ctx = ffi.gc(
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
|
||||||
lib.dc_context_unref,
|
lib.dc_context_unref,
|
||||||
)
|
)
|
||||||
db_fname = tmpdir.join("test.db")
|
|
||||||
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
|
|
||||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||||
assert 'deltachat_core_version' in info
|
assert 'deltachat_core_version' in info
|
||||||
assert 'database_dir' in info
|
assert 'database_dir' in info
|
||||||
|
|
||||||
|
|
||||||
def test_is_open_closed():
|
|
||||||
ctx = ffi.gc(
|
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
assert lib.dc_is_open(ctx) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_open_actually_open(tmpdir):
|
|
||||||
ctx = ffi.gc(
|
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
db_fname = tmpdir.join("test.db")
|
|
||||||
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
|
|
||||||
assert lib.dc_is_open(ctx) == 1
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from deltachat import const
|
|
||||||
from deltachat import provider
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_from_email():
|
|
||||||
example = provider.Provider.from_email("email@example.com")
|
|
||||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
|
||||||
assert example.name == "Example"
|
|
||||||
assert example.markdown == "\n..."
|
|
||||||
assert example.status_date == "2018-09"
|
|
||||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_from_domain():
|
|
||||||
example = provider.Provider("example.com")
|
|
||||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
|
||||||
assert example.name == "Example"
|
|
||||||
assert example.markdown == "\n..."
|
|
||||||
assert example.status_date == "2018-09"
|
|
||||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_none():
|
|
||||||
with pytest.raises(provider.ProviderNotFoundError):
|
|
||||||
provider.Provider.from_email("email@unexistent.no")
|
|
||||||
@@ -7,13 +7,14 @@ envlist =
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
|
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
|
||||||
# python tests/package_wheels.py {toxworkdir}/wheelhouse
|
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||||
passenv =
|
passenv =
|
||||||
TRAVIS
|
TRAVIS
|
||||||
DCC_RS_DEV
|
DCC_RS_DEV
|
||||||
DCC_RS_TARGET
|
DCC_RS_TARGET
|
||||||
DCC_PY_LIVECONFIG
|
DCC_PY_LIVECONFIG
|
||||||
|
DCC_NEW_TMP_EMAIL
|
||||||
CARGO_TARGET_DIR
|
CARGO_TARGET_DIR
|
||||||
RUSTC_WRAPPER
|
RUSTC_WRAPPER
|
||||||
deps =
|
deps =
|
||||||
@@ -40,13 +41,13 @@ deps =
|
|||||||
restructuredtext_lint
|
restructuredtext_lint
|
||||||
commands =
|
commands =
|
||||||
flake8 src/deltachat
|
flake8 src/deltachat
|
||||||
flake8 tests/
|
flake8 tests/ examples/
|
||||||
rst-lint --encoding 'utf-8' README.rst
|
rst-lint --encoding 'utf-8' README.rst
|
||||||
|
|
||||||
[testenv:doc]
|
[testenv:doc]
|
||||||
changedir=doc
|
changedir=doc
|
||||||
deps =
|
deps =
|
||||||
sphinx==2.2.0
|
sphinx
|
||||||
breathe
|
breathe
|
||||||
commands =
|
commands =
|
||||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||||
@@ -65,12 +66,13 @@ commands =
|
|||||||
|
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = -v -ra
|
addopts = -v -ra --strict-markers
|
||||||
python_files = tests/test_*.py
|
|
||||||
norecursedirs = .tox
|
norecursedirs = .tox
|
||||||
xfail_strict=true
|
xfail_strict=true
|
||||||
timeout = 60
|
timeout = 90
|
||||||
timeout_method = thread
|
timeout_method = thread
|
||||||
|
markers =
|
||||||
|
ignored: ignore this test in default test runs, use --ignored to run.
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
nightly-2019-11-06
|
1.43.1
|
||||||
|
|||||||
80
scripts/proxy.py
Normal file
80
scripts/proxy.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Examples:
|
||||||
|
#
|
||||||
|
# Original server that doesn't use SSL:
|
||||||
|
# ./proxy.py 8080 imap.nauta.cu 143
|
||||||
|
# ./proxy.py 8081 smtp.nauta.cu 25
|
||||||
|
#
|
||||||
|
# Original server that uses SSL:
|
||||||
|
# ./proxy.py 8080 testrun.org 993 --ssl
|
||||||
|
# ./proxy.py 8081 testrun.org 465 --ssl
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import argparse
|
||||||
|
import selectors
|
||||||
|
import ssl
|
||||||
|
import socket
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(socketserver.ThreadingTCPServer):
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
|
||||||
|
self.real_host = real_host
|
||||||
|
self.real_port = real_port
|
||||||
|
self.use_ssl = use_ssl
|
||||||
|
super().__init__((proxy_host, proxy_port), RequestHandler)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(socketserver.BaseRequestHandler):
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
real_server = (self.server.real_host, self.server.real_port)
|
||||||
|
with socket.create_connection(real_server) as sock:
|
||||||
|
if self.server.use_ssl:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
sock = context.wrap_socket(
|
||||||
|
sock, server_hostname=real_server[0])
|
||||||
|
|
||||||
|
forward = {self.request: sock, sock: self.request}
|
||||||
|
|
||||||
|
sel = selectors.DefaultSelector()
|
||||||
|
sel.register(self.request, selectors.EVENT_READ,
|
||||||
|
self.client_address)
|
||||||
|
sel.register(sock, selectors.EVENT_READ, real_server)
|
||||||
|
|
||||||
|
active = True
|
||||||
|
while active:
|
||||||
|
events = sel.select()
|
||||||
|
for key, mask in events:
|
||||||
|
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
|
||||||
|
data = key.fileobj.recv(1024)
|
||||||
|
received = len(data)
|
||||||
|
total += received
|
||||||
|
print(data)
|
||||||
|
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
|
||||||
|
if data:
|
||||||
|
forward[key.fileobj].sendall(data)
|
||||||
|
else:
|
||||||
|
print('\nCLOSING CONNECTION.\n\n')
|
||||||
|
forward[key.fileobj].close()
|
||||||
|
key.fileobj.close()
|
||||||
|
active = False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
p = argparse.ArgumentParser(description='Simple Python Proxy')
|
||||||
|
p.add_argument(
|
||||||
|
"proxy_port", help="the port where the proxy will listen", type=int)
|
||||||
|
p.add_argument('host', help="the real host")
|
||||||
|
p.add_argument('port', help="the port of the real host", type=int)
|
||||||
|
p.add_argument("--ssl", help="use ssl to connect to the real host",
|
||||||
|
action="store_true")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
|
||||||
|
proxy.serve_forever()
|
||||||
@@ -35,7 +35,7 @@ if __name__ == "__main__":
|
|||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
||||||
print("{}: {}".format(x, read_toml_version(x)))
|
print("{}: {}".format(x, read_toml_version(x)))
|
||||||
raise SystemExit("need argument: new version, example 1.0.0-beta.27")
|
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||||
newversion = sys.argv[1]
|
newversion = sys.argv[1]
|
||||||
if newversion.count(".") < 2:
|
if newversion.count(".") < 2:
|
||||||
raise SystemExit("need at least two dots in version")
|
raise SystemExit("need at least two dots in version")
|
||||||
@@ -45,7 +45,7 @@ if __name__ == "__main__":
|
|||||||
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
||||||
|
|
||||||
for line in open("CHANGELOG.md"):
|
for line in open("CHANGELOG.md"):
|
||||||
## 1.0.0-beta5
|
## 1.25.0
|
||||||
if line.startswith("## "):
|
if line.startswith("## "):
|
||||||
if line[2:].strip().startswith(newversion):
|
if line[2:].strip().startswith(newversion):
|
||||||
break
|
break
|
||||||
@@ -55,6 +55,7 @@ if __name__ == "__main__":
|
|||||||
replace_toml_version("Cargo.toml", newversion)
|
replace_toml_version("Cargo.toml", newversion)
|
||||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||||
|
|
||||||
|
subprocess.call(["cargo", "check"])
|
||||||
subprocess.call(["git", "add", "-u"])
|
subprocess.call(["git", "add", "-u"])
|
||||||
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||||
|
|
||||||
|
|||||||
119
spec.md
119
spec.md
@@ -1,10 +1,12 @@
|
|||||||
# Chat-over-Email specification
|
# chat-mail specification
|
||||||
|
|
||||||
Version 0.20.0
|
Version: 0.32.0
|
||||||
|
Status: In-progress
|
||||||
|
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||||
|
|
||||||
This document describes how emails can be used
|
This document roughly describes how chat-mail
|
||||||
to implement typical messenger functions
|
apps use the standard e-mail system
|
||||||
while staying compatible to existing MUAs.
|
to implement typical messenger functions.
|
||||||
|
|
||||||
- [Encryption](#encryption)
|
- [Encryption](#encryption)
|
||||||
- [Outgoing messages](#outgoing-messages)
|
- [Outgoing messages](#outgoing-messages)
|
||||||
@@ -17,6 +19,9 @@ while staying compatible to existing MUAs.
|
|||||||
- [Change group name](#change-group-name)
|
- [Change group name](#change-group-name)
|
||||||
- [Set group image](#set-group-image)
|
- [Set group image](#set-group-image)
|
||||||
- [Set profile image](#set-profile-image)
|
- [Set profile image](#set-profile-image)
|
||||||
|
- [Locations](#locations)
|
||||||
|
- [User locations](#user-locations)
|
||||||
|
- [Points of interest](#points-of-interest)
|
||||||
- [Miscellaneous](#miscellaneous)
|
- [Miscellaneous](#miscellaneous)
|
||||||
|
|
||||||
|
|
||||||
@@ -27,18 +32,14 @@ Messages SHOULD be encrypted by the
|
|||||||
`prefer-encrypt=mutual` MAY be set by default.
|
`prefer-encrypt=mutual` MAY be set by default.
|
||||||
|
|
||||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
||||||
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
|
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||||
If Memoryhole is not used,
|
|
||||||
the subject of encrypted messages SHOULD be replaced by the string
|
|
||||||
`Chat: Encrypted message` where the part after the colon MAY be localized.
|
|
||||||
|
|
||||||
|
|
||||||
# Outgoing messages
|
# Outgoing messages
|
||||||
|
|
||||||
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
|
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
|
||||||
For filtering and smart appearance of the messages in normal MUAs,
|
For filtering and smart appearance of the messages in normal MUAs,
|
||||||
the `Subject` header SHOULD start with the characters `Chat:`
|
the `Subject` header SHOULD be `Message from <sender name>`.
|
||||||
and SHOULD be an excerpt of the message.
|
|
||||||
Replies to messages MAY follow the typical `Re:`-format.
|
Replies to messages MAY follow the typical `Re:`-format.
|
||||||
|
|
||||||
The body MAY contain text which MUST have the content type `text/plain`
|
The body MAY contain text which MUST have the content type `text/plain`
|
||||||
@@ -56,7 +57,7 @@ Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
|
|||||||
To: rcpt@domain
|
To: rcpt@domain
|
||||||
Chat-Version: 1.0
|
Chat-Version: 1.0
|
||||||
Content-Type: text/plain
|
Content-Type: text/plain
|
||||||
Subject: Chat: Hello ...
|
Subject: Message from sender@domain
|
||||||
|
|
||||||
Hello world!
|
Hello world!
|
||||||
|
|
||||||
@@ -113,6 +114,7 @@ but MUAs typically expose the sender in the UI.
|
|||||||
Groups are chats with usually more than one recipient,
|
Groups are chats with usually more than one recipient,
|
||||||
each defined by an email-address.
|
each defined by an email-address.
|
||||||
The sender plus the recipients are the group members.
|
The sender plus the recipients are the group members.
|
||||||
|
All group members form the member list.
|
||||||
|
|
||||||
To allow different groups with the same members,
|
To allow different groups with the same members,
|
||||||
groups are identified by a group-id.
|
groups are identified by a group-id.
|
||||||
@@ -135,8 +137,7 @@ The group-name MUST be written to `Chat-Group-Name` header
|
|||||||
to join a group chat on a second device any time).
|
to join a group chat on a second device any time).
|
||||||
|
|
||||||
The `Subject` header of outgoing group messages
|
The `Subject` header of outgoing group messages
|
||||||
SHOULD start with the characters `Chat:`
|
SHOULD be set to the group-name.
|
||||||
followed by the group-name and a colon followed by an excerpt of the message.
|
|
||||||
|
|
||||||
To identify the group-id on replies from normal MUAs,
|
To identify the group-id on replies from normal MUAs,
|
||||||
the group-id MUST also be added to the message-id of outgoing messages.
|
the group-id MUST also be added to the message-id of outgoing messages.
|
||||||
@@ -153,8 +154,8 @@ The message-id MUST have the format `Gr.<group-id>.<unique data>`.
|
|||||||
Hello group - this group contains three members
|
Hello group - this group contains three members
|
||||||
|
|
||||||
Messengers adding the member list in the form `Name <email-address>`
|
Messengers adding the member list in the form `Name <email-address>`
|
||||||
MUST take care only to spread the names authorized by the contacts themselves.
|
MUST take care only to distribute the names authorized by the contacts themselves.
|
||||||
Otherwise, names as _Daddy_ or _Honey_ may be spread
|
Otherwise, names as _Daddy_ or _Honey_ may be distributed
|
||||||
(this issue is also true for normal MUAs, however,
|
(this issue is also true for normal MUAs, however,
|
||||||
for more contact- and chat-centralized apps
|
for more contact- and chat-centralized apps
|
||||||
such situations happen more frequently).
|
such situations happen more frequently).
|
||||||
@@ -177,12 +178,22 @@ to a normal single-user chat with the email-address given in `From`.
|
|||||||
|
|
||||||
## Add and remove members
|
## Add and remove members
|
||||||
|
|
||||||
Messenger clients MUST construct the member list
|
Messenger clients MUST init the member list
|
||||||
from the `From`/`To` headers only on the first group message
|
from the `From`/`To` headers on the first group message.
|
||||||
or if they see a `Chat-Group-Member-Added`
|
|
||||||
or `Chat-Group-Member-Removed` action header.
|
When a member is added later,
|
||||||
Both headers MUST have the email-address
|
a `Chat-Group-Member-Added` action header must be sent
|
||||||
of the added or removed member as the value.
|
with the value set to the email-address of the added member.
|
||||||
|
When receiving a `Chat-Group-Member-Added` header, however,
|
||||||
|
_all missing_ members the `From`/`To` headers has to be added.
|
||||||
|
This is to mitigate problems when receiving messages
|
||||||
|
in different orders, esp. on creating new groups.
|
||||||
|
|
||||||
|
To remove a member, a `Chat-Group-Member-Removed` header must be sent
|
||||||
|
with the value set to the email-address of the member to remove.
|
||||||
|
When receiving a `Chat-Group-Member-Removed` header,
|
||||||
|
only exaxtly the given member has to be removed from the member list.
|
||||||
|
|
||||||
Messenger clients MUST NOT construct the member list
|
Messenger clients MUST NOT construct the member list
|
||||||
on other group messages
|
on other group messages
|
||||||
(this is to avoid accidentally altered To-lists in normal MUAs;
|
(this is to avoid accidentally altered To-lists in normal MUAs;
|
||||||
@@ -288,7 +299,7 @@ to add a `Chat-Group-Avatar` only on image changes.
|
|||||||
|
|
||||||
# Set profile image
|
# Set profile image
|
||||||
|
|
||||||
A user MAY have a profile-image that MAY be spread to their contacts.
|
A user MAY have a profile-image that MAY be distributed to their contacts.
|
||||||
To change or set the profile-image,
|
To change or set the profile-image,
|
||||||
the messenger MUST attach an image file to a message
|
the messenger MUST attach an image file to a message
|
||||||
and MUST add the header `Chat-User-Avatar`
|
and MUST add the header `Chat-User-Avatar`
|
||||||
@@ -297,7 +308,7 @@ with the value set to the image name.
|
|||||||
To remove the profile-image,
|
To remove the profile-image,
|
||||||
the messenger MUST add the header `Chat-User-Avatar: 0`.
|
the messenger MUST add the header `Chat-User-Avatar: 0`.
|
||||||
|
|
||||||
To spread the image,
|
To distribute the image,
|
||||||
the messenger MAY send the profile image
|
the messenger MAY send the profile image
|
||||||
together with the next mail to a given contact
|
together with the next mail to a given contact
|
||||||
(to do this only once,
|
(to do this only once,
|
||||||
@@ -332,6 +343,64 @@ To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
|
|||||||
only on image changes.
|
only on image changes.
|
||||||
|
|
||||||
|
|
||||||
|
# Locations
|
||||||
|
|
||||||
|
Locations can be attachted to messages using
|
||||||
|
[standard kml-files](https://www.opengeospatial.org/standards/kml/)
|
||||||
|
with well-known names.
|
||||||
|
|
||||||
|
|
||||||
|
## User locations
|
||||||
|
|
||||||
|
To send the location of the sender,
|
||||||
|
the app can attach a file with the name `location.kml`.
|
||||||
|
The file can contain one or more locations.
|
||||||
|
Apps that support location streaming will typically collect some location events
|
||||||
|
and send them together in one file.
|
||||||
|
As each location has an independent timestamp,
|
||||||
|
the apps can show the location as a track.
|
||||||
|
|
||||||
|
Note that the `addr` attribute inside the `location.kml` file
|
||||||
|
MUST match the users email-address.
|
||||||
|
Otherwise, the file is discarded silently;
|
||||||
|
this is to protect against getting wrong locations,
|
||||||
|
eg. forwarded from a normal MUA.
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||||
|
<Document addr="ndh@deltachat.de">
|
||||||
|
<Placemark>
|
||||||
|
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
|
||||||
|
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
|
||||||
|
</Placemark>
|
||||||
|
<Placemark>
|
||||||
|
<Timestamp><when>2020-01-11T20:40:25Z</when></Timestamp>
|
||||||
|
<Point><coordinates accuracy="5.4">7.654,3.21</coordinates></Point>
|
||||||
|
</Placemark>
|
||||||
|
</Document>
|
||||||
|
</kml>
|
||||||
|
|
||||||
|
|
||||||
|
## Points of interest
|
||||||
|
|
||||||
|
To send an "Point of interest", a POI,
|
||||||
|
use a normal message and attach a file with the name `message.kml`.
|
||||||
|
In contrast to user locations, this file should contain only one location
|
||||||
|
and an address-attribute is not needed -
|
||||||
|
as the location belongs to the message content,
|
||||||
|
it is fine if the location is detected on forwarding etc.
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||||
|
<Document>
|
||||||
|
<Placemark>
|
||||||
|
<Timestamp><when>2020-01-01T20:40:19Z</when></Timestamp>
|
||||||
|
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
|
||||||
|
</Placemark>
|
||||||
|
</Document>
|
||||||
|
</kml>
|
||||||
|
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
|
|
||||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||||
@@ -368,4 +437,4 @@ as the sending time of the message as indicated by its Date header,
|
|||||||
or the time of first receipt if that date is in the future or unavailable.
|
or the time of first receipt if that date is in the future or unavailable.
|
||||||
|
|
||||||
|
|
||||||
Copyright © 2017-2019 Delta Chat contributors.
|
Copyright © 2017-2020 Delta Chat contributors.
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ use std::collections::BTreeMap;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fmt, str};
|
use std::{fmt, str};
|
||||||
|
|
||||||
use crate::constants::*;
|
use deltachat_derive::*;
|
||||||
|
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::key::*;
|
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||||
|
use crate::key::{DcKey, SignedPublicKey};
|
||||||
|
|
||||||
/// Possible values for encryption preference
|
/// Possible values for encryption preference
|
||||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
|
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive, Sqlx)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum EncryptPreference {
|
pub enum EncryptPreference {
|
||||||
NoPreference = 0,
|
NoPreference = 0,
|
||||||
@@ -52,13 +54,17 @@ impl str::FromStr for EncryptPreference {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Aheader {
|
pub struct Aheader {
|
||||||
pub addr: String,
|
pub addr: String,
|
||||||
pub public_key: Key,
|
pub public_key: SignedPublicKey,
|
||||||
pub prefer_encrypt: EncryptPreference,
|
pub prefer_encrypt: EncryptPreference,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Aheader {
|
impl Aheader {
|
||||||
/// Creates new autocrypt header
|
/// Creates new autocrypt header
|
||||||
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
|
pub fn new(
|
||||||
|
addr: String,
|
||||||
|
public_key: SignedPublicKey,
|
||||||
|
prefer_encrypt: EncryptPreference,
|
||||||
|
) -> Self {
|
||||||
Aheader {
|
Aheader {
|
||||||
addr,
|
addr,
|
||||||
public_key,
|
public_key,
|
||||||
@@ -71,9 +77,7 @@ impl Aheader {
|
|||||||
wanted_from: &str,
|
wanted_from: &str,
|
||||||
headers: &[mailparse::MailHeader<'_>],
|
headers: &[mailparse::MailHeader<'_>],
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
use mailparse::MailHeaderMap;
|
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||||
|
|
||||||
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
|
|
||||||
match Self::from_str(&value) {
|
match Self::from_str(&value) {
|
||||||
Ok(header) => {
|
Ok(header) => {
|
||||||
if addr_cmp(&header.addr, wanted_from) {
|
if addr_cmp(&header.addr, wanted_from) {
|
||||||
@@ -125,14 +129,10 @@ impl str::FromStr for Aheader {
|
|||||||
.split(';')
|
.split(';')
|
||||||
.filter_map(|a| {
|
.filter_map(|a| {
|
||||||
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
||||||
if attribute.len() < 2 {
|
match &attribute[..] {
|
||||||
return None;
|
[key, value] => Some((key.trim().to_string(), value.trim().to_string())),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((
|
|
||||||
attribute[0].trim().to_string(),
|
|
||||||
attribute[1].trim().to_string(),
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -142,22 +142,11 @@ impl str::FromStr for Aheader {
|
|||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let public_key: SignedPublicKey = attributes
|
||||||
let public_key = match attributes
|
|
||||||
.remove("keydata")
|
.remove("keydata")
|
||||||
.and_then(|raw| Key::from_base64(&raw, KeyType::Public))
|
.ok_or(())
|
||||||
{
|
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
|
||||||
Some(key) => {
|
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
|
||||||
if key.verify() {
|
|
||||||
key
|
|
||||||
} else {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let prefer_encrypt = attributes
|
let prefer_encrypt = attributes
|
||||||
.remove("prefer-encrypt")
|
.remove("prefer-encrypt")
|
||||||
@@ -292,7 +281,7 @@ mod tests {
|
|||||||
"{}",
|
"{}",
|
||||||
Aheader::new(
|
Aheader::new(
|
||||||
"test@example.com".to_string(),
|
"test@example.com".to_string(),
|
||||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||||
EncryptPreference::Mutual
|
EncryptPreference::Mutual
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -305,7 +294,7 @@ mod tests {
|
|||||||
"{}",
|
"{}",
|
||||||
Aheader::new(
|
Aheader::new(
|
||||||
"test@example.com".to_string(),
|
"test@example.com".to_string(),
|
||||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||||
EncryptPreference::NoPreference
|
EncryptPreference::NoPreference
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
442
src/blob.rs
442
src/blob.rs
@@ -2,12 +2,21 @@
|
|||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
|
use async_std::path::{Path, PathBuf};
|
||||||
|
use async_std::prelude::*;
|
||||||
|
use async_std::{fs, io};
|
||||||
|
|
||||||
|
use image::GenericImageView;
|
||||||
|
use num_traits::FromPrimitive;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
use crate::error::Error;
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
|
use crate::message;
|
||||||
|
|
||||||
/// Represents a file in the blob directory.
|
/// Represents a file in the blob directory.
|
||||||
///
|
///
|
||||||
@@ -39,31 +48,35 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// [BlobError::WriteFailure] is used when the file could not
|
/// [BlobError::WriteFailure] is used when the file could not
|
||||||
/// be written to. You can expect [BlobError.cause] to contain an
|
/// be written to. You can expect [BlobError.cause] to contain an
|
||||||
/// underlying error.
|
/// underlying error.
|
||||||
pub fn create(
|
pub async fn create(
|
||||||
context: &'a Context,
|
context: &'a Context,
|
||||||
suggested_name: impl AsRef<str>,
|
suggested_name: impl AsRef<str>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||||
let blobdir = context.get_blobdir();
|
let blobdir = context.get_blobdir();
|
||||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
||||||
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
|
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?;
|
||||||
file.write_all(data)
|
file.write_all(data)
|
||||||
|
.await
|
||||||
.map_err(|err| BlobError::WriteFailure {
|
.map_err(|err| BlobError::WriteFailure {
|
||||||
blobdir: blobdir.to_path_buf(),
|
blobdir: blobdir.to_path_buf(),
|
||||||
blobname: name.clone(),
|
blobname: name.clone(),
|
||||||
cause: err,
|
cause: err.into(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
let blob = BlobObject {
|
let blob = BlobObject {
|
||||||
blobdir,
|
blobdir,
|
||||||
name: format!("$BLOBDIR/{}", name),
|
name: format!("$BLOBDIR/{}", name),
|
||||||
};
|
};
|
||||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
|
||||||
Ok(blob)
|
Ok(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new file, returning a tuple of the name and the handle.
|
// Creates a new file, returning a tuple of the name and the handle.
|
||||||
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> {
|
async fn create_new_file(
|
||||||
|
dir: &Path,
|
||||||
|
stem: &str,
|
||||||
|
ext: &str,
|
||||||
|
) -> Result<(String, fs::File), BlobError> {
|
||||||
let max_attempt = 15;
|
let max_attempt = 15;
|
||||||
let mut name = format!("{}{}", stem, ext);
|
let mut name = format!("{}{}", stem, ext);
|
||||||
for attempt in 0..max_attempt {
|
for attempt in 0..max_attempt {
|
||||||
@@ -72,6 +85,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
.create_new(true)
|
.create_new(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(&path)
|
.open(&path)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(file) => return Ok((name, file)),
|
Ok(file) => return Ok((name, file)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -80,7 +94,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: dir.to_path_buf(),
|
blobdir: dir.to_path_buf(),
|
||||||
blobname: name,
|
blobname: name,
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||||
@@ -93,7 +106,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: dir.to_path_buf(),
|
blobdir: dir.to_path_buf(),
|
||||||
blobname: name,
|
blobname: name,
|
||||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,39 +121,41 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// In addition to the errors in [BlobObject::create] the
|
/// In addition to the errors in [BlobObject::create] the
|
||||||
/// [BlobError::CopyFailure] is used when the data can not be
|
/// [BlobError::CopyFailure] is used when the data can not be
|
||||||
/// copied.
|
/// copied.
|
||||||
pub fn create_and_copy(
|
pub async fn create_and_copy(
|
||||||
context: &'a Context,
|
context: &'a Context,
|
||||||
src: impl AsRef<Path>,
|
src: impl AsRef<Path>,
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||||
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure {
|
let mut src_file =
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
fs::File::open(src.as_ref())
|
||||||
blobname: String::from(""),
|
.await
|
||||||
src: src.as_ref().to_path_buf(),
|
.map_err(|err| BlobError::CopyFailure {
|
||||||
cause: err,
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
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 (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||||
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
|
let (name, mut dst_file) =
|
||||||
|
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
|
||||||
let name_for_err = name.clone();
|
let name_for_err = name.clone();
|
||||||
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
|
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
|
||||||
{
|
{
|
||||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||||
let path = context.get_blobdir().join(&name_for_err);
|
let path = context.get_blobdir().join(&name_for_err);
|
||||||
fs::remove_file(path).ok();
|
fs::remove_file(path).await.ok();
|
||||||
}
|
}
|
||||||
BlobError::CopyFailure {
|
return Err(BlobError::CopyFailure {
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobname: name_for_err,
|
blobname: name_for_err,
|
||||||
src: src.as_ref().to_path_buf(),
|
src: src.as_ref().to_path_buf(),
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
});
|
||||||
}
|
}
|
||||||
})?;
|
|
||||||
let blob = BlobObject {
|
let blob = BlobObject {
|
||||||
blobdir: context.get_blobdir(),
|
blobdir: context.get_blobdir(),
|
||||||
name: format!("$BLOBDIR/{}", name),
|
name: format!("$BLOBDIR/{}", name),
|
||||||
};
|
};
|
||||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
|
||||||
Ok(blob)
|
Ok(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,14 +173,14 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// This merely delegates to the [BlobObject::create_and_copy] and
|
/// This merely delegates to the [BlobObject::create_and_copy] and
|
||||||
/// the [BlobObject::from_path] methods. See those for possible
|
/// the [BlobObject::from_path] methods. See those for possible
|
||||||
/// errors.
|
/// errors.
|
||||||
pub fn new_from_path(
|
pub async fn new_from_path(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
src: impl AsRef<Path>,
|
src: impl AsRef<Path>,
|
||||||
) -> std::result::Result<BlobObject, BlobError> {
|
) -> std::result::Result<BlobObject<'_>, BlobError> {
|
||||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
if src.as_ref().starts_with(context.get_blobdir()) {
|
||||||
BlobObject::from_path(context, src)
|
BlobObject::from_path(context, src)
|
||||||
} else {
|
} else {
|
||||||
BlobObject::create_and_copy(context, src)
|
BlobObject::create_and_copy(context, src).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,17 +208,14 @@ impl<'a> BlobObject<'a> {
|
|||||||
.map_err(|_| BlobError::WrongBlobdir {
|
.map_err(|_| BlobError::WrongBlobdir {
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
src: path.as_ref().to_path_buf(),
|
src: path.as_ref().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||||
return Err(BlobError::WrongName {
|
return Err(BlobError::WrongName {
|
||||||
blobname: path.as_ref().to_path_buf(),
|
blobname: path.as_ref().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||||
blobname: path.as_ref().to_path_buf(),
|
blobname: path.as_ref().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
BlobObject::from_name(context, name.to_string())
|
BlobObject::from_name(context, name.to_string())
|
||||||
}
|
}
|
||||||
@@ -232,7 +243,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||||
return Err(BlobError::WrongName {
|
return Err(BlobError::WrongName {
|
||||||
blobname: PathBuf::from(name),
|
blobname: PathBuf::from(name),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(BlobObject {
|
Ok(BlobObject {
|
||||||
@@ -318,13 +328,12 @@ impl<'a> BlobObject<'a> {
|
|||||||
|
|
||||||
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
||||||
let mut iter = clean.splitn(2, '.');
|
let mut iter = clean.splitn(2, '.');
|
||||||
let mut stem = iter.next().unwrap_or_default().to_string();
|
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
|
||||||
let mut ext = iter.next().unwrap_or_default().to_string();
|
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
|
||||||
stem.truncate(64);
|
if ext.is_empty() {
|
||||||
ext.truncate(32);
|
(stem, "".to_string())
|
||||||
match ext.len() {
|
} else {
|
||||||
0 => (stem, "".to_string()),
|
(stem, format!(".{}", ext).to_lowercase())
|
||||||
_ => (stem, format!(".{}", ext).to_lowercase()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +358,91 @@ impl<'a> BlobObject<'a> {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn recode_to_avatar_size(&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 = 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
|
||||||
|
let blob_abs = self.to_abs_path();
|
||||||
|
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
|
||||||
|
!= Some((Viewtype::Image, "image/jpeg"))
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
if img.width() <= img_wh && img.height() <= img_wh {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
|
||||||
|
let file = std::fs::File::open(self.to_abs_path())?;
|
||||||
|
let mut bufreader = std::io::BufReader::new(&file);
|
||||||
|
let exifreader = exif::Reader::new();
|
||||||
|
let exif = exifreader.read_from_container(&mut bufreader)?;
|
||||||
|
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||||
|
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
|
||||||
|
// we only use rotation, in practise, flipping is not used.
|
||||||
|
match orientation.value.get_uint(0) {
|
||||||
|
Some(3) => return Ok(180),
|
||||||
|
Some(6) => return Ok(90),
|
||||||
|
Some(8) => return Ok(270),
|
||||||
|
other => warn!(context, "exif orientation value ignored: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> fmt::Display for BlobObject<'a> {
|
impl<'a> fmt::Display for BlobObject<'a> {
|
||||||
@@ -358,88 +452,41 @@ impl<'a> fmt::Display for BlobObject<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Errors for the [BlobObject].
|
/// Errors for the [BlobObject].
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum BlobError {
|
pub enum BlobError {
|
||||||
|
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
|
||||||
CreateFailure {
|
CreateFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: std::io::Error,
|
cause: std::io::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
|
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
|
||||||
WriteFailure {
|
WriteFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: std::io::Error,
|
cause: anyhow::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
|
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
|
||||||
CopyFailure {
|
CopyFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
src: PathBuf,
|
src: PathBuf,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: std::io::Error,
|
cause: std::io::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
WrongBlobdir {
|
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
|
||||||
|
RecodeFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
src: PathBuf,
|
blobname: String,
|
||||||
backtrace: failure::Backtrace,
|
#[source]
|
||||||
|
cause: image::ImageError,
|
||||||
},
|
},
|
||||||
WrongName {
|
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
|
||||||
blobname: PathBuf,
|
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||||
backtrace: failure::Backtrace,
|
#[error("Blob has a badname {}", .blobname.display())]
|
||||||
},
|
WrongName { blobname: PathBuf },
|
||||||
}
|
|
||||||
|
|
||||||
// Implementing Display is done by hand because the failure
|
|
||||||
// #[fail(display = "...")] syntax does not allow using
|
|
||||||
// `blobdir.display()`.
|
|
||||||
impl fmt::Display for BlobError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
// Match on the data rather than kind, they are equivalent for
|
|
||||||
// identifying purposes but contain the actual data we need.
|
|
||||||
match &self {
|
|
||||||
BlobError::CreateFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to create blob {} in {}",
|
|
||||||
blobname,
|
|
||||||
blobdir.display()
|
|
||||||
),
|
|
||||||
BlobError::WriteFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to write data to blob {} in {}",
|
|
||||||
blobname,
|
|
||||||
blobdir.display()
|
|
||||||
),
|
|
||||||
BlobError::CopyFailure {
|
|
||||||
blobdir,
|
|
||||||
blobname,
|
|
||||||
src,
|
|
||||||
..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to copy data from {} to blob {} in {}",
|
|
||||||
src.display(),
|
|
||||||
blobname,
|
|
||||||
blobdir.display(),
|
|
||||||
),
|
|
||||||
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
|
|
||||||
f,
|
|
||||||
"File path {} is not in blobdir {}",
|
|
||||||
src.display(),
|
|
||||||
blobdir.display(),
|
|
||||||
),
|
|
||||||
BlobError::WrongName { blobname, .. } => {
|
|
||||||
write!(f, "Blob has a bad name: {}", blobname.display(),)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -448,58 +495,71 @@ mod tests {
|
|||||||
|
|
||||||
use crate::test_utils::*;
|
use crate::test_utils::*;
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create() {
|
async fn test_create() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap();
|
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
|
||||||
let fname = t.ctx.get_blobdir().join("foo");
|
let fname = t.ctx.get_blobdir().join("foo");
|
||||||
let data = fs::read(fname).unwrap();
|
let data = fs::read(fname).await.unwrap();
|
||||||
assert_eq!(data, b"hello");
|
assert_eq!(data, b"hello");
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
|
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.ctx.get_blobdir().join("foo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_lowercase_ext() {
|
async fn test_lowercase_ext() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap();
|
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
|
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_as_file_name() {
|
async fn test_as_file_name() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(blob.as_file_name(), "foo.txt");
|
assert_eq!(blob.as_file_name(), "foo.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_as_rel_path() {
|
async fn test_as_rel_path() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
|
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_suffix() {
|
async fn test_suffix() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(blob.suffix(), Some("txt"));
|
assert_eq!(blob.suffix(), Some("txt"));
|
||||||
let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap();
|
let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
|
||||||
assert_eq!(blob.suffix(), None);
|
assert_eq!(blob.suffix(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_dup() {
|
async fn test_create_dup() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let foo_path = t.ctx.get_blobdir().join("foo.txt");
|
let foo_path = t.ctx.get_blobdir().join("foo.txt");
|
||||||
assert!(foo_path.exists());
|
assert!(foo_path.exists().await);
|
||||||
BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap();
|
BlobObject::create(&t.ctx, "foo.txt", b"world")
|
||||||
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
|
||||||
|
while let Some(dirent) = dir.next().await {
|
||||||
let fname = dirent.unwrap().file_name();
|
let fname = dirent.unwrap().file_name();
|
||||||
if fname == foo_path.file_name().unwrap() {
|
if fname == foo_path.file_name().unwrap() {
|
||||||
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
|
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
|
||||||
} else {
|
} else {
|
||||||
let name = fname.to_str().unwrap();
|
let name = fname.to_str().unwrap();
|
||||||
assert!(name.starts_with("foo"));
|
assert!(name.starts_with("foo"));
|
||||||
@@ -508,17 +568,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_double_ext_preserved() {
|
async fn test_double_ext_preserved() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap();
|
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
|
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
|
||||||
assert!(foo_path.exists());
|
assert!(foo_path.exists().await);
|
||||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap();
|
BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
|
||||||
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
|
||||||
|
while let Some(dirent) = dir.next().await {
|
||||||
let fname = dirent.unwrap().file_name();
|
let fname = dirent.unwrap().file_name();
|
||||||
if fname == foo_path.file_name().unwrap() {
|
if fname == foo_path.file_name().unwrap() {
|
||||||
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
|
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
|
||||||
} else {
|
} else {
|
||||||
let name = fname.to_str().unwrap();
|
let name = fname.to_str().unwrap();
|
||||||
println!("{}", name);
|
println!("{}", name);
|
||||||
@@ -528,55 +593,55 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_long_names() {
|
async fn test_create_long_names() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let s = "1".repeat(150);
|
let s = "1".repeat(150);
|
||||||
let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap();
|
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
|
||||||
let blobname = blob.as_name().split('/').last().unwrap();
|
let blobname = blob.as_name().split('/').last().unwrap();
|
||||||
assert!(blobname.len() < 128);
|
assert!(blobname.len() < 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_and_copy() {
|
async fn test_create_and_copy() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let src = t.dir.path().join("src");
|
let src = t.dir.path().join("src");
|
||||||
fs::write(&src, b"boo").unwrap();
|
fs::write(&src, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap();
|
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
||||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
let whoops = t.dir.path().join("whoops");
|
let whoops = t.dir.path().join("whoops");
|
||||||
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).is_err());
|
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err());
|
||||||
let whoops = t.ctx.get_blobdir().join("whoops");
|
let whoops = t.ctx.get_blobdir().join("whoops");
|
||||||
assert!(!whoops.exists());
|
assert!(!whoops.exists().await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_from_path() {
|
async fn test_create_from_path() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
let src_ext = t.dir.path().join("external");
|
let src_ext = t.dir.path().join("external");
|
||||||
fs::write(&src_ext, b"boo").unwrap();
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
|
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
||||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
let src_int = t.ctx.get_blobdir().join("internal");
|
let src_int = t.ctx.get_blobdir().join("internal");
|
||||||
fs::write(&src_int, b"boo").unwrap();
|
fs::write(&src_int, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
|
let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
||||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
}
|
}
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_from_name_long() {
|
async fn test_create_from_name_long() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||||
fs::write(&src_ext, b"boo").unwrap();
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
|
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blob.as_name(),
|
blob.as_name(),
|
||||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||||
@@ -592,4 +657,43 @@ mod tests {
|
|||||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitise_name() {
|
||||||
|
let (stem, ext) =
|
||||||
|
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
||||||
|
assert_eq!(ext, ".txt");
|
||||||
|
assert!(!stem.is_empty());
|
||||||
|
|
||||||
|
// the extensions are kept together as between stem and extension a number may be added -
|
||||||
|
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
|
||||||
|
assert_eq!(stem, "wot");
|
||||||
|
assert_eq!(ext, ".tar.gz");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
|
||||||
|
assert_eq!(stem, "");
|
||||||
|
assert_eq!(ext, ".foo.bar");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||||
|
assert!(stem.contains("foo"));
|
||||||
|
assert!(!stem.contains("?"));
|
||||||
|
assert_eq!(ext, ".bar");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||||
|
assert_eq!(stem, "no-extension");
|
||||||
|
assert_eq!(ext, "");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
|
||||||
|
assert_eq!(ext, ".c");
|
||||||
|
assert!(!stem.contains("path"));
|
||||||
|
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("?"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3679
src/chat.rs
3679
src/chat.rs
File diff suppressed because it is too large
Load Diff
377
src/chatlist.rs
377
src/chatlist.rs
@@ -1,10 +1,11 @@
|
|||||||
//! # Chat list module
|
//! # Chat list module
|
||||||
|
|
||||||
|
use crate::chat;
|
||||||
use crate::chat::*;
|
use crate::chat::*;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::*;
|
use crate::context::*;
|
||||||
use crate::error::Result;
|
use crate::error::{bail, ensure, Result};
|
||||||
use crate::lot::Lot;
|
use crate::lot::Lot;
|
||||||
use crate::message::{Message, MessageState, MsgId};
|
use crate::message::{Message, MessageState, MsgId};
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
@@ -36,7 +37,7 @@ use crate::stock::StockMessage;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Chatlist {
|
pub struct Chatlist {
|
||||||
/// Stores pairs of `chat_id, message_id`
|
/// Stores pairs of `chat_id, message_id`
|
||||||
ids: Vec<(u32, MsgId)>,
|
ids: Vec<(ChatId, MsgId)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chatlist {
|
impl Chatlist {
|
||||||
@@ -60,7 +61,7 @@ impl Chatlist {
|
|||||||
/// or "Not now".
|
/// or "Not now".
|
||||||
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
||||||
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
||||||
/// archived *any* chat using dc_archive_chat(). The UI should show a link as
|
/// 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
|
/// "Show archived chats", if the user clicks this item, the UI should show a
|
||||||
/// list of all archived chats that can be created by this function hen using
|
/// list of all archived chats that can be created by this function hen using
|
||||||
/// the DC_GCL_ARCHIVED_ONLY flag.
|
/// the DC_GCL_ARCHIVED_ONLY flag.
|
||||||
@@ -73,6 +74,9 @@ impl Chatlist {
|
|||||||
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||||
/// chats
|
/// chats
|
||||||
|
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||||
|
/// and hides the device-chat,
|
||||||
|
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||||
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||||
@@ -82,23 +86,32 @@ impl Chatlist {
|
|||||||
/// are returned.
|
/// are returned.
|
||||||
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
|
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
|
||||||
/// are returned.
|
/// are returned.
|
||||||
pub fn try_load(
|
pub async fn try_load(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
listflags: usize,
|
listflags: usize,
|
||||||
query: Option<&str>,
|
query: Option<&str>,
|
||||||
query_contact_id: Option<u32>,
|
query_contact_id: Option<u32>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
|
||||||
|
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
|
||||||
|
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
|
||||||
|
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
|
||||||
|
|
||||||
|
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||||
|
// messages get deleted to avoid reloading the same chatlist.
|
||||||
|
if let Err(err) = delete_device_expired_messages(context).await {
|
||||||
|
warn!(context, "Failed to hide expired messages: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
let mut add_archived_link_item = false;
|
let mut add_archived_link_item = false;
|
||||||
|
|
||||||
let process_row = |row: &rusqlite::Row| {
|
let skip_id = if flag_for_forwarding {
|
||||||
let chat_id: u32 = row.get(0)?;
|
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||||
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
.await
|
||||||
Ok((chat_id, msg_id))
|
.unwrap_or_default()
|
||||||
};
|
.0
|
||||||
|
} else {
|
||||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
ChatId::new(0)
|
||||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
|
||||||
.map_err(Into::into)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// select with left join and minimum:
|
// select with left join and minimum:
|
||||||
@@ -118,105 +131,131 @@ impl Chatlist {
|
|||||||
// shown at all permanent in the chatlist.
|
// shown at all permanent in the chatlist.
|
||||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||||
// show chats shared with a given contact
|
// show chats shared with a given contact
|
||||||
context.sql.query_map(
|
context.sql.query_rows(
|
||||||
"SELECT c.id, m.id
|
"SELECT c.id, m.id
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
AND m.timestamp=(
|
AND m.id=(
|
||||||
SELECT MAX(timestamp)
|
SELECT id
|
||||||
FROM msgs
|
FROM msgs
|
||||||
WHERE chat_id=c.id
|
WHERE chat_id=c.id
|
||||||
AND (hidden=0 OR state=?))
|
AND (hidden=0 OR state=?)
|
||||||
|
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||||
WHERE c.id>9
|
WHERE c.id>9
|
||||||
AND c.blocked=0
|
AND c.blocked=0
|
||||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
|
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
params![MessageState::OutDraft, query_contact_id as i32],
|
paramsx![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||||
process_row,
|
).await?
|
||||||
process_rows,
|
} else if flag_archived_only {
|
||||||
)?
|
|
||||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
|
||||||
// show archived chats
|
// show archived chats
|
||||||
context.sql.query_map(
|
// (this includes the archived device-chat; we could skip it,
|
||||||
"SELECT c.id, m.id
|
// however, then the number of archived chats do not match, which might be even more irritating.
|
||||||
|
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.query_rows(
|
||||||
|
"SELECT c.id, m.id
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
AND m.timestamp=(
|
AND m.id=(
|
||||||
SELECT MAX(timestamp)
|
SELECT id
|
||||||
FROM msgs
|
FROM msgs
|
||||||
WHERE chat_id=c.id
|
WHERE chat_id=c.id
|
||||||
AND (hidden=0 OR state=?))
|
AND (hidden=0 OR state=?)
|
||||||
|
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||||
WHERE c.id>9
|
WHERE c.id>9
|
||||||
AND c.blocked=0
|
AND c.blocked=0
|
||||||
AND c.archived=1
|
AND c.archived=1
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
params![MessageState::OutDraft],
|
paramsx![MessageState::OutDraft],
|
||||||
process_row,
|
)
|
||||||
process_rows,
|
.await?
|
||||||
)?
|
|
||||||
} else if let Some(query) = query {
|
} else if let Some(query) = query {
|
||||||
let query = query.trim().to_string();
|
let query = query.trim().to_string();
|
||||||
ensure!(!query.is_empty(), "missing query");
|
ensure!(!query.is_empty(), "missing query");
|
||||||
|
|
||||||
|
// allow searching over special names that may change at any time
|
||||||
|
// when the ui calls set_stock_translation()
|
||||||
|
if let Err(err) = update_special_chat_names(context).await {
|
||||||
|
warn!(context, "cannot update special chat names: {:?}", err)
|
||||||
|
}
|
||||||
|
|
||||||
let str_like_cmd = format!("%{}%", query);
|
let str_like_cmd = format!("%{}%", query);
|
||||||
context.sql.query_map(
|
context
|
||||||
"SELECT c.id, m.id
|
.sql
|
||||||
|
.query_rows(
|
||||||
|
"SELECT c.id, m.id
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
AND m.timestamp=(
|
AND m.id=(
|
||||||
SELECT MAX(timestamp)
|
SELECT id
|
||||||
FROM msgs
|
FROM msgs
|
||||||
WHERE chat_id=c.id
|
WHERE chat_id=c.id
|
||||||
AND (hidden=0 OR state=?))
|
AND (hidden=0 OR state=?1)
|
||||||
WHERE c.id>9
|
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||||
|
WHERE c.id>9 AND c.id!=?2
|
||||||
AND c.blocked=0
|
AND c.blocked=0
|
||||||
AND c.name LIKE ?
|
AND c.name LIKE ?3
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
params![MessageState::OutDraft, str_like_cmd],
|
paramsx![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||||
process_row,
|
)
|
||||||
process_rows,
|
.await?
|
||||||
)?
|
|
||||||
} else {
|
} else {
|
||||||
// show normal chatlist
|
// show normal chatlist
|
||||||
let mut ids = context.sql.query_map(
|
let sort_id_up = if flag_for_forwarding {
|
||||||
|
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.0
|
||||||
|
} else {
|
||||||
|
ChatId::new(0)
|
||||||
|
};
|
||||||
|
let mut ids = context.sql.query_rows(
|
||||||
"SELECT c.id, m.id
|
"SELECT c.id, m.id
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
AND m.timestamp=(
|
AND m.id=(
|
||||||
SELECT MAX(timestamp)
|
SELECT id
|
||||||
FROM msgs
|
FROM msgs
|
||||||
WHERE chat_id=c.id
|
WHERE chat_id=c.id
|
||||||
AND (hidden=0 OR state=?))
|
AND (hidden=0 OR state=?1)
|
||||||
WHERE c.id>9
|
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||||
|
WHERE c.id>9 AND c.id!=?2
|
||||||
AND c.blocked=0
|
AND c.blocked=0
|
||||||
AND c.archived=0
|
AND NOT c.archived=?3
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
params![MessageState::OutDraft],
|
paramsx![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||||
process_row,
|
).await?;
|
||||||
process_rows,
|
|
||||||
)?;
|
if !flag_no_specials {
|
||||||
if 0 == listflags & DC_GCL_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) {
|
{
|
||||||
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
|
if !flag_for_forwarding {
|
||||||
|
ids.insert(
|
||||||
|
0,
|
||||||
|
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
add_archived_link_item = true;
|
add_archived_link_item = true;
|
||||||
}
|
}
|
||||||
ids
|
ids
|
||||||
};
|
};
|
||||||
|
|
||||||
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
|
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
|
||||||
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
|
if ids.is_empty() && flag_add_alldone_hint {
|
||||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
|
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
||||||
}
|
}
|
||||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
|
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Chatlist { ids })
|
Ok(Chatlist { ids })
|
||||||
@@ -235,19 +274,21 @@ impl Chatlist {
|
|||||||
/// Get a single chat ID of a chatlist.
|
/// Get a single chat ID of a chatlist.
|
||||||
///
|
///
|
||||||
/// To get the message object from the message ID, use dc_get_chat().
|
/// To get the message object from the message ID, use dc_get_chat().
|
||||||
pub fn get_chat_id(&self, index: usize) -> u32 {
|
pub fn get_chat_id(&self, index: usize) -> ChatId {
|
||||||
if index >= self.ids.len() {
|
match self.ids.get(index) {
|
||||||
return 0;
|
Some((chat_id, _msg_id)) => *chat_id,
|
||||||
|
None => ChatId::new(0),
|
||||||
}
|
}
|
||||||
self.ids[index].0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a single message ID of a chatlist.
|
/// Get a single message ID of a chatlist.
|
||||||
///
|
///
|
||||||
/// To get the message object from the message ID, use dc_get_msg().
|
/// To get the message object from the message ID, use dc_get_msg().
|
||||||
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
|
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
|
||||||
ensure!(index < self.ids.len(), "Chatlist index out of range");
|
match self.ids.get(index) {
|
||||||
Ok(self.ids[index].1)
|
Some((_chat_id, msg_id)) => Ok(*msg_id),
|
||||||
|
None => bail!("Chatlist index out of range"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a summary for a chatlist index.
|
/// Get a summary for a chatlist index.
|
||||||
@@ -264,36 +305,38 @@ impl Chatlist {
|
|||||||
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
|
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
|
||||||
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
|
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
|
||||||
// 0 if not applicable.
|
// 0 if not applicable.
|
||||||
pub fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
|
pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
|
||||||
// The summary is created by the chat, not by the last message.
|
// The summary is created by the chat, not by the last message.
|
||||||
// This is because we may want to display drafts here or stuff as
|
// This is because we may want to display drafts here or stuff as
|
||||||
// "is typing".
|
// "is typing".
|
||||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||||
|
|
||||||
let mut ret = Lot::new();
|
let mut ret = Lot::new();
|
||||||
if index >= self.ids.len() {
|
|
||||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
let (chat_id, lastmsg_id) = match self.ids.get(index) {
|
||||||
return ret;
|
Some(ids) => ids,
|
||||||
}
|
None => {
|
||||||
|
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let chat_loaded: Chat;
|
let chat_loaded: Chat;
|
||||||
let chat = if let Some(chat) = chat {
|
let chat = if let Some(chat) = chat {
|
||||||
chat
|
chat
|
||||||
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
|
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
|
||||||
chat_loaded = chat;
|
chat_loaded = chat;
|
||||||
&chat_loaded
|
&chat_loaded
|
||||||
} else {
|
} else {
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastmsg_id = self.ids[index].1;
|
|
||||||
let mut lastcontact = None;
|
let mut lastcontact = None;
|
||||||
|
|
||||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
|
||||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||||
{
|
{
|
||||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
|
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(lastmsg)
|
Some(lastmsg)
|
||||||
@@ -301,66 +344,85 @@ impl Chatlist {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if chat.id == DC_CHAT_ID_ARCHIVED_LINK {
|
if chat.id.is_archived_link() {
|
||||||
ret.text2 = None;
|
ret.text2 = None;
|
||||||
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
||||||
{
|
{
|
||||||
ret.text2 = Some(context.stock_str(StockMessage::NoMessages).to_string());
|
ret.text2 = Some(
|
||||||
|
context
|
||||||
|
.stock_str(StockMessage::NoMessages)
|
||||||
|
.await
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context);
|
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
|
||||||
|
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of archived chats
|
/// Returns the number of archived chats
|
||||||
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
|
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||||
context
|
let v: i32 = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_value(
|
||||||
context,
|
|
||||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||||
params![],
|
paramsx![],
|
||||||
)
|
)
|
||||||
.unwrap_or_default()
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
v as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||||
// We have an index over the state-column, this should be
|
// We have an index over the state-column, this should be
|
||||||
// sufficient as there are typically only few fresh messages.
|
// sufficient as there are typically only few fresh messages.
|
||||||
context.sql.query_get_value(
|
context
|
||||||
context,
|
.sql
|
||||||
concat!(
|
.query_value(
|
||||||
"SELECT m.id",
|
r#"
|
||||||
" FROM msgs m",
|
SELECT m.id
|
||||||
" LEFT JOIN chats c",
|
FROM msgs m
|
||||||
" ON c.id=m.chat_id",
|
LEFT JOIN chats c
|
||||||
" WHERE m.state=10",
|
ON c.id=m.chat_id
|
||||||
" AND m.hidden=0",
|
WHERE m.state=10
|
||||||
" AND c.blocked=2",
|
AND m.hidden=0
|
||||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
AND c.blocked=2
|
||||||
),
|
ORDER BY m.timestamp DESC, m.id DESC;
|
||||||
params![],
|
"#,
|
||||||
)
|
paramsx![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::chat;
|
|
||||||
use crate::test_utils::*;
|
use crate::test_utils::*;
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_try_load() {
|
async fn test_try_load() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap();
|
.await
|
||||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap();
|
.unwrap();
|
||||||
|
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// check that the chatlist starts with the most recent message
|
// check that the chatlist starts with the most recent message
|
||||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
assert_eq!(chats.len(), 3);
|
assert_eq!(chats.len(), 3);
|
||||||
assert_eq!(chats.get_chat_id(0), chat_id3);
|
assert_eq!(chats.get_chat_id(0), chat_id3);
|
||||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||||
@@ -369,19 +431,102 @@ mod tests {
|
|||||||
// drafts are sorted to the top
|
// drafts are sorted to the top
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(Some("hello".to_string()));
|
msg.set_text(Some("hello".to_string()));
|
||||||
set_draft(&t.ctx, chat_id2, Some(&mut msg));
|
chat_id2.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||||
|
|
||||||
// check chatlist query and archive functionality
|
// check chatlist query and archive functionality
|
||||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap();
|
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(chats.len(), 1);
|
assert_eq!(chats.len(), 1);
|
||||||
|
|
||||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(chats.len(), 0);
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
chat::archive(&t.ctx, chat_id1, true).ok();
|
chat_id1
|
||||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(chats.len(), 1);
|
assert_eq!(chats.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
|
assert!(chats.len() == 3);
|
||||||
|
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_self_talk());
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 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))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_self_talk());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_search_special_chat_names() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
t.ctx.update_device_chats().await.unwrap();
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 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)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
|
t.ctx
|
||||||
|
.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)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(chats.len(), 1);
|
||||||
|
|
||||||
|
t.ctx
|
||||||
|
.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)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(chats.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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")
|
||||||
|
.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;
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
|
let summary = chats.get_summary(&t.ctx, 0, None).await;
|
||||||
|
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
239
src/config.rs
239
src/config.rs
@@ -4,12 +4,14 @@ use strum::{EnumProperty, IntoEnumIterator};
|
|||||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
|
use crate::chat::ChatId;
|
||||||
use crate::constants::DC_VERSION_STR;
|
use crate::constants::DC_VERSION_STR;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::job::*;
|
use crate::events::Event;
|
||||||
|
use crate::message::MsgId;
|
||||||
|
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
use rusqlite::NO_PARAMS;
|
|
||||||
|
|
||||||
/// The available configuration keys.
|
/// The available configuration keys.
|
||||||
#[derive(
|
#[derive(
|
||||||
@@ -61,6 +63,31 @@ pub enum Config {
|
|||||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||||
ShowEmails,
|
ShowEmails,
|
||||||
|
|
||||||
|
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||||
|
MediaQuality,
|
||||||
|
|
||||||
|
#[strum(props(default = "0"))]
|
||||||
|
KeyGenType,
|
||||||
|
|
||||||
|
/// Timer in seconds after which the message is deleted from the
|
||||||
|
/// server.
|
||||||
|
///
|
||||||
|
/// Equals to 0 by default, which means the message is never
|
||||||
|
/// deleted.
|
||||||
|
///
|
||||||
|
/// Value 1 is treated as "delete at once": messages are deleted
|
||||||
|
/// immediately, without moving to DeltaChat folder.
|
||||||
|
#[strum(props(default = "0"))]
|
||||||
|
DeleteServerAfter,
|
||||||
|
|
||||||
|
/// Timer in seconds after which the message is deleted from the
|
||||||
|
/// device.
|
||||||
|
///
|
||||||
|
/// Equals to 0 by default, which means the message is never
|
||||||
|
/// deleted.
|
||||||
|
#[strum(props(default = "0"))]
|
||||||
|
DeleteDeviceAfter,
|
||||||
|
|
||||||
SaveMimeHeaders,
|
SaveMimeHeaders,
|
||||||
ConfiguredAddr,
|
ConfiguredAddr,
|
||||||
ConfiguredMailServer,
|
ConfiguredMailServer,
|
||||||
@@ -77,6 +104,9 @@ pub enum Config {
|
|||||||
ConfiguredServerFlags,
|
ConfiguredServerFlags,
|
||||||
ConfiguredSendSecurity,
|
ConfiguredSendSecurity,
|
||||||
ConfiguredE2EEEnabled,
|
ConfiguredE2EEEnabled,
|
||||||
|
ConfiguredInboxFolder,
|
||||||
|
ConfiguredMvboxFolder,
|
||||||
|
ConfiguredSentboxFolder,
|
||||||
Configured,
|
Configured,
|
||||||
|
|
||||||
#[strum(serialize = "sys.version")]
|
#[strum(serialize = "sys.version")]
|
||||||
@@ -90,17 +120,21 @@ pub enum Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
|
pub async fn config_exists(&self, key: Config) -> bool {
|
||||||
|
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.
|
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
|
||||||
pub fn get_config(&self, key: Config) -> Option<String> {
|
pub async fn get_config(&self, key: Config) -> Option<String> {
|
||||||
let value = match key {
|
let value = match key {
|
||||||
Config::Selfavatar => {
|
Config::Selfavatar => {
|
||||||
let rel_path = self.sql.get_raw_config(self, key);
|
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())
|
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||||
}
|
}
|
||||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
|
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||||
_ => self.sql.get_raw_config(self, key),
|
_ => self.sql.get_raw_config(key).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
if value.is_some() {
|
if value.is_some() {
|
||||||
@@ -109,64 +143,88 @@ impl Context {
|
|||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
match key {
|
match key {
|
||||||
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).into_owned()),
|
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()),
|
_ => key.get_str("default").map(|s| s.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_int(&self, key: Config) -> i32 {
|
pub async fn get_config_int(&self, key: Config) -> i32 {
|
||||||
self.get_config(key)
|
self.get_config(key)
|
||||||
|
.await
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_bool(&self, key: Config) -> bool {
|
pub async fn get_config_bool(&self, key: Config) -> bool {
|
||||||
self.get_config_int(key) != 0
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the given config key.
|
/// 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.
|
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||||
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||||
match key {
|
match key {
|
||||||
Config::Selfavatar => {
|
Config::Selfavatar => {
|
||||||
self.sql
|
self.sql
|
||||||
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?;
|
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsx![])
|
||||||
|
.await?;
|
||||||
self.sql
|
self.sql
|
||||||
.set_raw_config_bool(self, "attach_selfavatar", true)?;
|
.set_raw_config_bool(self, "attach_selfavatar", true)
|
||||||
|
.await?;
|
||||||
match value {
|
match value {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let blob = BlobObject::new_from_path(&self, value)?;
|
let blob = BlobObject::new_from_path(&self, value).await?;
|
||||||
self.sql.set_raw_config(self, key, Some(blob.as_name()))
|
blob.recode_to_avatar_size(self)?;
|
||||||
|
self.sql
|
||||||
|
.set_raw_config(self, key, Some(blob.as_name()))
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
None => self.sql.set_raw_config(self, key, None),
|
None => self.sql.set_raw_config(self, key, None).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Config::InboxWatch => {
|
|
||||||
let ret = self.sql.set_raw_config(self, key, value);
|
|
||||||
interrupt_inbox_idle(self, true);
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
Config::SentboxWatch => {
|
|
||||||
let ret = self.sql.set_raw_config(self, key, value);
|
|
||||||
interrupt_sentbox_idle(self);
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
Config::MvboxWatch => {
|
|
||||||
let ret = self.sql.set_raw_config(self, key, value);
|
|
||||||
interrupt_mvbox_idle(self);
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
Config::Selfstatus => {
|
Config::Selfstatus => {
|
||||||
let def = self.stock_str(StockMessage::StatusLine);
|
let def = self.stock_str(StockMessage::StatusLine).await;
|
||||||
let val = if value.is_none() || value.unwrap() == def {
|
let val = if value.is_none() || value.unwrap() == def {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
value
|
value
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sql.set_raw_config(self, key, val)
|
self.sql.set_raw_config(self, key, val).await
|
||||||
}
|
}
|
||||||
_ => self.sql.set_raw_config(self, key, value),
|
Config::DeleteDeviceAfter => {
|
||||||
|
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||||
|
// Force chatlist reload to delete old messages immediately.
|
||||||
|
self.emit_event(Event::MsgsChanged {
|
||||||
|
msg_id: MsgId::new(0),
|
||||||
|
chat_id: ChatId::new(0),
|
||||||
|
});
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
_ => self.sql.set_raw_config(self, key, value).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +247,13 @@ mod tests {
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
|
use crate::constants;
|
||||||
|
use crate::constants::AVATAR_SIZE;
|
||||||
use crate::test_utils::*;
|
use crate::test_utils::*;
|
||||||
|
use image::GenericImageView;
|
||||||
|
use num_traits::FromPrimitive;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_string() {
|
fn test_to_string() {
|
||||||
@@ -208,31 +272,102 @@ mod tests {
|
|||||||
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
|
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_selfavatar() -> failure::Fallible<()> {
|
async fn test_selfavatar_outside_blobdir() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||||
std::fs::write(&avatar_src, b"avatar")?;
|
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");
|
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
|
||||||
assert!(!avatar_blob.exists());
|
assert!(!avatar_blob.exists().await);
|
||||||
t.ctx
|
t.ctx
|
||||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
|
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||||
assert!(avatar_blob.exists());
|
.await
|
||||||
assert_eq!(std::fs::read(&avatar_blob)?, b"avatar");
|
.unwrap();
|
||||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
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()));
|
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||||
Ok(())
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_selfavatar_in_blobdir() -> failure::Fallible<()> {
|
async fn test_selfavatar_in_blobdir() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let avatar_src = t.ctx.get_blobdir().join("avatar.jpg");
|
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
|
||||||
std::fs::write(&avatar_src, b"avatar")?;
|
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
|
t.ctx
|
||||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
|
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
.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()));
|
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
|
||||||
Ok(())
|
|
||||||
|
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;
|
||||||
|
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();
|
||||||
|
|
||||||
|
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||||
|
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();
|
||||||
|
assert_eq!(media_quality, constants::MediaQuality::Worse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//! # Thunderbird's Autoconfiguration implementation
|
//! # Thunderbird's Autoconfiguration implementation
|
||||||
//!
|
//!
|
||||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
@@ -9,33 +8,7 @@ use crate::context::Context;
|
|||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
|
|
||||||
use super::read_url::read_url;
|
use super::read_url::read_url;
|
||||||
|
use super::Error;
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "Invalid email address: {:?}", _0)]
|
|
||||||
InvalidEmailAddress(String),
|
|
||||||
|
|
||||||
#[fail(display = "XML error at position {}", position)]
|
|
||||||
InvalidXml {
|
|
||||||
position: usize,
|
|
||||||
#[cause]
|
|
||||||
error: quick_xml::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[fail(display = "Bad or incomplete autoconfig")]
|
|
||||||
IncompleteAutoconfig(LoginParam),
|
|
||||||
|
|
||||||
#[fail(display = "Failed to get URL {}", _0)]
|
|
||||||
ReadUrlError(#[cause] super::read_url::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<super::read_url::Error> for Error {
|
|
||||||
fn from(err: super::read_url::Error) -> Error {
|
|
||||||
Error::ReadUrlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct MozAutoconfigure<'a> {
|
struct MozAutoconfigure<'a> {
|
||||||
@@ -65,16 +38,16 @@ enum MozConfigTag {
|
|||||||
Username,
|
Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||||
reader.trim_text(true);
|
reader.trim_text(true);
|
||||||
|
|
||||||
// Split address into local part and domain part.
|
// Split address into local part and domain part.
|
||||||
let p = in_emailaddr
|
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
|
||||||
.find('@')
|
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
|
||||||
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
|
[domain, local] => (local, domain),
|
||||||
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
|
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
|
||||||
let in_emaildomain = &in_emaildomain[1..];
|
};
|
||||||
|
|
||||||
let mut moz_ac = MozAutoconfigure {
|
let mut moz_ac = MozAutoconfigure {
|
||||||
in_emailaddr,
|
in_emailaddr,
|
||||||
@@ -121,12 +94,12 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn moz_autoconfigure(
|
pub async fn moz_autoconfigure(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
param_in: &LoginParam,
|
param_in: &LoginParam,
|
||||||
) -> Result<LoginParam> {
|
) -> Result<LoginParam, Error> {
|
||||||
let xml_raw = read_url(context, url)?;
|
let xml_raw = read_url(context, url).await?;
|
||||||
|
|
||||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
let res = parse_xml(¶m_in.addr, &xml_raw);
|
||||||
if let Err(err) = &res {
|
if let Err(err) = &res {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Outlook's Autodiscover
|
//! Outlook's Autodiscover
|
||||||
|
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::BytesEnd;
|
use quick_xml::events::BytesEnd;
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
@@ -8,33 +7,7 @@ use crate::context::Context;
|
|||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
|
|
||||||
use super::read_url::read_url;
|
use super::read_url::read_url;
|
||||||
|
use super::Error;
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "XML error at position {}", position)]
|
|
||||||
InvalidXml {
|
|
||||||
position: usize,
|
|
||||||
#[cause]
|
|
||||||
error: quick_xml::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[fail(display = "Bad or incomplete autoconfig")]
|
|
||||||
IncompleteAutoconfig(LoginParam),
|
|
||||||
|
|
||||||
#[fail(display = "Failed to get URL {}", _0)]
|
|
||||||
ReadUrlError(#[cause] super::read_url::Error),
|
|
||||||
|
|
||||||
#[fail(display = "Number of redirection is exceeded")]
|
|
||||||
RedirectionError,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<super::read_url::Error> for Error {
|
|
||||||
fn from(err: super::read_url::Error) -> Error {
|
|
||||||
Error::ReadUrlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OutlookAutodiscover {
|
struct OutlookAutodiscover {
|
||||||
pub out: LoginParam,
|
pub out: LoginParam,
|
||||||
@@ -52,7 +25,7 @@ enum ParsingResult {
|
|||||||
RedirectUrl(String),
|
RedirectUrl(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
|
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||||
let mut outlk_ad = OutlookAutodiscover {
|
let mut outlk_ad = OutlookAutodiscover {
|
||||||
out: LoginParam::new(),
|
out: LoginParam::new(),
|
||||||
out_imap_set: false,
|
out_imap_set: false,
|
||||||
@@ -139,15 +112,15 @@ fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn outlk_autodiscover(
|
pub async fn outlk_autodiscover(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
_param_in: &LoginParam,
|
_param_in: &LoginParam,
|
||||||
) -> Result<LoginParam> {
|
) -> Result<LoginParam, Error> {
|
||||||
let mut url = url.to_string();
|
let mut url = url.to_string();
|
||||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||||
for _i in 0..10 {
|
for _i in 0..10 {
|
||||||
let xml_raw = read_url(context, &url)?;
|
let xml_raw = read_url(context, &url).await?;
|
||||||
let res = parse_xml(&xml_raw);
|
let res = parse_xml(&xml_raw);
|
||||||
if let Err(err) = &res {
|
if let Err(err) = &res {
|
||||||
warn!(context, "{}", err);
|
warn!(context, "{}", err);
|
||||||
|
|||||||
1013
src/configure/mod.rs
1013
src/configure/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,15 @@
|
|||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "URL request error")]
|
#[error("URL request error")]
|
||||||
GetError(#[cause] reqwest::Error),
|
GetError(surf::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
|
||||||
|
|
||||||
pub fn read_url(context: &Context, url: &str) -> Result<String> {
|
|
||||||
info!(context, "Requesting URL {}", url);
|
info!(context, "Requesting URL {}", url);
|
||||||
|
|
||||||
match reqwest::Client::new()
|
match surf::get(url).recv_string().await {
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.and_then(|mut res| res.text())
|
|
||||||
{
|
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
info!(context, "Can\'t read URL {}", url);
|
info!(context, "Can\'t read URL {}", url);
|
||||||
|
|||||||
101
src/constants.rs
101
src/constants.rs
@@ -1,8 +1,8 @@
|
|||||||
//! # Constants
|
//! # Constants
|
||||||
#![allow(non_camel_case_types, dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use deltachat_derive::*;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
||||||
@@ -15,8 +15,20 @@ const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
|
|||||||
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
|
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
|
||||||
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
#[derive(
|
||||||
#[repr(u8)]
|
Debug,
|
||||||
|
Display,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
FromPrimitive,
|
||||||
|
ToPrimitive,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
sqlx::Type,
|
||||||
|
)]
|
||||||
|
#[repr(i32)]
|
||||||
pub enum Blocked {
|
pub enum Blocked {
|
||||||
Not = 0,
|
Not = 0,
|
||||||
Manually = 1,
|
Manually = 1,
|
||||||
@@ -29,8 +41,8 @@ impl Default for Blocked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||||
#[repr(u8)]
|
#[repr(i32)]
|
||||||
pub enum ShowEmails {
|
pub enum ShowEmails {
|
||||||
Off = 0,
|
Off = 0,
|
||||||
AcceptedContacts = 1,
|
AcceptedContacts = 1,
|
||||||
@@ -43,15 +55,43 @@ impl Default for ShowEmails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DC_IMAP_SEEN: u32 = 0x1;
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum MediaQuality {
|
||||||
|
Balanced = 0,
|
||||||
|
Worse = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MediaQuality {
|
||||||
|
fn default() -> Self {
|
||||||
|
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum KeyGenType {
|
||||||
|
Default = 0,
|
||||||
|
Rsa2048 = 1,
|
||||||
|
Ed25519 = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyGenType {
|
||||||
|
fn default() -> Self {
|
||||||
|
KeyGenType::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
||||||
|
|
||||||
|
pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
|
||||||
|
|
||||||
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
||||||
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
||||||
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
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_ADDDAYMARKER: u32 = 0x01;
|
||||||
|
|
||||||
@@ -61,10 +101,6 @@ pub const DC_GCL_ADD_SELF: usize = 0x02;
|
|||||||
// unchanged user avatars are resent to the recipients every some days
|
// unchanged user avatars are resent to the recipients every some days
|
||||||
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||||
|
|
||||||
// values for DC_PARAM_FORCE_PLAINTEXT
|
|
||||||
pub(crate) const DC_FP_NO_AUTOCRYPT_HEADER: i32 = 2;
|
|
||||||
pub(crate) const DC_FP_ADD_AUTOCRYPT_HEADER: i32 = 1;
|
|
||||||
|
|
||||||
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
||||||
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
|
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 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)
|
/// 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)
|
||||||
@@ -89,11 +125,12 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
|||||||
Eq,
|
Eq,
|
||||||
FromPrimitive,
|
FromPrimitive,
|
||||||
ToPrimitive,
|
ToPrimitive,
|
||||||
FromSql,
|
|
||||||
ToSql,
|
|
||||||
IntoStaticStr,
|
IntoStaticStr,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
sqlx::Type,
|
||||||
)]
|
)]
|
||||||
#[repr(u32)]
|
#[repr(i32)]
|
||||||
pub enum Chattype {
|
pub enum Chattype {
|
||||||
Undefined = 0,
|
Undefined = 0,
|
||||||
Single = 100,
|
Single = 100,
|
||||||
@@ -114,7 +151,7 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
|||||||
/// approx. max. length returned by dc_msg_get_text()
|
/// approx. max. length returned by dc_msg_get_text()
|
||||||
const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
||||||
/// approx. max. length returned by dc_get_msg_info()
|
/// approx. max. length returned by dc_get_msg_info()
|
||||||
const DC_MAX_GET_INFO_LEN: usize = 100000;
|
const DC_MAX_GET_INFO_LEN: usize = 100_000;
|
||||||
|
|
||||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
||||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
||||||
@@ -170,13 +207,13 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
|
|||||||
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
||||||
|
|
||||||
/// if none of these flags are set, the default is chosen
|
/// 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);
|
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
|
/// if none of these flags are set, the default is chosen
|
||||||
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
||||||
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
|
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
|
/// if none of these flags are set, the default is chosen
|
||||||
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||||
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
|
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
|
||||||
|
|
||||||
// QR code scanning (view from Bob, the joiner)
|
// QR code scanning (view from Bob, the joiner)
|
||||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
||||||
@@ -184,10 +221,33 @@ pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
|
|||||||
pub const DC_BOB_ERROR: i32 = 0;
|
pub const DC_BOB_ERROR: i32 = 0;
|
||||||
pub const DC_BOB_SUCCESS: i32 = 1;
|
pub const DC_BOB_SUCCESS: i32 = 1;
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
// max. width/height of an avatar
|
||||||
|
pub const AVATAR_SIZE: u32 = 192;
|
||||||
|
|
||||||
|
// max. width/height of images
|
||||||
|
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Display,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
FromPrimitive,
|
||||||
|
ToPrimitive,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
sqlx::Type,
|
||||||
|
)]
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
pub enum Viewtype {
|
pub enum Viewtype {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
|
|
||||||
/// Text message.
|
/// Text message.
|
||||||
/// The text of the message is set using dc_msg_set_text()
|
/// The text of the message is set using dc_msg_set_text()
|
||||||
/// and retrieved with dc_msg_get_text().
|
/// and retrieved with dc_msg_get_text().
|
||||||
@@ -267,8 +327,6 @@ const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
|||||||
const DC_STR_NOMESSAGES: usize = 1;
|
const DC_STR_NOMESSAGES: usize = 1;
|
||||||
const DC_STR_SELF: usize = 2;
|
const DC_STR_SELF: usize = 2;
|
||||||
const DC_STR_DRAFT: usize = 3;
|
const DC_STR_DRAFT: usize = 3;
|
||||||
const DC_STR_MEMBER: usize = 4;
|
|
||||||
const DC_STR_CONTACT: usize = 6;
|
|
||||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
const DC_STR_VOICEMESSAGE: usize = 7;
|
||||||
const DC_STR_DEADDROP: usize = 8;
|
const DC_STR_DEADDROP: usize = 8;
|
||||||
const DC_STR_IMAGE: usize = 9;
|
const DC_STR_IMAGE: usize = 9;
|
||||||
@@ -300,7 +358,6 @@ const DC_STR_ARCHIVEDCHATS: usize = 40;
|
|||||||
const DC_STR_STARREDMSGS: usize = 41;
|
const DC_STR_STARREDMSGS: usize = 41;
|
||||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||||
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
|
|
||||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||||
|
|||||||
1085
src/contact.rs
1085
src/contact.rs
File diff suppressed because it is too large
Load Diff
638
src/context.rs
638
src/context.rs
@@ -1,75 +1,70 @@
|
|||||||
//! Contacts module
|
//! Context module
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::{Path, PathBuf};
|
use std::ops::Deref;
|
||||||
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
|
||||||
|
|
||||||
use libc::uintptr_t;
|
use async_std::path::{Path, PathBuf};
|
||||||
|
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||||
|
|
||||||
use crate::chat::*;
|
use crate::chat::*;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
|
use crate::dc_tools::duration_to_str;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::events::Event;
|
use crate::events::{Event, EventEmitter, Events};
|
||||||
use crate::imap::*;
|
use crate::job::{self, Action};
|
||||||
use crate::job::*;
|
use crate::key::{DcKey, SignedPublicKey};
|
||||||
use crate::job_thread::JobThread;
|
|
||||||
use crate::key::*;
|
|
||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
use crate::lot::Lot;
|
use crate::lot::Lot;
|
||||||
use crate::message::{self, Message, MsgId};
|
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||||
use crate::param::Params;
|
use crate::param::Params;
|
||||||
use crate::smtp::Smtp;
|
use crate::scheduler::Scheduler;
|
||||||
use crate::sql::Sql;
|
use crate::sql::Sql;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
/// Callback function type for [Context]
|
#[derive(Clone, Debug)]
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
///
|
|
||||||
/// * `context` - The context object as returned by [Context::new].
|
|
||||||
/// * `event` - One of the [Event] items.
|
|
||||||
/// * `data1` - Depends on the event parameter, see [Event].
|
|
||||||
/// * `data2` - Depends on the event parameter, see [Event].
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// This callback must return 0 unless stated otherwise in the event
|
|
||||||
/// description at [Event].
|
|
||||||
pub type ContextCallback = dyn Fn(&Context, Event) -> uintptr_t + Send + Sync;
|
|
||||||
|
|
||||||
#[derive(DebugStub)]
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
/// Database file path
|
pub(crate) inner: Arc<InnerContext>,
|
||||||
dbfile: PathBuf,
|
|
||||||
/// Blob directory path
|
|
||||||
blobdir: PathBuf,
|
|
||||||
pub sql: Sql,
|
|
||||||
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
|
|
||||||
pub probe_imap_network: Arc<RwLock<bool>>,
|
|
||||||
pub inbox_thread: Arc<RwLock<JobThread>>,
|
|
||||||
pub sentbox_thread: Arc<RwLock<JobThread>>,
|
|
||||||
pub mvbox_thread: Arc<RwLock<JobThread>>,
|
|
||||||
pub smtp: Arc<Mutex<Smtp>>,
|
|
||||||
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
|
|
||||||
pub oauth2_critical: Arc<Mutex<()>>,
|
|
||||||
#[debug_stub = "Callback"]
|
|
||||||
cb: Box<ContextCallback>,
|
|
||||||
pub os_name: Option<String>,
|
|
||||||
pub cmdline_sel_chat_id: Arc<RwLock<u32>>,
|
|
||||||
pub bob: Arc<RwLock<BobStatus>>,
|
|
||||||
pub last_smeared_timestamp: RwLock<i64>,
|
|
||||||
pub running_state: Arc<RwLock<RunningState>>,
|
|
||||||
/// Mutex to avoid generating the key for the user more than once.
|
|
||||||
pub generating_key_mutex: Mutex<()>,
|
|
||||||
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
impl Deref for Context {
|
||||||
|
type Target = InnerContext;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InnerContext {
|
||||||
|
/// Database file path
|
||||||
|
pub(crate) dbfile: PathBuf,
|
||||||
|
/// Blob directory path
|
||||||
|
pub(crate) blobdir: PathBuf,
|
||||||
|
pub(crate) sql: Sql,
|
||||||
|
pub(crate) os_name: Option<String>,
|
||||||
|
pub(crate) bob: RwLock<BobStatus>,
|
||||||
|
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.
|
||||||
|
pub(crate) generating_key_mutex: Mutex<()>,
|
||||||
|
/// Mutex to enforce only a single running oauth2 is running.
|
||||||
|
pub(crate) oauth2_mutex: Mutex<()>,
|
||||||
|
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||||
|
pub(crate) events: Events,
|
||||||
|
|
||||||
|
pub(crate) scheduler: RwLock<Scheduler>,
|
||||||
|
|
||||||
|
creation_time: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct RunningState {
|
pub struct RunningState {
|
||||||
pub ongoing_running: bool,
|
pub ongoing_running: bool,
|
||||||
shall_stop_ongoing: bool,
|
shall_stop_ongoing: bool,
|
||||||
|
cancel_sender: Option<Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return some info about deltachat-core
|
/// Return some info about deltachat-core
|
||||||
@@ -78,90 +73,106 @@ pub struct RunningState {
|
|||||||
/// actual keys and their values which will be present are not
|
/// actual keys and their values which will be present are not
|
||||||
/// guaranteed. Calling [Context::get_info] also includes information
|
/// guaranteed. Calling [Context::get_info] also includes information
|
||||||
/// about the context on top of the information here.
|
/// about the context on top of the information here.
|
||||||
pub fn get_info() -> HashMap<&'static str, String> {
|
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||||
let mut res = HashMap::new();
|
let mut res = BTreeMap::new();
|
||||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
|
||||||
res.insert(
|
let version =
|
||||||
"sqlite_thread_safe",
|
String::from_utf8(libsqlite3_sys::SQLITE_VERSION.to_vec()).expect("invalid version");
|
||||||
unsafe { rusqlite::ffi::sqlite3_threadsafe() }.to_string(),
|
res.insert("sqlite_version", version);
|
||||||
);
|
|
||||||
res.insert(
|
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||||
"arch",
|
|
||||||
(std::mem::size_of::<*mut libc::c_void>())
|
|
||||||
.wrapping_mul(8)
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
res.insert("level", "awesome".into());
|
res.insert("level", "awesome".into());
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Creates new context.
|
/// Creates new context.
|
||||||
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
|
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||||
let mut blob_fname = OsString::new();
|
let mut blob_fname = OsString::new();
|
||||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||||
blob_fname.push("-blobs");
|
blob_fname.push("-blobs");
|
||||||
let blobdir = dbfile.with_file_name(blob_fname);
|
let blobdir = dbfile.with_file_name(blob_fname);
|
||||||
if !blobdir.exists() {
|
if !blobdir.exists().await {
|
||||||
std::fs::create_dir_all(&blobdir)?;
|
async_std::fs::create_dir_all(&blobdir).await?;
|
||||||
}
|
}
|
||||||
Context::with_blobdir(cb, os_name, dbfile, blobdir)
|
Context::with_blobdir(os_name, dbfile, blobdir).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_blobdir(
|
pub async fn with_blobdir(
|
||||||
cb: Box<ContextCallback>,
|
|
||||||
os_name: String,
|
os_name: String,
|
||||||
dbfile: PathBuf,
|
dbfile: PathBuf,
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
) -> Result<Context> {
|
) -> Result<Context> {
|
||||||
ensure!(
|
ensure!(
|
||||||
blobdir.is_dir(),
|
blobdir.is_dir().await,
|
||||||
"Blobdir does not exist: {}",
|
"Blobdir does not exist: {}",
|
||||||
blobdir.display()
|
blobdir.display()
|
||||||
);
|
);
|
||||||
let ctx = Context {
|
|
||||||
|
let inner = InnerContext {
|
||||||
blobdir,
|
blobdir,
|
||||||
dbfile,
|
dbfile,
|
||||||
cb,
|
|
||||||
os_name: Some(os_name),
|
os_name: Some(os_name),
|
||||||
running_state: Arc::new(RwLock::new(Default::default())),
|
running_state: RwLock::new(Default::default()),
|
||||||
sql: Sql::new(),
|
sql: Sql::new(),
|
||||||
smtp: Arc::new(Mutex::new(Smtp::new())),
|
bob: RwLock::new(Default::default()),
|
||||||
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
|
|
||||||
oauth2_critical: Arc::new(Mutex::new(())),
|
|
||||||
bob: Arc::new(RwLock::new(Default::default())),
|
|
||||||
last_smeared_timestamp: RwLock::new(0),
|
last_smeared_timestamp: RwLock::new(0),
|
||||||
cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
|
|
||||||
inbox_thread: Arc::new(RwLock::new(JobThread::new(
|
|
||||||
"INBOX",
|
|
||||||
"configured_inbox_folder",
|
|
||||||
Imap::new(),
|
|
||||||
))),
|
|
||||||
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
|
|
||||||
"SENTBOX",
|
|
||||||
"configured_sentbox_folder",
|
|
||||||
Imap::new(),
|
|
||||||
))),
|
|
||||||
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
|
|
||||||
"MVBOX",
|
|
||||||
"configured_mvbox_folder",
|
|
||||||
Imap::new(),
|
|
||||||
))),
|
|
||||||
probe_imap_network: Arc::new(RwLock::new(false)),
|
|
||||||
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
|
|
||||||
generating_key_mutex: Mutex::new(()),
|
generating_key_mutex: Mutex::new(()),
|
||||||
|
oauth2_mutex: Mutex::new(()),
|
||||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||||
|
events: Events::default(),
|
||||||
|
scheduler: RwLock::new(Scheduler::Stopped),
|
||||||
|
creation_time: std::time::SystemTime::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure!(
|
let ctx = Context {
|
||||||
ctx.sql.open(&ctx, &ctx.dbfile, false),
|
inner: Arc::new(inner),
|
||||||
"Failed opening sqlite database"
|
};
|
||||||
);
|
|
||||||
|
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
|
||||||
|
|
||||||
Ok(ctx)
|
Ok(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts the IO scheduler.
|
||||||
|
pub async fn start_io(&self) {
|
||||||
|
info!(self, "starting IO");
|
||||||
|
if self.is_io_running().await {
|
||||||
|
info!(self, "IO is already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let l = &mut *self.inner.scheduler.write().await;
|
||||||
|
l.start(self.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the underlying SQL instance.
|
||||||
|
///
|
||||||
|
/// Warning: this is only here for testing, not part of the public API.
|
||||||
|
#[cfg(feature = "internals")]
|
||||||
|
pub fn sql(&self) -> &Sql {
|
||||||
|
&self.inner.sql
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns database file path.
|
/// Returns database file path.
|
||||||
pub fn get_dbfile(&self) -> &Path {
|
pub fn get_dbfile(&self) -> &Path {
|
||||||
self.dbfile.as_path()
|
self.dbfile.as_path()
|
||||||
@@ -172,49 +183,57 @@ impl Context {
|
|||||||
self.blobdir.as_path()
|
self.blobdir.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call_cb(&self, event: Event) -> uintptr_t {
|
/// Emits a single event.
|
||||||
(*self.cb)(self, event)
|
pub fn emit_event(&self, event: Event) {
|
||||||
|
self.events.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/// Get the next queued event.
|
||||||
* Ongoing process allocation/free/check
|
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||||
******************************************************************************/
|
self.events.get_emitter()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn alloc_ongoing(&self) -> bool {
|
// Ongoing process allocation/free/check
|
||||||
if self.has_ongoing() {
|
|
||||||
warn!(self, "There is already another ongoing process running.",);
|
|
||||||
|
|
||||||
false
|
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||||
} else {
|
if self.has_ongoing().await {
|
||||||
let s_a = self.running_state.clone();
|
bail!("There is already another ongoing process running.");
|
||||||
let mut s = s_a.write().unwrap();
|
|
||||||
|
|
||||||
s.ongoing_running = true;
|
|
||||||
s.shall_stop_ongoing = false;
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let s_a = &self.running_state;
|
||||||
|
let mut s = s_a.write().await;
|
||||||
|
|
||||||
|
s.ongoing_running = true;
|
||||||
|
s.shall_stop_ongoing = false;
|
||||||
|
let (sender, receiver) = channel(1);
|
||||||
|
s.cancel_sender = Some(sender);
|
||||||
|
|
||||||
|
Ok(receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free_ongoing(&self) {
|
pub async fn free_ongoing(&self) {
|
||||||
let s_a = self.running_state.clone();
|
let s_a = &self.running_state;
|
||||||
let mut s = s_a.write().unwrap();
|
let mut s = s_a.write().await;
|
||||||
|
|
||||||
s.ongoing_running = false;
|
s.ongoing_running = false;
|
||||||
s.shall_stop_ongoing = true;
|
s.shall_stop_ongoing = true;
|
||||||
|
s.cancel_sender.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_ongoing(&self) -> bool {
|
pub async fn has_ongoing(&self) -> bool {
|
||||||
let s_a = self.running_state.clone();
|
let s_a = &self.running_state;
|
||||||
let s = s_a.read().unwrap();
|
let s = s_a.read().await;
|
||||||
|
|
||||||
s.ongoing_running || !s.shall_stop_ongoing
|
s.ongoing_running || !s.shall_stop_ongoing
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signal an ongoing process to stop.
|
/// Signal an ongoing process to stop.
|
||||||
pub fn stop_ongoing(&self) {
|
pub async fn stop_ongoing(&self) {
|
||||||
let s_a = self.running_state.clone();
|
let s_a = &self.running_state;
|
||||||
let mut s = s_a.write().unwrap();
|
let mut s = s_a.write().await;
|
||||||
|
if let Some(cancel) = s.cancel_sender.take() {
|
||||||
|
cancel.send(()).await;
|
||||||
|
}
|
||||||
|
|
||||||
if s.ongoing_running && !s.shall_stop_ongoing {
|
if s.ongoing_running && !s.shall_stop_ongoing {
|
||||||
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
||||||
@@ -224,69 +243,71 @@ impl Context {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shall_stop_ongoing(&self) -> bool {
|
pub async fn shall_stop_ongoing(&self) -> bool {
|
||||||
self.running_state
|
self.running_state.read().await.shall_stop_ongoing
|
||||||
.clone()
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.shall_stop_ongoing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* UI chat/message related API
|
* UI chat/message related API
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
pub fn get_info(&self) -> HashMap<&'static str, String> {
|
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
|
||||||
let unset = "0";
|
let unset = "0";
|
||||||
let l = LoginParam::from_database(self, "");
|
let l = LoginParam::from_database(self, "").await;
|
||||||
let l2 = LoginParam::from_database(self, "configured_");
|
let l2 = LoginParam::from_database(self, "configured_").await;
|
||||||
let displayname = self.get_config(Config::Displayname);
|
let displayname = self.get_config(Config::Displayname).await;
|
||||||
let chats = get_chat_cnt(self) as usize;
|
let chats = get_chat_cnt(self).await as usize;
|
||||||
let real_msgs = message::get_real_msg_cnt(self) as usize;
|
let real_msgs = message::get_real_msg_cnt(self).await as usize;
|
||||||
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize;
|
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
|
||||||
let contacts = Contact::get_real_cnt(self) as usize;
|
let contacts = Contact::get_real_cnt(self).await as usize;
|
||||||
let is_configured = self.get_config_int(Config::Configured);
|
let is_configured = self.get_config_int(Config::Configured).await;
|
||||||
let dbversion = self
|
let dbversion = self
|
||||||
.sql
|
.sql
|
||||||
.get_raw_config_int(self, "dbversion")
|
.get_raw_config_int("dbversion")
|
||||||
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let journal_mode = self
|
||||||
|
.sql
|
||||||
|
.query_value("PRAGMA journal_mode;", paramsx![])
|
||||||
|
.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);
|
let prv_key_cnt: Option<i32> = self
|
||||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled);
|
.sql
|
||||||
let bcc_self = self.get_config_int(Config::BccSelf);
|
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
let prv_key_cnt: Option<isize> =
|
let pub_key_cnt: Option<i32> = self
|
||||||
self.sql
|
.sql
|
||||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", rusqlite::NO_PARAMS);
|
.query_value("SELECT COUNT(*) FROM acpeerstates;", paramsx![])
|
||||||
|
.await
|
||||||
let pub_key_cnt: Option<isize> = self.sql.query_get_value(
|
.ok();
|
||||||
self,
|
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||||
"SELECT COUNT(*) FROM acpeerstates;",
|
Ok(key) => key.fingerprint().hex(),
|
||||||
rusqlite::NO_PARAMS,
|
Err(err) => format!("<key failure: {}>", err),
|
||||||
);
|
|
||||||
|
|
||||||
let fingerprint_str = if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql) {
|
|
||||||
key.fingerprint()
|
|
||||||
} else {
|
|
||||||
"<Not yet calculated>".into()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inbox_watch = self.get_config_int(Config::InboxWatch);
|
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
|
||||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch);
|
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
|
||||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch);
|
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
|
||||||
let mvbox_move = self.get_config_int(Config::MvboxMove);
|
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
|
||||||
let folders_configured = self
|
let folders_configured = self
|
||||||
.sql
|
.sql
|
||||||
.get_raw_config_int(self, "folders_configured")
|
.get_raw_config_int("folders_configured")
|
||||||
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let configured_sentbox_folder = self
|
let configured_sentbox_folder = self
|
||||||
.sql
|
.get_config(Config::ConfiguredSentboxFolder)
|
||||||
.get_raw_config(self, "configured_sentbox_folder")
|
.await
|
||||||
.unwrap_or_else(|| "<unset>".to_string());
|
.unwrap_or_else(|| "<unset>".to_string());
|
||||||
let configured_mvbox_folder = self
|
let configured_mvbox_folder = self
|
||||||
.sql
|
.get_config(Config::ConfiguredMvboxFolder)
|
||||||
.get_raw_config(self, "configured_mvbox_folder")
|
.await
|
||||||
.unwrap_or_else(|| "<unset>".to_string());
|
.unwrap_or_else(|| "<unset>".to_string());
|
||||||
|
|
||||||
let mut res = get_info();
|
let mut res = get_info();
|
||||||
@@ -296,11 +317,13 @@ impl Context {
|
|||||||
res.insert("number_of_contacts", contacts.to_string());
|
res.insert("number_of_contacts", contacts.to_string());
|
||||||
res.insert("database_dir", self.get_dbfile().display().to_string());
|
res.insert("database_dir", self.get_dbfile().display().to_string());
|
||||||
res.insert("database_version", dbversion.to_string());
|
res.insert("database_version", dbversion.to_string());
|
||||||
|
res.insert("journal_mode", journal_mode);
|
||||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||||
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
|
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
|
||||||
res.insert(
|
res.insert(
|
||||||
"selfavatar",
|
"selfavatar",
|
||||||
self.get_config(Config::Selfavatar)
|
self.get_config(Config::Selfavatar)
|
||||||
|
.await
|
||||||
.unwrap_or_else(|| "<unset>".to_string()),
|
.unwrap_or_else(|| "<unset>".to_string()),
|
||||||
);
|
);
|
||||||
res.insert("is_configured", is_configured.to_string());
|
res.insert("is_configured", is_configured.to_string());
|
||||||
@@ -326,42 +349,38 @@ impl Context {
|
|||||||
);
|
);
|
||||||
res.insert("fingerprint", fingerprint_str);
|
res.insert("fingerprint", fingerprint_str);
|
||||||
|
|
||||||
|
let elapsed = self.creation_time.elapsed();
|
||||||
|
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fresh_msgs(&self) -> Vec<MsgId> {
|
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
|
||||||
let show_deaddrop = 0;
|
let show_deaddrop: i32 = 0;
|
||||||
self.sql
|
self.sql
|
||||||
.query_map(
|
.query_values(
|
||||||
concat!(
|
r#"
|
||||||
"SELECT m.id",
|
SELECT m.id
|
||||||
" FROM msgs m",
|
FROM msgs m
|
||||||
" LEFT JOIN contacts ct",
|
LEFT JOIN contacts ct
|
||||||
" ON m.from_id=ct.id",
|
ON m.from_id=ct.id
|
||||||
" LEFT JOIN chats c",
|
LEFT JOIN chats c
|
||||||
" ON m.chat_id=c.id",
|
ON m.chat_id=c.id
|
||||||
" WHERE m.state=?",
|
WHERE m.state=?
|
||||||
" AND m.hidden=0",
|
AND m.hidden=0
|
||||||
" AND m.chat_id>?",
|
AND m.chat_id>?
|
||||||
" AND ct.blocked=0",
|
AND ct.blocked=0
|
||||||
" AND (c.blocked=0 OR c.blocked=?)",
|
AND (c.blocked=0 OR c.blocked=?)
|
||||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
ORDER BY m.timestamp DESC,m.id DESC;
|
||||||
),
|
"#,
|
||||||
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
paramsx![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||||
|row| row.get::<_, MsgId>(0),
|
|
||||||
|rows| {
|
|
||||||
let mut ret = Vec::new();
|
|
||||||
for row in rows {
|
|
||||||
ret.push(row?);
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn search_msgs(&self, chat_id: u32, query: impl AsRef<str>) -> Vec<MsgId> {
|
pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
|
||||||
let real_query = query.as_ref().trim();
|
let real_query = query.as_ref().trim();
|
||||||
if real_query.is_empty() {
|
if real_query.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -369,114 +388,100 @@ impl Context {
|
|||||||
let strLikeInText = format!("%{}%", real_query);
|
let strLikeInText = format!("%{}%", real_query);
|
||||||
let strLikeBeg = format!("{}%", real_query);
|
let strLikeBeg = format!("{}%", real_query);
|
||||||
|
|
||||||
let query = if 0 != chat_id {
|
let query = if !chat_id.is_unset() {
|
||||||
concat!(
|
r#"
|
||||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
SELECT m.id
|
||||||
" FROM msgs m",
|
FROM msgs
|
||||||
" LEFT JOIN contacts ct",
|
LEFT JOIN contacts ct
|
||||||
" ON m.from_id=ct.id",
|
ON m.from_id=ct.id
|
||||||
" WHERE m.chat_id=?",
|
WHERE m.chat_id=?
|
||||||
" AND m.hidden=0",
|
AND m.hidden=0
|
||||||
" AND ct.blocked=0",
|
AND ct.blocked=0
|
||||||
" AND (txt LIKE ? OR ct.name LIKE ?)",
|
AND (txt LIKE ? OR ct.name LIKE ?)
|
||||||
" ORDER BY m.timestamp,m.id;"
|
ORDER BY m.timestamp,m.id;
|
||||||
)
|
"#
|
||||||
} else {
|
} else {
|
||||||
concat!(
|
r#"
|
||||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
SELECT m.id
|
||||||
" FROM msgs m",
|
FROM msgs m
|
||||||
" LEFT JOIN contacts ct",
|
LEFT JOIN contacts ct
|
||||||
" ON m.from_id=ct.id",
|
ON m.from_id=ct.id
|
||||||
" LEFT JOIN chats c",
|
LEFT JOIN chats c
|
||||||
" ON m.chat_id=c.id",
|
ON m.chat_id=c.id
|
||||||
" WHERE m.chat_id>9",
|
WHERE m.chat_id>9
|
||||||
" AND m.hidden=0",
|
AND m.hidden=0
|
||||||
" AND (c.blocked=0 OR c.blocked=?)",
|
AND (c.blocked=0 OR c.blocked=?)
|
||||||
" AND ct.blocked=0",
|
AND ct.blocked=0
|
||||||
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
|
AND (m.txt LIKE ? OR ct.name LIKE ?)
|
||||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
ORDER BY m.timestamp DESC,m.id DESC;
|
||||||
)
|
"#
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sql
|
self.sql
|
||||||
.query_map(
|
.query_values(query, paramsx![chat_id, strLikeInText, strLikeBeg])
|
||||||
query,
|
.await
|
||||||
params![chat_id as i32, &strLikeInText, &strLikeBeg],
|
|
||||||
|row| row.get::<_, MsgId>("id"),
|
|
||||||
|rows| {
|
|
||||||
let mut ret = Vec::new();
|
|
||||||
for id in rows {
|
|
||||||
ret.push(id?);
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
|
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||||
folder_name.as_ref() == "INBOX"
|
self.get_config(Config::ConfiguredInboxFolder).await
|
||||||
|
== Some(folder_name.as_ref().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
|
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||||
let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder");
|
self.get_config(Config::ConfiguredSentboxFolder).await
|
||||||
if let Some(name) = sentbox_name {
|
== Some(folder_name.as_ref().to_string())
|
||||||
name == folder_name.as_ref()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
|
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||||
let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder");
|
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||||
|
== Some(folder_name.as_ref().to_string())
|
||||||
if let Some(name) = mvbox_name {
|
|
||||||
name == folder_name.as_ref()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
|
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
|
||||||
if !self.get_config_bool(Config::MvboxMove) {
|
if !self.get_config_bool(Config::MvboxMove).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_mvbox(folder) {
|
if self.is_mvbox(folder).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(msg) = Message::load_from_db(self, msg_id) {
|
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
|
||||||
if msg.is_setupmessage() {
|
if msg.is_setupmessage() {
|
||||||
// do not move setup messages;
|
// do not move setup messages;
|
||||||
// there may be a non-delta device that wants to handle it
|
// there may be a non-delta device that wants to handle it
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1 = dc message, 2 = reply to dc message
|
match msg.is_dc_message {
|
||||||
if 0 != msg.is_dc_message {
|
MessengerMessage::No => {}
|
||||||
job_add(
|
MessengerMessage::Yes | MessengerMessage::Reply => {
|
||||||
self,
|
job::add(
|
||||||
Action::MoveMsg,
|
self,
|
||||||
msg.id.to_u32() as i32,
|
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
|
||||||
Params::new(),
|
)
|
||||||
0,
|
.await;
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Context {
|
impl InnerContext {
|
||||||
fn drop(&mut self) {
|
async fn is_io_running(&self) -> bool {
|
||||||
info!(self, "disconnecting inbox-thread",);
|
self.scheduler.read().await.is_running()
|
||||||
self.inbox_thread.read().unwrap().imap.disconnect(self);
|
}
|
||||||
info!(self, "disconnecting sentbox-thread",);
|
|
||||||
self.sentbox_thread.read().unwrap().imap.disconnect(self);
|
async fn stop_io(&self) {
|
||||||
info!(self, "disconnecting mvbox-thread",);
|
assert!(self.is_io_running().await, "context is already stopped");
|
||||||
self.mvbox_thread.read().unwrap().imap.disconnect(self);
|
let token = {
|
||||||
info!(self, "disconnecting SMTP");
|
let lock = &*self.scheduler.read().await;
|
||||||
self.smtp.clone().lock().unwrap().disconnect();
|
lock.pre_stop().await
|
||||||
self.sql.close(self);
|
};
|
||||||
|
{
|
||||||
|
let lock = &mut *self.scheduler.write().await;
|
||||||
|
lock.stop(token).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,39 +490,18 @@ impl Default for RunningState {
|
|||||||
RunningState {
|
RunningState {
|
||||||
ongoing_running: false,
|
ongoing_running: false,
|
||||||
shall_stop_ongoing: true,
|
shall_stop_ongoing: true,
|
||||||
|
cancel_sender: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct BobStatus {
|
pub(crate) struct BobStatus {
|
||||||
pub expects: i32,
|
pub expects: i32,
|
||||||
pub status: i32,
|
pub status: i32,
|
||||||
pub qr_scan: Option<Lot>,
|
pub qr_scan: Option<Lot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum PerformJobsNeeded {
|
|
||||||
Not,
|
|
||||||
AtOnce,
|
|
||||||
AvoidDos,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PerformJobsNeeded {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Not
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct SmtpState {
|
|
||||||
pub idle: bool,
|
|
||||||
pub suspended: bool,
|
|
||||||
pub doing_jobs: bool,
|
|
||||||
pub perform_jobs_needed: PerformJobsNeeded,
|
|
||||||
pub probe_network: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_version_str() -> &'static str {
|
pub fn get_version_str() -> &'static str {
|
||||||
&DC_VERSION_STR
|
&DC_VERSION_STR
|
||||||
}
|
}
|
||||||
@@ -528,81 +512,81 @@ mod tests {
|
|||||||
|
|
||||||
use crate::test_utils::*;
|
use crate::test_utils::*;
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_wrong_db() {
|
async fn test_wrong_db() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
std::fs::write(&dbfile, b"123").unwrap();
|
std::fs::write(&dbfile, b"123").unwrap();
|
||||||
let res = Context::new(Box::new(|_, _| 0), "FakeOs".into(), dbfile);
|
let res = Context::new("FakeOs".into(), dbfile.into()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_get_fresh_msgs() {
|
async fn test_get_fresh_msgs() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let fresh = t.ctx.get_fresh_msgs();
|
let fresh = t.ctx.get_fresh_msgs().await;
|
||||||
assert!(fresh.is_empty())
|
assert!(fresh.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_blobdir_exists() {
|
async fn test_blobdir_exists() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
|
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||||
assert!(blobdir.is_dir());
|
assert!(blobdir.is_dir());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_wrong_blogdir() {
|
async fn test_wrong_blogdir() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||||
std::fs::write(&blobdir, b"123").unwrap();
|
std::fs::write(&blobdir, b"123").unwrap();
|
||||||
let res = Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile);
|
let res = Context::new("FakeOS".into(), dbfile.into()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_sqlite_parent_not_exists() {
|
async fn test_sqlite_parent_not_exists() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let subdir = tmp.path().join("subdir");
|
let subdir = tmp.path().join("subdir");
|
||||||
let dbfile = subdir.join("db.sqlite");
|
let dbfile = subdir.join("db.sqlite");
|
||||||
let dbfile2 = dbfile.clone();
|
let dbfile2 = dbfile.clone();
|
||||||
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
|
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||||
assert!(subdir.is_dir());
|
assert!(subdir.is_dir());
|
||||||
assert!(dbfile2.is_file());
|
assert!(dbfile2.is_file());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_with_empty_blobdir() {
|
async fn test_with_empty_blobdir() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
let blobdir = PathBuf::new();
|
let blobdir = PathBuf::new();
|
||||||
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
|
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_with_blobdir_not_exists() {
|
async fn test_with_blobdir_not_exists() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
let blobdir = tmp.path().join("blobs");
|
let blobdir = tmp.path().join("blobs");
|
||||||
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
|
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn no_crashes_on_context_deref() {
|
async fn no_crashes_on_context_deref() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
std::mem::drop(t.ctx);
|
std::mem::drop(t.ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_get_info() {
|
async fn test_get_info() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
let info = t.ctx.get_info();
|
let info = t.ctx.get_info().await;
|
||||||
assert!(info.get("database_dir").is_some());
|
assert!(info.get("database_dir").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,273 +0,0 @@
|
|||||||
use crate::dehtml::*;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub struct Simplify {
|
|
||||||
pub is_forwarded: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return index of footer line in vector of message lines, or vector length if
|
|
||||||
/// no footer is found.
|
|
||||||
///
|
|
||||||
/// Also return whether not-standard (rfc3676, §4.3) footer is found.
|
|
||||||
fn find_message_footer(lines: &[&str]) -> (usize, bool) {
|
|
||||||
for (ix, &line) in lines.iter().enumerate() {
|
|
||||||
// quoted-printable may encode `-- ` to `-- =20` which is converted
|
|
||||||
// back to `-- `
|
|
||||||
match line {
|
|
||||||
"-- " | "-- " => return (ix, false),
|
|
||||||
"--" | "---" | "----" => return (ix, true),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(lines.len(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Simplify {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Simplify {
|
|
||||||
is_forwarded: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simplify and normalise text: Remove quotes, signatures, unnecessary
|
|
||||||
/// lineends etc.
|
|
||||||
/// The data returned from simplify() must be free()'d when no longer used.
|
|
||||||
pub fn simplify(&mut self, input: &str, is_html: bool, is_msgrmsg: bool) -> String {
|
|
||||||
let mut out = if is_html {
|
|
||||||
dehtml(input)
|
|
||||||
} else {
|
|
||||||
input.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
out.retain(|c| c != '\r');
|
|
||||||
out = self.simplify_plain_text(&out, is_msgrmsg);
|
|
||||||
out.retain(|c| c != '\r');
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simplify Plain Text
|
|
||||||
*/
|
|
||||||
#[allow(non_snake_case, clippy::mut_range_bound, clippy::needless_range_loop)]
|
|
||||||
fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
|
|
||||||
/* This function ...
|
|
||||||
... removes all text after the line `-- ` (footer mark)
|
|
||||||
... removes full quotes at the beginning and at the end of the text -
|
|
||||||
these are all lines starting with the character `>`
|
|
||||||
... remove a non-empty line before the removed quote (contains sth. like "On 2.9.2016, Bjoern wrote:" in different formats and lanugages) */
|
|
||||||
/* split the given buffer into lines */
|
|
||||||
let lines: Vec<_> = buf_terminated.split('\n').collect();
|
|
||||||
let mut l_first: usize = 0;
|
|
||||||
let mut is_cut_at_begin = false;
|
|
||||||
let (mut l_last, mut is_cut_at_end) = find_message_footer(&lines);
|
|
||||||
|
|
||||||
if l_last > l_first + 2 {
|
|
||||||
let line0 = lines[l_first];
|
|
||||||
let line1 = lines[l_first + 1];
|
|
||||||
let line2 = lines[l_first + 2];
|
|
||||||
if line0 == "---------- Forwarded message ----------"
|
|
||||||
&& line1.starts_with("From: ")
|
|
||||||
&& line2.is_empty()
|
|
||||||
{
|
|
||||||
self.is_forwarded = true;
|
|
||||||
l_first += 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for l in l_first..l_last {
|
|
||||||
let line = lines[l];
|
|
||||||
if line == "-----"
|
|
||||||
|| line == "_____"
|
|
||||||
|| line == "====="
|
|
||||||
|| line == "*****"
|
|
||||||
|| line == "~~~~~"
|
|
||||||
{
|
|
||||||
l_last = l;
|
|
||||||
is_cut_at_end = true;
|
|
||||||
/* done */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !is_msgrmsg {
|
|
||||||
let mut l_lastQuotedLine = None;
|
|
||||||
for l in (l_first..l_last).rev() {
|
|
||||||
let line = lines[l];
|
|
||||||
if is_plain_quote(line) {
|
|
||||||
l_lastQuotedLine = Some(l)
|
|
||||||
} else if !is_empty_line(line) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(last_quoted_line) = l_lastQuotedLine {
|
|
||||||
l_last = last_quoted_line;
|
|
||||||
is_cut_at_end = true;
|
|
||||||
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
|
|
||||||
l_last -= 1
|
|
||||||
}
|
|
||||||
if l_last > 1 {
|
|
||||||
let line = lines[l_last - 1];
|
|
||||||
if is_quoted_headline(line) {
|
|
||||||
l_last -= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !is_msgrmsg {
|
|
||||||
let mut l_lastQuotedLine_0 = None;
|
|
||||||
let mut hasQuotedHeadline = 0;
|
|
||||||
for l in l_first..l_last {
|
|
||||||
let line = lines[l];
|
|
||||||
if is_plain_quote(line) {
|
|
||||||
l_lastQuotedLine_0 = Some(l)
|
|
||||||
} else if !is_empty_line(line) {
|
|
||||||
if is_quoted_headline(line)
|
|
||||||
&& 0 == hasQuotedHeadline
|
|
||||||
&& l_lastQuotedLine_0.is_none()
|
|
||||||
{
|
|
||||||
hasQuotedHeadline = 1i32
|
|
||||||
} else {
|
|
||||||
/* non-quoting line found */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(last_quoted_line) = l_lastQuotedLine_0 {
|
|
||||||
l_first = last_quoted_line + 1;
|
|
||||||
is_cut_at_begin = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* re-create buffer from the remaining lines */
|
|
||||||
let mut ret = String::new();
|
|
||||||
if is_cut_at_begin {
|
|
||||||
ret += "[...]";
|
|
||||||
}
|
|
||||||
/* we write empty lines only in case and non-empty line follows */
|
|
||||||
let mut pending_linebreaks = 0;
|
|
||||||
let mut content_lines_added = 0;
|
|
||||||
for l in l_first..l_last {
|
|
||||||
let line = lines[l];
|
|
||||||
if is_empty_line(line) {
|
|
||||||
pending_linebreaks += 1
|
|
||||||
} else {
|
|
||||||
if 0 != content_lines_added {
|
|
||||||
if pending_linebreaks > 2i32 {
|
|
||||||
pending_linebreaks = 2i32
|
|
||||||
}
|
|
||||||
while 0 != pending_linebreaks {
|
|
||||||
ret += "\n";
|
|
||||||
pending_linebreaks -= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// the incoming message might contain invalid UTF8
|
|
||||||
ret += line;
|
|
||||||
content_lines_added += 1;
|
|
||||||
pending_linebreaks = 1i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_cut_at_end && (!is_cut_at_begin || 0 != content_lines_added) {
|
|
||||||
ret += " [...]";
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tools
|
|
||||||
*/
|
|
||||||
fn is_empty_line(buf: &str) -> bool {
|
|
||||||
// XXX: can it be simplified to buf.chars().all(|c| c.is_whitespace())?
|
|
||||||
//
|
|
||||||
// Strictly speaking, it is not equivalent (^A is not whitespace, but less than ' '),
|
|
||||||
// but having control sequences in email body?!
|
|
||||||
//
|
|
||||||
// See discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
|
|
||||||
for c in buf.chars() {
|
|
||||||
if c > ' ' {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_quoted_headline(buf: &str) -> bool {
|
|
||||||
/* This function may be called for the line _directly_ before a quote.
|
|
||||||
The function checks if the line contains sth. like "On 01.02.2016, xy@z wrote:" in various languages.
|
|
||||||
- Currently, we simply check if the last character is a ':'.
|
|
||||||
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
|
|
||||||
|
|
||||||
buf.len() <= 80 && buf.ends_with(':')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_plain_quote(buf: &str) -> bool {
|
|
||||||
buf.starts_with('>')
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use proptest::prelude::*;
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test]
|
|
||||||
// proptest does not support [[:graphical:][:space:]] regex.
|
|
||||||
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
|
|
||||||
let output = Simplify::new().simplify_plain_text(&input, true);
|
|
||||||
assert!(output.split('\n').all(|s| s != "-- "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simplify_trim() {
|
|
||||||
let mut simplify = Simplify::new();
|
|
||||||
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
|
|
||||||
let plain = simplify.simplify(html, true, false);
|
|
||||||
|
|
||||||
assert_eq!(plain, "line1\nline2");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simplify_parse_href() {
|
|
||||||
let mut simplify = Simplify::new();
|
|
||||||
let html = "<a href=url>text</a";
|
|
||||||
let plain = simplify.simplify(html, true, false);
|
|
||||||
|
|
||||||
assert_eq!(plain, "[text](url)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simplify_bold_text() {
|
|
||||||
let mut simplify = Simplify::new();
|
|
||||||
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
|
|
||||||
let plain = simplify.simplify(html, true, false);
|
|
||||||
|
|
||||||
assert_eq!(plain, "text *bold*<>");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simplify_html_encoded() {
|
|
||||||
let mut simplify = Simplify::new();
|
|
||||||
let html =
|
|
||||||
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
|
||||||
|
|
||||||
let plain = simplify.simplify(html, true, false);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
plain,
|
|
||||||
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simplify_utilities() {
|
|
||||||
assert!(is_empty_line(" \t"));
|
|
||||||
assert!(is_empty_line(""));
|
|
||||||
assert!(is_empty_line(" \r"));
|
|
||||||
assert!(!is_empty_line(" x"));
|
|
||||||
assert!(is_plain_quote("> hello world"));
|
|
||||||
assert!(is_plain_quote(">>"));
|
|
||||||
assert!(!is_plain_quote("Life is pain"));
|
|
||||||
assert!(!is_plain_quote(""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
466
src/dc_tools.rs
466
src/dc_tools.rs
@@ -3,26 +3,27 @@
|
|||||||
|
|
||||||
use core::cmp::{max, min};
|
use core::cmp::{max, min};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::{Path, PathBuf};
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::SystemTime;
|
use std::time::{Duration, SystemTime};
|
||||||
use std::{fmt, fs};
|
|
||||||
|
|
||||||
|
use async_std::path::{Path, PathBuf};
|
||||||
|
use async_std::{fs, io};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, Error};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
|
|
||||||
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||||
0 != v && 0 == v & (v - 1)
|
0 != v && 0 == v & (v - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
|
/// Shortens a string to a specified length and adds "[...]" to the
|
||||||
/// the shortened string.
|
/// end of the shortened string.
|
||||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
|
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||||
let ellipse = if do_unwrap { "..." } else { "[...]" };
|
let ellipse = "[...]";
|
||||||
|
|
||||||
let count = buf.chars().count();
|
let count = buf.chars().count();
|
||||||
if approx_chars > 0 && count > approx_chars + ellipse.len() {
|
if approx_chars > 0 && count > approx_chars + ellipse.len() {
|
||||||
@@ -49,8 +50,8 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Co
|
|||||||
/// - harmonize together while being different enough
|
/// - harmonize together while being different enough
|
||||||
/// (therefore, we cannot just use random rgb colors :)
|
/// (therefore, we cannot just use random rgb colors :)
|
||||||
const COLORS: [u32; 16] = [
|
const COLORS: [u32; 16] = [
|
||||||
0xe56555, 0xf28c48, 0x8e85ee, 0x76c84d, 0x5bb6cc, 0x549cdd, 0xd25c99, 0xb37800, 0xf23030,
|
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
|
||||||
0x39b249, 0xbb243b, 0x964078, 0x66874f, 0x308ab9, 0x127ed0, 0xbe450c,
|
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
||||||
@@ -59,7 +60,7 @@ pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
|||||||
let bytes = str_lower.as_bytes();
|
let bytes = str_lower.as_bytes();
|
||||||
for (i, byte) in bytes.iter().enumerate() {
|
for (i, byte) in bytes.iter().enumerate() {
|
||||||
checksum += (i + 1) * *byte as usize;
|
checksum += (i + 1) * *byte as usize;
|
||||||
checksum %= 0xffffff;
|
checksum %= 0x00ff_ffff;
|
||||||
}
|
}
|
||||||
let color_index = checksum % COLORS.len();
|
let color_index = checksum % COLORS.len();
|
||||||
|
|
||||||
@@ -75,6 +76,14 @@ pub fn dc_timestamp_to_str(wanted: i64) -> String {
|
|||||||
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn duration_to_str(duration: Duration) -> String {
|
||||||
|
let secs = duration.as_secs();
|
||||||
|
let h = secs / 3600;
|
||||||
|
let m = (secs % 3600) / 60;
|
||||||
|
let s = (secs % 3600) % 60;
|
||||||
|
format!("{}h {}m {}s", h, m, s)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_gm2local_offset() -> i64 {
|
pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||||
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
||||||
the function may return negative values. */
|
the function may return negative values. */
|
||||||
@@ -99,9 +108,9 @@ const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
|||||||
// returns the currently smeared timestamp,
|
// returns the currently smeared timestamp,
|
||||||
// may be used to check if call to dc_create_smeared_timestamp() is needed or not.
|
// may be used to check if call to dc_create_smeared_timestamp() is needed or not.
|
||||||
// the returned timestamp MUST NOT be used to be sent out or saved in the database!
|
// the returned timestamp MUST NOT be used to be sent out or saved in the database!
|
||||||
pub(crate) fn dc_smeared_time(context: &Context) -> i64 {
|
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||||
let mut now = time();
|
let mut now = time();
|
||||||
let ts = *context.last_smeared_timestamp.read().unwrap();
|
let ts = *context.last_smeared_timestamp.read().await;
|
||||||
if ts >= now {
|
if ts >= now {
|
||||||
now = ts + 1;
|
now = ts + 1;
|
||||||
}
|
}
|
||||||
@@ -110,11 +119,11 @@ pub(crate) fn dc_smeared_time(context: &Context) -> i64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns a timestamp that is guaranteed to be unique.
|
// returns a timestamp that is guaranteed to be unique.
|
||||||
pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
||||||
let now = time();
|
let now = time();
|
||||||
let mut ret = now;
|
let mut ret = now;
|
||||||
|
|
||||||
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap();
|
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
|
||||||
if ret <= *last_smeared_timestamp {
|
if ret <= *last_smeared_timestamp {
|
||||||
ret = *last_smeared_timestamp + 1;
|
ret = *last_smeared_timestamp + 1;
|
||||||
if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE {
|
if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE {
|
||||||
@@ -129,12 +138,12 @@ pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
|||||||
// creates `count` timestamps that are guaranteed to be unique.
|
// creates `count` timestamps that are guaranteed to be unique.
|
||||||
// the frist created timestamps is returned directly,
|
// the frist created timestamps is returned directly,
|
||||||
// get the other timestamps just by adding 1..count-1
|
// get the other timestamps just by adding 1..count-1
|
||||||
pub(crate) fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
|
pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
|
||||||
let now = time();
|
let now = time();
|
||||||
let count = count as i64;
|
let count = count as i64;
|
||||||
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
|
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
|
||||||
|
|
||||||
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap();
|
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
|
||||||
start = max(*last_smeared_timestamp + 1, start);
|
start = max(*last_smeared_timestamp + 1, start);
|
||||||
|
|
||||||
*last_smeared_timestamp = start + count - 1;
|
*last_smeared_timestamp = start + count - 1;
|
||||||
@@ -222,45 +231,7 @@ pub(crate) fn dc_extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function returns a sanitized basename that does not contain
|
|
||||||
// win/linux path separators and also not any non-ascii chars
|
|
||||||
fn get_safe_basename(filename: &str) -> String {
|
|
||||||
// return the (potentially mangled) basename of the input filename
|
|
||||||
// this might be a path that comes in from another operating system
|
|
||||||
let mut index: usize = 0;
|
|
||||||
|
|
||||||
if let Some(unix_index) = filename.rfind('/') {
|
|
||||||
index = unix_index + 1;
|
|
||||||
}
|
|
||||||
if let Some(win_index) = filename.rfind('\\') {
|
|
||||||
index = max(index, win_index + 1);
|
|
||||||
}
|
|
||||||
if index >= filename.len() {
|
|
||||||
"nobasename".to_string()
|
|
||||||
} else {
|
|
||||||
// we don't allow any non-ascii to be super-safe
|
|
||||||
filename[index..].replace(|c: char| !c.is_ascii() || c == ':', "-")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dc_derive_safe_stem_ext(filename: &str) -> (String, String) {
|
|
||||||
let basename = get_safe_basename(&filename);
|
|
||||||
let (mut stem, mut ext) = if let Some(index) = basename.rfind('.') {
|
|
||||||
(
|
|
||||||
basename[0..index].to_string(),
|
|
||||||
basename[index..].to_string(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(basename, "".to_string())
|
|
||||||
};
|
|
||||||
// limit length of stem and ext
|
|
||||||
stem.truncate(32);
|
|
||||||
ext.truncate(32);
|
|
||||||
(stem, ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// the returned suffix is lower-case
|
// the returned suffix is lower-case
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
||||||
Path::new(path_filename.as_ref())
|
Path::new(path_filename.as_ref())
|
||||||
.extension()
|
.extension()
|
||||||
@@ -278,11 +249,8 @@ pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
|
|||||||
///
|
///
|
||||||
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
|
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
|
||||||
/// Otherwise, returns path as is.
|
/// Otherwise, returns path as is.
|
||||||
pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>(
|
pub(crate) fn dc_get_abs_path<P: AsRef<Path>>(context: &Context, path: P) -> PathBuf {
|
||||||
context: &Context,
|
let p: &Path = path.as_ref();
|
||||||
path: P,
|
|
||||||
) -> std::path::PathBuf {
|
|
||||||
let p: &std::path::Path = path.as_ref();
|
|
||||||
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
|
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
|
||||||
context.get_blobdir().join(p)
|
context.get_blobdir().join(p)
|
||||||
} else {
|
} else {
|
||||||
@@ -290,20 +258,20 @@ pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_get_filebytes(context: &Context, path: impl AsRef<std::path::Path>) -> u64 {
|
pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
|
||||||
let path_abs = dc_get_abs_path(context, &path);
|
let path_abs = dc_get_abs_path(context, &path);
|
||||||
match fs::metadata(&path_abs) {
|
match fs::metadata(&path_abs).await {
|
||||||
Ok(meta) => meta.len() as u64,
|
Ok(meta) => meta.len() as u64,
|
||||||
Err(_err) => 0,
|
Err(_err) => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
|
pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
|
||||||
let path_abs = dc_get_abs_path(context, &path);
|
let path_abs = dc_get_abs_path(context, &path);
|
||||||
if !path_abs.exists() {
|
if !path_abs.exists().await {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if !path_abs.is_file() {
|
if !path_abs.is_file().await {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"refusing to delete non-file \"{}\".",
|
"refusing to delete non-file \"{}\".",
|
||||||
@@ -313,9 +281,9 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dpath = format!("{}", path.as_ref().to_string_lossy());
|
let dpath = format!("{}", path.as_ref().to_string_lossy());
|
||||||
match fs::remove_file(path_abs) {
|
match fs::remove_file(path_abs).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
context.call_cb(Event::DeletedBlobFile(dpath));
|
context.emit_event(Event::DeletedBlobFile(dpath));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -325,13 +293,13 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_copy_file(
|
pub(crate) async fn dc_copy_file(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
src_path: impl AsRef<std::path::Path>,
|
src_path: impl AsRef<Path>,
|
||||||
dest_path: impl AsRef<std::path::Path>,
|
dest_path: impl AsRef<Path>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let src_abs = dc_get_abs_path(context, &src_path);
|
let src_abs = dc_get_abs_path(context, &src_path);
|
||||||
let mut src_file = match fs::File::open(&src_abs) {
|
let mut src_file = match fs::File::open(&src_abs).await {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -349,6 +317,7 @@ pub(crate) fn dc_copy_file(
|
|||||||
.create_new(true)
|
.create_new(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(&dest_abs)
|
.open(&dest_abs)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -362,7 +331,7 @@ pub(crate) fn dc_copy_file(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match std::io::copy(&mut src_file, &mut dest_file) {
|
match io::copy(&mut src_file, &mut dest_file).await {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -374,18 +343,21 @@ pub(crate) fn dc_copy_file(
|
|||||||
);
|
);
|
||||||
{
|
{
|
||||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||||
fs::remove_file(dest_abs).ok();
|
fs::remove_file(dest_abs).await.ok();
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
|
pub(crate) async fn dc_create_folder(
|
||||||
|
context: &Context,
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
let path_abs = dc_get_abs_path(context, &path);
|
let path_abs = dc_get_abs_path(context, &path);
|
||||||
if !path_abs.exists() {
|
if !path_abs.exists().await {
|
||||||
match fs::create_dir_all(path_abs) {
|
match fs::create_dir_all(path_abs).await {
|
||||||
Ok(_) => true,
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
@@ -393,22 +365,22 @@ pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Pa
|
|||||||
path.as_ref().display(),
|
path.as_ref().display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
false
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
true
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a the given content to provied file path.
|
/// Write a the given content to provied file path.
|
||||||
pub(crate) fn dc_write_file(
|
pub(crate) async fn dc_write_file(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
buf: &[u8],
|
buf: &[u8],
|
||||||
) -> Result<(), std::io::Error> {
|
) -> Result<(), io::Error> {
|
||||||
let path_abs = dc_get_abs_path(context, &path);
|
let path_abs = dc_get_abs_path(context, &path);
|
||||||
fs::write(&path_abs, buf).map_err(|err| {
|
fs::write(&path_abs, buf).await.map_err(|err| {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"Cannot write {} bytes to \"{}\": {}",
|
"Cannot write {} bytes to \"{}\": {}",
|
||||||
@@ -420,13 +392,10 @@ pub(crate) fn dc_write_file(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dc_read_file<P: AsRef<std::path::Path>>(
|
pub async fn dc_read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec<u8>, Error> {
|
||||||
context: &Context,
|
|
||||||
path: P,
|
|
||||||
) -> Result<Vec<u8>, Error> {
|
|
||||||
let path_abs = dc_get_abs_path(context, &path);
|
let path_abs = dc_get_abs_path(context, &path);
|
||||||
|
|
||||||
match fs::read(&path_abs) {
|
match fs::read(&path_abs).await {
|
||||||
Ok(bytes) => Ok(bytes),
|
Ok(bytes) => Ok(bytes),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -440,13 +409,31 @@ pub fn dc_read_file<P: AsRef<std::path::Path>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dc_open_file<P: AsRef<std::path::Path>>(
|
pub async fn dc_open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs::File, Error> {
|
||||||
|
let path_abs = dc_get_abs_path(context, &path);
|
||||||
|
|
||||||
|
match fs::File::open(&path_abs).await {
|
||||||
|
Ok(bytes) => Ok(bytes),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
context,
|
||||||
|
"Cannot read \"{}\" or file is empty: {}",
|
||||||
|
path.as_ref().display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
Err(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
path: P,
|
path: P,
|
||||||
) -> Result<std::fs::File, Error> {
|
) -> Result<std::fs::File, Error> {
|
||||||
let path_abs = dc_get_abs_path(context, &path);
|
let p: PathBuf = path.as_ref().into();
|
||||||
|
let path_abs = dc_get_abs_path(context, p);
|
||||||
|
|
||||||
match fs::File::open(&path_abs) {
|
match std::fs::File::open(&path_abs) {
|
||||||
Ok(bytes) => Ok(bytes),
|
Ok(bytes) => Ok(bytes),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -460,7 +447,7 @@ pub fn dc_open_file<P: AsRef<std::path::Path>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_get_next_backup_path(
|
pub(crate) async fn dc_get_next_backup_path(
|
||||||
folder: impl AsRef<Path>,
|
folder: impl AsRef<Path>,
|
||||||
backup_time: i64,
|
backup_time: i64,
|
||||||
) -> Result<PathBuf, Error> {
|
) -> Result<PathBuf, Error> {
|
||||||
@@ -473,7 +460,7 @@ pub(crate) fn dc_get_next_backup_path(
|
|||||||
for i in 0..64 {
|
for i in 0..64 {
|
||||||
let mut path = folder.clone();
|
let mut path = folder.clone();
|
||||||
path.push(format!("{}-{}.bak", stem, i));
|
path.push(format!("{}-{}.bak", stem, i));
|
||||||
if !path.exists() {
|
if !path.exists().await {
|
||||||
return Ok(path);
|
return Ok(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,10 +470,18 @@ pub(crate) fn dc_get_next_backup_path(
|
|||||||
pub(crate) fn time() -> i64 {
|
pub(crate) fn time() -> i64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap_or_default()
|
||||||
.as_secs() as i64
|
.as_secs() as i64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An invalid email address was encountered
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("Invalid email address: {message} ({addr})")]
|
||||||
|
pub struct InvalidEmailError {
|
||||||
|
message: String,
|
||||||
|
addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Very simple email address wrapper.
|
/// Very simple email address wrapper.
|
||||||
///
|
///
|
||||||
/// Represents an email address, right now just the `name@domain` portion.
|
/// Represents an email address, right now just the `name@domain` portion.
|
||||||
@@ -503,14 +498,14 @@ pub(crate) fn time() -> i64 {
|
|||||||
/// assert_eq!(&email.domain, "example.com");
|
/// assert_eq!(&email.domain, "example.com");
|
||||||
/// assert_eq!(email.to_string(), "someone@example.com");
|
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct EmailAddress {
|
pub struct EmailAddress {
|
||||||
pub local: String,
|
pub local: String,
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmailAddress {
|
impl EmailAddress {
|
||||||
pub fn new(input: &str) -> Result<Self, Error> {
|
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
|
||||||
input.parse::<EmailAddress>()
|
input.parse::<EmailAddress>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,31 +517,54 @@ impl fmt::Display for EmailAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for EmailAddress {
|
impl FromStr for EmailAddress {
|
||||||
type Err = Error;
|
type Err = InvalidEmailError;
|
||||||
|
|
||||||
/// Performs a dead-simple parse of an email address.
|
/// Performs a dead-simple parse of an email address.
|
||||||
fn from_str(input: &str) -> Result<EmailAddress, Error> {
|
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
|
||||||
ensure!(!input.is_empty(), "empty string is not valid");
|
let err = |msg: &str| {
|
||||||
|
Err(InvalidEmailError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
addr: input.to_string(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if input.is_empty() {
|
||||||
|
return err("empty string is not valid");
|
||||||
|
}
|
||||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||||
|
|
||||||
ensure!(parts.len() > 1, "missing '@' character");
|
if input
|
||||||
let local = parts[1];
|
.chars()
|
||||||
let domain = parts[0];
|
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||||
|
{
|
||||||
|
return err("Email must not contain whitespaces, '>' or '<'");
|
||||||
|
}
|
||||||
|
|
||||||
ensure!(
|
match &parts[..] {
|
||||||
!local.is_empty(),
|
[domain, local] => {
|
||||||
"empty string is not valid for local part"
|
if local.is_empty() {
|
||||||
);
|
return err("empty string is not valid for local part");
|
||||||
ensure!(domain.len() > 3, "domain is too short");
|
}
|
||||||
|
if domain.len() <= 3 {
|
||||||
let dot = domain.find('.');
|
return err("domain is too short");
|
||||||
ensure!(dot.is_some(), "invalid domain");
|
}
|
||||||
ensure!(dot.unwrap() < domain.len() - 2, "invalid domain");
|
let dot = domain.find('.');
|
||||||
|
match dot {
|
||||||
Ok(EmailAddress {
|
None => {
|
||||||
local: local.to_string(),
|
return err("invalid domain");
|
||||||
domain: domain.to_string(),
|
}
|
||||||
})
|
Some(dot_idx) => {
|
||||||
|
if dot_idx >= domain.len() - 2 {
|
||||||
|
return err("invalid domain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(EmailAddress {
|
||||||
|
local: (*local).to_string(),
|
||||||
|
domain: (*domain).to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => err("missing '@' character"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,54 +591,42 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_dc_truncate_1() {
|
fn test_dc_truncate_1() {
|
||||||
let s = "this is a little test string";
|
let s = "this is a little test string";
|
||||||
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
|
assert_eq!(dc_truncate(s, 16), "this is a [...]");
|
||||||
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_truncate_2() {
|
fn test_dc_truncate_2() {
|
||||||
assert_eq!(dc_truncate("1234", 2, false), "1234");
|
assert_eq!(dc_truncate("1234", 2), "1234");
|
||||||
assert_eq!(dc_truncate("1234", 2, true), "1234");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_truncate_3() {
|
fn test_dc_truncate_3() {
|
||||||
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
|
assert_eq!(dc_truncate("1234567", 1), "1[...]");
|
||||||
assert_eq!(dc_truncate("1234567", 1, true), "1...");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_truncate_4() {
|
fn test_dc_truncate_4() {
|
||||||
assert_eq!(dc_truncate("123456", 4, false), "123456");
|
assert_eq!(dc_truncate("123456", 4), "123456");
|
||||||
assert_eq!(dc_truncate("123456", 4, true), "123456");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_truncate_edge() {
|
fn test_dc_truncate_edge() {
|
||||||
assert_eq!(dc_truncate("", 4, false), "");
|
assert_eq!(dc_truncate("", 4), "");
|
||||||
assert_eq!(dc_truncate("", 4, true), "");
|
|
||||||
|
|
||||||
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
|
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
|
||||||
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
|
|
||||||
|
|
||||||
|
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
|
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
|
||||||
"𐠈[...]"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
|
|
||||||
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
|
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 9 characters, so no truncation
|
// 9 characters, so no truncation
|
||||||
assert_eq!(
|
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
|
||||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
|
|
||||||
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 12 characters, truncation
|
// 12 characters, truncation
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
|
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
|
||||||
"𑒀ὐ¢🜀\u{1e01b}A[...]",
|
"𑒀ὐ¢🜀\u{1e01b}A[...]",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -651,7 +657,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_extract_grpid_from_rfc724_mid() {
|
fn test_dc_extract_grpid_from_rfc724_mid() {
|
||||||
// Should return None if we pass invalid mid
|
// Should return None if we pass invalid mid
|
||||||
@@ -706,29 +711,29 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_emailaddress_parse() {
|
fn test_emailaddress_parse() {
|
||||||
assert_eq!(EmailAddress::new("").is_ok(), false);
|
assert_eq!("".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
EmailAddress::new("user@domain.tld").unwrap(),
|
"user@domain.tld".parse::<EmailAddress>().unwrap(),
|
||||||
EmailAddress {
|
EmailAddress {
|
||||||
local: "user".into(),
|
local: "user".into(),
|
||||||
domain: "domain.tld".into(),
|
domain: "domain.tld".into(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(EmailAddress::new("tt.dd@uu").is_ok(), false);
|
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(EmailAddress::new("u@d").is_ok(), false);
|
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(EmailAddress::new("u@d.").is_ok(), false);
|
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(EmailAddress::new("u@d.t").is_ok(), false);
|
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
EmailAddress::new("u@d.tt").unwrap(),
|
"u@d.tt".parse::<EmailAddress>().unwrap(),
|
||||||
EmailAddress {
|
EmailAddress {
|
||||||
local: "u".into(),
|
local: "u".into(),
|
||||||
domain: "d.tt".into(),
|
domain: "d.tt".into(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(EmailAddress::new("u@.tt").is_ok(), false);
|
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
|
||||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
@@ -737,11 +742,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_dc_truncate(
|
fn test_dc_truncate(
|
||||||
buf: String,
|
buf: String,
|
||||||
approx_chars in 0..10000usize,
|
approx_chars in 0..10000usize
|
||||||
do_unwrap: bool,
|
|
||||||
) {
|
) {
|
||||||
let res = dc_truncate(&buf, approx_chars, do_unwrap);
|
let res = dc_truncate(&buf, approx_chars);
|
||||||
let el_len = if do_unwrap { 3 } else { 5 };
|
let el_len = 5;
|
||||||
let l = res.chars().count();
|
let l = res.chars().count();
|
||||||
if approx_chars > 0 {
|
if approx_chars > 0 {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -755,53 +759,40 @@ mod tests {
|
|||||||
|
|
||||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||||
let l = res.len();
|
let l = res.len();
|
||||||
if do_unwrap {
|
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||||
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
|
|
||||||
} else {
|
|
||||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_file_get_safe_basename() {
|
async fn test_file_handling() {
|
||||||
assert_eq!(get_safe_basename("12312/hello"), "hello");
|
let t = TestContext::new().await;
|
||||||
assert_eq!(get_safe_basename("12312\\hello"), "hello");
|
|
||||||
assert_eq!(get_safe_basename("//12312\\hello"), "hello");
|
|
||||||
assert_eq!(get_safe_basename("//123:12\\hello"), "hello");
|
|
||||||
assert_eq!(get_safe_basename("//123:12/\\\\hello"), "hello");
|
|
||||||
assert_eq!(get_safe_basename("//123:12//hello"), "hello");
|
|
||||||
assert_eq!(get_safe_basename("//123:12//"), "nobasename");
|
|
||||||
assert_eq!(get_safe_basename("//123:12/"), "nobasename");
|
|
||||||
assert!(get_safe_basename("123\x012.hello").ends_with(".hello"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_handling() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let context = &t.ctx;
|
let context = &t.ctx;
|
||||||
let dc_file_exist = |ctx: &Context, fname: &str| {
|
macro_rules! dc_file_exist {
|
||||||
ctx.get_blobdir()
|
($ctx:expr, $fname:expr) => {
|
||||||
.join(Path::new(fname).file_name().unwrap())
|
$ctx.get_blobdir()
|
||||||
.exists()
|
.join(Path::new($fname).file_name().unwrap())
|
||||||
};
|
.exists()
|
||||||
|
};
|
||||||
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje"));
|
|
||||||
if dc_file_exist(context, "$BLOBDIR/foobar")
|
|
||||||
|| dc_file_exist(context, "$BLOBDIR/dada")
|
|
||||||
|| dc_file_exist(context, "$BLOBDIR/foobar.dadada")
|
|
||||||
|| dc_file_exist(context, "$BLOBDIR/foobar-folder")
|
|
||||||
{
|
|
||||||
dc_delete_file(context, "$BLOBDIR/foobar");
|
|
||||||
dc_delete_file(context, "$BLOBDIR/dada");
|
|
||||||
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
|
|
||||||
dc_delete_file(context, "$BLOBDIR/foobar-folder");
|
|
||||||
}
|
}
|
||||||
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content").is_ok());
|
|
||||||
assert!(dc_file_exist(context, "$BLOBDIR/foobar",));
|
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
|
||||||
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx"));
|
if dc_file_exist!(context, "$BLOBDIR/foobar").await
|
||||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7);
|
|| 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());
|
||||||
|
assert!(dc_file_exist!(context, "$BLOBDIR/foobar").await);
|
||||||
|
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx").await);
|
||||||
|
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7);
|
||||||
|
|
||||||
let abs_path = context
|
let abs_path = context
|
||||||
.get_blobdir()
|
.get_blobdir()
|
||||||
@@ -809,31 +800,33 @@ mod tests {
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
assert!(dc_file_exist(context, &abs_path));
|
assert!(dc_file_exist!(context, &abs_path).await);
|
||||||
|
|
||||||
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
|
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
|
||||||
|
|
||||||
// attempting to copy a second time should fail
|
// attempting to copy a second time should fail
|
||||||
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
|
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
|
||||||
|
|
||||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7);
|
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7);
|
||||||
|
|
||||||
let buf = dc_read_file(context, "$BLOBDIR/dada").unwrap();
|
let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap();
|
||||||
|
|
||||||
assert_eq!(buf.len(), 7);
|
assert_eq!(buf.len(), 7);
|
||||||
assert_eq!(&buf, b"content");
|
assert_eq!(&buf, b"content");
|
||||||
|
|
||||||
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
|
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
|
||||||
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
|
assert!(dc_delete_file(context, "$BLOBDIR/dada").await);
|
||||||
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder"));
|
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
|
||||||
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
|
.await
|
||||||
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));
|
.is_ok());
|
||||||
|
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder").await);
|
||||||
|
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await);
|
||||||
|
|
||||||
let fn0 = "$BLOBDIR/data.data";
|
let fn0 = "$BLOBDIR/data.data";
|
||||||
assert!(dc_write_file(context, &fn0, b"content").is_ok());
|
assert!(dc_write_file(context, &fn0, b"content").await.is_ok());
|
||||||
|
|
||||||
assert!(dc_delete_file(context, &fn0));
|
assert!(dc_delete_file(context, &fn0).await);
|
||||||
assert!(!dc_file_exist(context, &fn0));
|
assert!(!dc_file_exist!(context, &fn0).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -850,15 +843,15 @@ mod tests {
|
|||||||
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
|
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_smeared_timestamp() {
|
async fn test_create_smeared_timestamp() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
dc_create_smeared_timestamp(&t.ctx),
|
dc_create_smeared_timestamp(&t.ctx).await,
|
||||||
dc_create_smeared_timestamp(&t.ctx)
|
dc_create_smeared_timestamp(&t.ctx).await
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
dc_create_smeared_timestamp(&t.ctx)
|
dc_create_smeared_timestamp(&t.ctx).await
|
||||||
>= SystemTime::now()
|
>= SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -866,17 +859,50 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_create_smeared_timestamps() {
|
async fn test_create_smeared_timestamps() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
|
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
|
||||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize);
|
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||||
let next = dc_smeared_time(&t.ctx);
|
let next = dc_smeared_time(&t.ctx).await;
|
||||||
assert!((start + count - 1) < next);
|
assert!((start + count - 1) < next);
|
||||||
|
|
||||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
|
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
|
||||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize);
|
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||||
let next = dc_smeared_time(&t.ctx);
|
let next = dc_smeared_time(&t.ctx).await;
|
||||||
assert!((start + count - 1) < next);
|
assert!((start + count - 1) < next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duration_to_str() {
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s");
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(59 * 60 + 59)),
|
||||||
|
"0h 59m 59s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(59 * 60 + 60)),
|
||||||
|
"1h 0m 0s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)),
|
||||||
|
"2h 59m 59s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)),
|
||||||
|
"3h 0m 0s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)),
|
||||||
|
"3h 0m 59s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)),
|
||||||
|
"3h 1m 0s"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
//! A module to remove HTML tags from the email text
|
//! A module to remove HTML tags from the email text
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -35,6 +34,7 @@ pub fn dehtml(buf: &str) -> String {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut reader = quick_xml::Reader::from_str(buf);
|
let mut reader = quick_xml::Reader::from_str(buf);
|
||||||
|
reader.check_end_names(false);
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
@@ -139,13 +139,12 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
|||||||
dehtml.add_text = AddText::YesPreserveLineEnds;
|
dehtml.add_text = AddText::YesPreserveLineEnds;
|
||||||
}
|
}
|
||||||
"a" => {
|
"a" => {
|
||||||
if let Some(href) = event.html_attributes().find(|attr| {
|
if let Some(href) = event
|
||||||
attr.as_ref()
|
.html_attributes()
|
||||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "href")
|
.filter_map(|attr| attr.ok())
|
||||||
.unwrap_or_default()
|
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href")
|
||||||
}) {
|
{
|
||||||
let href = href
|
let href = href
|
||||||
.unwrap()
|
|
||||||
.unescape_and_decode_value(reader)
|
.unescape_and_decode_value(reader)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
@@ -189,4 +188,60 @@ mod tests {
|
|||||||
assert_eq!(dehtml(input), output);
|
assert_eq!(dehtml(input), output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dehtml_parse_br() {
|
||||||
|
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
|
||||||
|
let plain = dehtml(html);
|
||||||
|
|
||||||
|
assert_eq!(plain, "line1\n\r\r\rline2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dehtml_parse_href() {
|
||||||
|
let html = "<a href=url>text</a";
|
||||||
|
let plain = dehtml(html);
|
||||||
|
|
||||||
|
assert_eq!(plain, "[text](url)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dehtml_bold_text() {
|
||||||
|
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
|
||||||
|
let plain = dehtml(html);
|
||||||
|
|
||||||
|
assert_eq!(plain, "text *bold*<>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dehtml_html_encoded() {
|
||||||
|
let html =
|
||||||
|
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
||||||
|
|
||||||
|
let plain = dehtml(html);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
plain,
|
||||||
|
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unclosed_tags() {
|
||||||
|
let input = r##"
|
||||||
|
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'
|
||||||
|
'http://www.w3.org/TR/html4/loose.dtd'>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hi</title>
|
||||||
|
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
lots of text
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"##;
|
||||||
|
let txt = dehtml(input);
|
||||||
|
assert_eq!(txt.trim(), "lots of text");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
296
src/e2ee.rs
296
src/e2ee.rs
@@ -2,40 +2,41 @@
|
|||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use mailparse::MailHeaderMap;
|
use mailparse::ParsedMail;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
|
||||||
use crate::aheader::*;
|
use crate::aheader::*;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::key::*;
|
use crate::headerdef::HeaderDef;
|
||||||
|
use crate::headerdef::HeaderDefMap;
|
||||||
|
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||||
use crate::keyring::*;
|
use crate::keyring::*;
|
||||||
use crate::peerstate::*;
|
use crate::peerstate::*;
|
||||||
use crate::pgp;
|
use crate::pgp;
|
||||||
use crate::securejoin::handle_degrade_event;
|
use crate::securejoin::handle_degrade_event;
|
||||||
use crate::wrapmime;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EncryptHelper {
|
pub struct EncryptHelper {
|
||||||
pub prefer_encrypt: EncryptPreference,
|
pub prefer_encrypt: EncryptPreference,
|
||||||
pub addr: String,
|
pub addr: String,
|
||||||
pub public_key: Key,
|
pub public_key: SignedPublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncryptHelper {
|
impl EncryptHelper {
|
||||||
pub fn new(context: &Context) -> Result<EncryptHelper> {
|
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||||
let prefer_encrypt =
|
let prefer_encrypt =
|
||||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled))
|
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let addr = match context.get_config(Config::ConfiguredAddr) {
|
let addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||||
None => {
|
None => {
|
||||||
bail!("addr not configured!");
|
bail!("addr not configured!");
|
||||||
}
|
}
|
||||||
Some(addr) => addr,
|
Some(addr) => addr,
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = load_or_generate_self_public_key(context, &addr)?;
|
let public_key = SignedPublicKey::load_self(context).await?;
|
||||||
|
|
||||||
Ok(EncryptHelper {
|
Ok(EncryptHelper {
|
||||||
prefer_encrypt,
|
prefer_encrypt,
|
||||||
@@ -85,46 +86,44 @@ impl EncryptHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to encrypt the passed in `mail`.
|
/// Tries to encrypt the passed in `mail`.
|
||||||
pub fn encrypt(
|
pub async fn encrypt(
|
||||||
&mut self,
|
self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
min_verified: PeerstateVerifiedStatus,
|
min_verified: PeerstateVerifiedStatus,
|
||||||
mail_to_encrypt: lettre_email::PartBuilder,
|
mail_to_encrypt: lettre_email::PartBuilder,
|
||||||
peerstates: &[(Option<Peerstate>, &str)],
|
peerstates: Vec<(Option<Peerstate>, &str)>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut keyring = Keyring::default();
|
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||||
|
|
||||||
for (peerstate, addr) in peerstates
|
for (peerstate, addr) in peerstates
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter_map(|(state, addr)| state.as_ref().map(|s| (s, addr)))
|
.filter_map(|(state, addr)| state.map(|s| (s, addr)))
|
||||||
{
|
{
|
||||||
let key = peerstate.peek_key(min_verified).ok_or_else(|| {
|
let key = peerstate.take_key(min_verified).ok_or_else(|| {
|
||||||
format_err!("proper enc-key for {} missing, cannot encrypt", addr)
|
format_err!("proper enc-key for {} missing, cannot encrypt", addr)
|
||||||
})?;
|
})?;
|
||||||
keyring.add_ref(key);
|
keyring.add(key);
|
||||||
}
|
}
|
||||||
|
keyring.add(self.public_key.clone());
|
||||||
keyring.add_ref(&self.public_key);
|
let sign_key = SignedSecretKey::load_self(context).await?;
|
||||||
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
|
|
||||||
.ok_or_else(|| format_err!("missing own private key"))?;
|
|
||||||
|
|
||||||
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
||||||
|
|
||||||
let ctext = pgp::pk_encrypt(&raw_message, &keyring, Some(&sign_key))?;
|
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key)).await?;
|
||||||
|
|
||||||
Ok(ctext)
|
Ok(ctext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_decrypt(
|
pub async fn try_decrypt(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mail: &mailparse::ParsedMail<'_>,
|
mail: &ParsedMail<'_>,
|
||||||
message_time: i64,
|
message_time: i64,
|
||||||
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
) -> Result<(Option<Vec<u8>>, HashSet<Fingerprint>)> {
|
||||||
let from = mail
|
let from = mail
|
||||||
.headers
|
.headers
|
||||||
.get_first_value("From")?
|
.get_header(HeaderDef::From_)
|
||||||
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
|
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
|
||||||
.and_then(|from| from.extract_single_info())
|
.and_then(|from| from.extract_single_info())
|
||||||
.map(|from| from.addr)
|
.map(|from| from.addr)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@@ -133,111 +132,87 @@ pub fn try_decrypt(
|
|||||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||||
|
|
||||||
if message_time > 0 {
|
if message_time > 0 {
|
||||||
peerstate = Peerstate::from_addr(context, &context.sql, &from);
|
peerstate = Peerstate::from_addr(context, &from).await.ok();
|
||||||
|
|
||||||
if let Some(ref mut peerstate) = peerstate {
|
if let Some(ref mut peerstate) = peerstate {
|
||||||
if let Some(ref header) = autocryptheader {
|
if let Some(ref header) = autocryptheader {
|
||||||
peerstate.apply_header(&header, message_time);
|
peerstate.apply_header(&header, message_time);
|
||||||
peerstate.save_to_db(&context.sql, false)?;
|
peerstate.save_to_db(&context.sql, false).await?;
|
||||||
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
|
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
|
||||||
peerstate.degrade_encryption(message_time);
|
peerstate.degrade_encryption(message_time);
|
||||||
peerstate.save_to_db(&context.sql, false)?;
|
peerstate.save_to_db(&context.sql, false).await?;
|
||||||
}
|
}
|
||||||
} else if let Some(ref header) = autocryptheader {
|
} else if let Some(ref header) = autocryptheader {
|
||||||
let p = Peerstate::from_header(context, header, message_time);
|
let p = Peerstate::from_header(header, message_time);
|
||||||
p.save_to_db(&context.sql, true)?;
|
p.save_to_db(&context.sql, true).await?;
|
||||||
peerstate = Some(p);
|
peerstate = Some(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* possibly perform decryption */
|
/* possibly perform decryption */
|
||||||
let mut private_keyring = Keyring::default();
|
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||||
let mut public_keyring_for_validate = Keyring::default();
|
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||||
let mut out_mail = None;
|
|
||||||
let mut signatures = HashSet::default();
|
let mut signatures = HashSet::default();
|
||||||
let self_addr = context.get_config(Config::ConfiguredAddr);
|
|
||||||
|
|
||||||
if let Some(self_addr) = self_addr {
|
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||||
if private_keyring.load_self_private_for_decrypting(context, self_addr, &context.sql) {
|
peerstate = Peerstate::from_addr(&context, &from).await.ok();
|
||||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
}
|
||||||
peerstate = Peerstate::from_addr(&context, &context.sql, &from);
|
if let Some(peerstate) = peerstate {
|
||||||
}
|
if peerstate.degrade_event.is_some() {
|
||||||
if let Some(ref peerstate) = peerstate {
|
handle_degrade_event(context, &peerstate).await?;
|
||||||
if peerstate.degrade_event.is_some() {
|
}
|
||||||
handle_degrade_event(context, &peerstate)?;
|
if let Some(key) = peerstate.gossip_key {
|
||||||
}
|
public_keyring_for_validate.add(key);
|
||||||
if let Some(ref key) = peerstate.gossip_key {
|
}
|
||||||
public_keyring_for_validate.add_ref(key);
|
if let Some(key) = peerstate.public_key {
|
||||||
}
|
public_keyring_for_validate.add(key);
|
||||||
if let Some(ref key) = peerstate.public_key {
|
|
||||||
public_keyring_for_validate.add_ref(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out_mail = decrypt_if_autocrypt_message(
|
|
||||||
context,
|
|
||||||
mail,
|
|
||||||
&private_keyring,
|
|
||||||
&public_keyring_for_validate,
|
|
||||||
&mut signatures,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let out_mail = decrypt_if_autocrypt_message(
|
||||||
|
context,
|
||||||
|
mail,
|
||||||
|
private_keyring,
|
||||||
|
public_keyring_for_validate,
|
||||||
|
&mut signatures,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok((out_mail, signatures))
|
Ok((out_mail, signatures))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load public key from database or generate a new one.
|
/// 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>> {
|
||||||
/// This will load a public key from the database, generating and
|
ensure!(
|
||||||
/// storing a new one when one doesn't exist yet. Care is taken to
|
mail.ctype.mimetype == "multipart/encrypted",
|
||||||
/// only generate one key per context even when multiple threads call
|
"Not a multipart/encrypted message: {}",
|
||||||
/// this function concurrently.
|
mail.ctype.mimetype
|
||||||
fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str>) -> Result<Key> {
|
|
||||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
|
||||||
return Ok(key);
|
|
||||||
}
|
|
||||||
let _guard = context.generating_key_mutex.lock().unwrap();
|
|
||||||
|
|
||||||
// Check again in case the key was generated while we were waiting for the lock.
|
|
||||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
|
||||||
return Ok(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
info!(
|
|
||||||
context,
|
|
||||||
"Generating keypair with {} bits, e={} ...", 2048, 65537,
|
|
||||||
);
|
);
|
||||||
match pgp::create_keypair(&self_addr) {
|
ensure!(
|
||||||
Some((public_key, private_key)) => {
|
mail.subparts.len() == 2,
|
||||||
if dc_key_save_self_keypair(
|
"Invalid Autocrypt Level 1 Mime Parts"
|
||||||
context,
|
);
|
||||||
&public_key,
|
|
||||||
&private_key,
|
ensure!(
|
||||||
&self_addr,
|
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
|
||||||
true,
|
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||||
&context.sql,
|
mail.subparts[0].ctype,
|
||||||
) {
|
);
|
||||||
info!(
|
|
||||||
context,
|
ensure!(
|
||||||
"Keypair generated in {:.3}s.",
|
mail.subparts[1].ctype.mimetype == "application/octet-stream",
|
||||||
start.elapsed().as_secs()
|
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||||
);
|
mail.subparts[1].ctype
|
||||||
Ok(public_key)
|
);
|
||||||
} else {
|
|
||||||
Err(format_err!("Failed to save keypair"))
|
Ok(&mail.subparts[1])
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Err(format_err!("Failed to generate keypair")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt_if_autocrypt_message<'a>(
|
async fn decrypt_if_autocrypt_message<'a>(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mail: &mailparse::ParsedMail<'a>,
|
mail: &ParsedMail<'a>,
|
||||||
private_keyring: &Keyring,
|
private_keyring: Keyring<SignedSecretKey>,
|
||||||
public_keyring_for_validate: &Keyring,
|
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||||
ret_valid_signatures: &mut HashSet<String>,
|
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||||
) -> Result<Option<Vec<u8>>> {
|
) -> Result<Option<Vec<u8>>> {
|
||||||
// The returned bool is true if we detected an Autocrypt-encrypted
|
// The returned bool is true if we detected an Autocrypt-encrypted
|
||||||
// message and successfully decrypted it. Decryption then modifies the
|
// message and successfully decrypted it. Decryption then modifies the
|
||||||
@@ -246,7 +221,7 @@ fn decrypt_if_autocrypt_message<'a>(
|
|||||||
//
|
//
|
||||||
// Errors are returned for failures related to decryption of AC-messages.
|
// Errors are returned for failures related to decryption of AC-messages.
|
||||||
|
|
||||||
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
|
let encrypted_data_part = match get_autocrypt_mime(mail) {
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// not an autocrypt mime message, abort and ignore
|
// not an autocrypt mime message, abort and ignore
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -256,21 +231,20 @@ fn decrypt_if_autocrypt_message<'a>(
|
|||||||
info!(context, "Detected Autocrypt-mime message");
|
info!(context, "Detected Autocrypt-mime message");
|
||||||
|
|
||||||
decrypt_part(
|
decrypt_part(
|
||||||
context,
|
|
||||||
encrypted_data_part,
|
encrypted_data_part,
|
||||||
private_keyring,
|
private_keyring,
|
||||||
public_keyring_for_validate,
|
public_keyring_for_validate,
|
||||||
ret_valid_signatures,
|
ret_valid_signatures,
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns Ok(None) if nothing encrypted was found.
|
/// Returns Ok(None) if nothing encrypted was found.
|
||||||
fn decrypt_part(
|
async fn decrypt_part(
|
||||||
_context: &Context,
|
mail: &ParsedMail<'_>,
|
||||||
mail: &mailparse::ParsedMail<'_>,
|
private_keyring: Keyring<SignedSecretKey>,
|
||||||
private_keyring: &Keyring,
|
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||||
public_keyring_for_validate: &Keyring,
|
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||||
ret_valid_signatures: &mut HashSet<String>,
|
|
||||||
) -> Result<Option<Vec<u8>>> {
|
) -> Result<Option<Vec<u8>>> {
|
||||||
let data = mail.get_body_raw()?;
|
let data = mail.get_body_raw()?;
|
||||||
|
|
||||||
@@ -279,11 +253,12 @@ fn decrypt_part(
|
|||||||
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
|
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
|
||||||
|
|
||||||
let plain = pgp::pk_decrypt(
|
let plain = pgp::pk_decrypt(
|
||||||
&data,
|
data,
|
||||||
&private_keyring,
|
private_keyring,
|
||||||
&public_keyring_for_validate,
|
public_keyring_for_validate,
|
||||||
Some(ret_valid_signatures),
|
Some(ret_valid_signatures),
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
|
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
|
||||||
return Ok(Some(plain));
|
return Ok(Some(plain));
|
||||||
@@ -313,7 +288,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
|
|||||||
/// However, Delta Chat itself has no problem with encrypted multipart/report
|
/// However, Delta Chat itself has no problem with encrypted multipart/report
|
||||||
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
|
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
|
||||||
/// that we could use the normal Autocrypt processing.
|
/// that we could use the normal Autocrypt processing.
|
||||||
fn contains_report(mail: &mailparse::ParsedMail<'_>) -> bool {
|
fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
||||||
mail.ctype.mimetype == "multipart/report"
|
mail.ctype.mimetype == "multipart/report"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,14 +301,18 @@ fn contains_report(mail: &mailparse::ParsedMail<'_>) -> bool {
|
|||||||
///
|
///
|
||||||
/// If this succeeds you are also guaranteed that the
|
/// If this succeeds you are also guaranteed that the
|
||||||
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
||||||
pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
// TODO, remove this once deltachat::key::Key no longer exists.
|
||||||
let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| {
|
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||||
format_err!(concat!(
|
let self_addr = context
|
||||||
"Failed to get self address, ",
|
.get_config(Config::ConfiguredAddr)
|
||||||
"cannot ensure secret key if not configured."
|
.await
|
||||||
))
|
.ok_or_else(|| {
|
||||||
})?;
|
format_err!(concat!(
|
||||||
load_or_generate_self_public_key(context, &self_addr)?;
|
"Failed to get self address, ",
|
||||||
|
"cannot ensure secret key if not configured."
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
SignedPublicKey::load_self(context).await?;
|
||||||
Ok(self_addr)
|
Ok(self_addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,17 +325,17 @@ mod tests {
|
|||||||
mod ensure_secret_key_exists {
|
mod ensure_secret_key_exists {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_prexisting() {
|
async fn test_prexisting() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
let test_addr = configure_alice_keypair(&t.ctx);
|
let test_addr = t.configure_alice().await;
|
||||||
assert_eq!(ensure_secret_key_exists(&t.ctx).unwrap(), test_addr);
|
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[async_std::test]
|
||||||
fn test_not_configured() {
|
async fn test_not_configured() {
|
||||||
let t = dummy_context();
|
let t = TestContext::new().await;
|
||||||
assert!(ensure_secret_key_exists(&t.ctx).is_err());
|
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,49 +363,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mod load_or_generate_self_public_key {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_existing() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let addr = configure_alice_keypair(&t.ctx);
|
|
||||||
let key = load_or_generate_self_public_key(&t.ctx, addr);
|
|
||||||
assert!(key.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore] // generating keys is expensive
|
|
||||||
fn test_generate() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let addr = "alice@example.org";
|
|
||||||
let key0 = load_or_generate_self_public_key(&t.ctx, addr);
|
|
||||||
assert!(key0.is_ok());
|
|
||||||
let key1 = load_or_generate_self_public_key(&t.ctx, addr);
|
|
||||||
assert!(key1.is_ok());
|
|
||||||
assert_eq!(key0.unwrap(), key1.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn test_generate_concurrent() {
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
let t = dummy_context();
|
|
||||||
let ctx = Arc::new(t.ctx);
|
|
||||||
let ctx0 = Arc::clone(&ctx);
|
|
||||||
let thr0 =
|
|
||||||
thread::spawn(move || load_or_generate_self_public_key(&ctx0, "alice@example.org"));
|
|
||||||
let ctx1 = Arc::clone(&ctx);
|
|
||||||
let thr1 =
|
|
||||||
thread::spawn(move || load_or_generate_self_public_key(&ctx1, "alice@example.org"));
|
|
||||||
let res0 = thr0.join().unwrap();
|
|
||||||
let res1 = thr1.join().unwrap();
|
|
||||||
assert_eq!(res0.unwrap(), res1.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_has_decrypted_pgp_armor() {
|
fn test_has_decrypted_pgp_armor() {
|
||||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||||
|
|||||||
152
src/error.rs
152
src/error.rs
@@ -1,156 +1,6 @@
|
|||||||
//! # Error handling
|
//! # Error handling
|
||||||
|
|
||||||
use lettre_email::mime;
|
pub use anyhow::{bail, ensure, format_err, Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Failure(failure::Error),
|
|
||||||
#[fail(display = "SQL error: {:?}", _0)]
|
|
||||||
SqlError(#[cause] crate::sql::Error),
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Io(std::io::Error),
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Message(String),
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Image(image_meta::ImageError),
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Utf8(std::str::Utf8Error),
|
|
||||||
#[fail(display = "PGP: {:?}", _0)]
|
|
||||||
Pgp(pgp::errors::Error),
|
|
||||||
#[fail(display = "Base64Decode: {:?}", _0)]
|
|
||||||
Base64Decode(base64::DecodeError),
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
FromUtf8(std::string::FromUtf8Error),
|
|
||||||
#[fail(display = "{}", _0)]
|
|
||||||
BlobError(#[cause] crate::blob::BlobError),
|
|
||||||
#[fail(display = "Invalid Message ID.")]
|
|
||||||
InvalidMsgId,
|
|
||||||
#[fail(display = "Watch folder not found {:?}", _0)]
|
|
||||||
WatchFolderNotFound(String),
|
|
||||||
#[fail(display = "Invalid Email: {:?}", _0)]
|
|
||||||
MailParseError(#[cause] mailparse::MailParseError),
|
|
||||||
#[fail(display = "Building invalid Email: {:?}", _0)]
|
|
||||||
LettreError(#[cause] lettre_email::error::Error),
|
|
||||||
#[fail(display = "FromStr error: {:?}", _0)]
|
|
||||||
FromStr(#[cause] mime::FromStrError),
|
|
||||||
#[fail(display = "Not Configured")]
|
|
||||||
NotConfigured,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<crate::sql::Error> for Error {
|
|
||||||
fn from(err: crate::sql::Error) -> Error {
|
|
||||||
Error::SqlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<base64::DecodeError> for Error {
|
|
||||||
fn from(err: base64::DecodeError) -> Error {
|
|
||||||
Error::Base64Decode(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<failure::Error> for Error {
|
|
||||||
fn from(err: failure::Error) -> Error {
|
|
||||||
Error::Failure(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(err: std::io::Error) -> Error {
|
|
||||||
Error::Io(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::str::Utf8Error> for Error {
|
|
||||||
fn from(err: std::str::Utf8Error) -> Error {
|
|
||||||
Error::Utf8(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<image_meta::ImageError> for Error {
|
|
||||||
fn from(err: image_meta::ImageError) -> Error {
|
|
||||||
Error::Image(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pgp::errors::Error> for Error {
|
|
||||||
fn from(err: pgp::errors::Error) -> Error {
|
|
||||||
Error::Pgp(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::string::FromUtf8Error> for Error {
|
|
||||||
fn from(err: std::string::FromUtf8Error) -> Error {
|
|
||||||
Error::FromUtf8(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::blob::BlobError> for Error {
|
|
||||||
fn from(err: crate::blob::BlobError) -> Error {
|
|
||||||
Error::BlobError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::message::InvalidMsgId> for Error {
|
|
||||||
fn from(_err: crate::message::InvalidMsgId) -> Error {
|
|
||||||
Error::InvalidMsgId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<mailparse::MailParseError> for Error {
|
|
||||||
fn from(err: mailparse::MailParseError) -> Error {
|
|
||||||
Error::MailParseError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<lettre_email::error::Error> for Error {
|
|
||||||
fn from(err: lettre_email::error::Error) -> Error {
|
|
||||||
Error::LettreError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<mime::FromStrError> for Error {
|
|
||||||
fn from(err: mime::FromStrError) -> Error {
|
|
||||||
Error::FromStr(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! bail {
|
|
||||||
($e:expr) => {
|
|
||||||
return Err($crate::error::Error::Message($e.to_string()));
|
|
||||||
};
|
|
||||||
($fmt:expr, $($arg:tt)+) => {
|
|
||||||
return Err($crate::error::Error::Message(format!($fmt, $($arg)+)));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! format_err {
|
|
||||||
($e:expr) => {
|
|
||||||
$crate::error::Error::Message($e.to_string());
|
|
||||||
};
|
|
||||||
($fmt:expr, $($arg:tt)+) => {
|
|
||||||
$crate::error::Error::Message(format!($fmt, $($arg)+));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export(local_inner_macros)]
|
|
||||||
macro_rules! ensure {
|
|
||||||
($cond:expr, $e:expr) => {
|
|
||||||
if !($cond) {
|
|
||||||
bail!($e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($cond:expr, $fmt:expr, $($arg:tt)+) => {
|
|
||||||
if !($cond) {
|
|
||||||
bail!($fmt, $($arg)+);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ensure_eq {
|
macro_rules! ensure_eq {
|
||||||
|
|||||||
122
src/events.rs
122
src/events.rs
@@ -1,11 +1,65 @@
|
|||||||
//! # Events specification
|
//! # Events specification
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use async_std::path::PathBuf;
|
||||||
|
use async_std::sync::{channel, Receiver, Sender, TrySendError};
|
||||||
use strum::EnumProperty;
|
use strum::EnumProperty;
|
||||||
|
|
||||||
|
use crate::chat::ChatId;
|
||||||
use crate::message::MsgId;
|
use crate::message::MsgId;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Events {
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Events {
|
||||||
|
fn default() -> Self {
|
||||||
|
let (sender, receiver) = channel(1_000);
|
||||||
|
|
||||||
|
Self { receiver, sender }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Events {
|
||||||
|
pub fn emit(&self, event: Event) {
|
||||||
|
match self.sender.try_send(event) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(TrySendError::Full(event)) => {
|
||||||
|
// when we are full, we pop remove the oldest event and push on the new one
|
||||||
|
let _ = self.receiver.try_recv();
|
||||||
|
|
||||||
|
// try again
|
||||||
|
self.emit(event);
|
||||||
|
}
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
unreachable!("unable to emit event, channel disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the event emitter.
|
||||||
|
pub fn get_emitter(&self) -> EventEmitter {
|
||||||
|
EventEmitter(self.receiver.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EventEmitter(Receiver<Event>);
|
||||||
|
|
||||||
|
impl EventEmitter {
|
||||||
|
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
|
||||||
|
pub fn recv_sync(&self) -> Option<Event> {
|
||||||
|
async_std::task::block_on(self.recv())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blocking async recv of an event. Return `None` if the `Sender` has been droped.
|
||||||
|
pub async fn recv(&self) -> Option<Event> {
|
||||||
|
// TODO: change once we can use async channels internally.
|
||||||
|
self.0.recv().await.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
/// Returns the corresponding Event id.
|
/// Returns the corresponding Event id.
|
||||||
pub fn as_id(&self) -> i32 {
|
pub fn as_id(&self) -> i32 {
|
||||||
@@ -21,56 +75,38 @@ pub enum Event {
|
|||||||
/// The library-user may write an informational string to the log.
|
/// The library-user may write an informational string to the log.
|
||||||
/// Passed to the callback given to dc_context_new().
|
/// Passed to the callback given to dc_context_new().
|
||||||
/// This event should not be reported to the end-user using a popup or something like that.
|
/// This event should not be reported to the end-user using a popup or something like that.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "100"))]
|
#[strum(props(id = "100"))]
|
||||||
Info(String),
|
Info(String),
|
||||||
|
|
||||||
/// Emitted when SMTP connection is established and login was successful.
|
/// Emitted when SMTP connection is established and login was successful.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "101"))]
|
#[strum(props(id = "101"))]
|
||||||
SmtpConnected(String),
|
SmtpConnected(String),
|
||||||
|
|
||||||
/// Emitted when IMAP connection is established and login was successful.
|
/// Emitted when IMAP connection is established and login was successful.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "102"))]
|
#[strum(props(id = "102"))]
|
||||||
ImapConnected(String),
|
ImapConnected(String),
|
||||||
|
|
||||||
/// Emitted when a message was successfully sent to the SMTP server.
|
/// Emitted when a message was successfully sent to the SMTP server.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "103"))]
|
#[strum(props(id = "103"))]
|
||||||
SmtpMessageSent(String),
|
SmtpMessageSent(String),
|
||||||
|
|
||||||
/// Emitted when an IMAP message has been marked as deleted
|
/// Emitted when an IMAP message has been marked as deleted
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "104"))]
|
#[strum(props(id = "104"))]
|
||||||
ImapMessageDeleted(String),
|
ImapMessageDeleted(String),
|
||||||
|
|
||||||
/// Emitted when an IMAP message has been moved
|
/// Emitted when an IMAP message has been moved
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "105"))]
|
#[strum(props(id = "105"))]
|
||||||
ImapMessageMoved(String),
|
ImapMessageMoved(String),
|
||||||
|
|
||||||
/// Emitted when an IMAP folder was emptied
|
/// Emitted when an IMAP folder was emptied
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "106"))]
|
#[strum(props(id = "106"))]
|
||||||
ImapFolderEmptied(String),
|
ImapFolderEmptied(String),
|
||||||
|
|
||||||
/// Emitted when an new file in the $BLOBDIR was created
|
/// Emitted when an new file in the $BLOBDIR was created
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "150"))]
|
#[strum(props(id = "150"))]
|
||||||
NewBlobFile(String),
|
NewBlobFile(String),
|
||||||
|
|
||||||
/// Emitted when an new file in the $BLOBDIR was created
|
/// Emitted when an new file in the $BLOBDIR was created
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "151"))]
|
#[strum(props(id = "151"))]
|
||||||
DeletedBlobFile(String),
|
DeletedBlobFile(String),
|
||||||
|
|
||||||
@@ -78,8 +114,6 @@ pub enum Event {
|
|||||||
/// Passed to the callback given to dc_context_new().
|
/// Passed to the callback given to dc_context_new().
|
||||||
///
|
///
|
||||||
/// This event should not be reported to the end-user using a popup or something like that.
|
/// This event should not be reported to the end-user using a popup or something like that.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "300"))]
|
#[strum(props(id = "300"))]
|
||||||
Warning(String),
|
Warning(String),
|
||||||
|
|
||||||
@@ -94,8 +128,6 @@ pub enum Event {
|
|||||||
/// it might be better to delay showing these events until the function has really
|
/// it might be better to delay showing these events until the function has really
|
||||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||||
/// in a messasge box then.
|
/// in a messasge box then.
|
||||||
///
|
|
||||||
/// @return
|
|
||||||
#[strum(props(id = "400"))]
|
#[strum(props(id = "400"))]
|
||||||
Error(String),
|
Error(String),
|
||||||
|
|
||||||
@@ -112,8 +144,6 @@ pub enum Event {
|
|||||||
/// Moreover, if the UI detects that the device is offline,
|
/// Moreover, if the UI detects that the device is offline,
|
||||||
/// it is probably more useful to report this to the user
|
/// it is probably more useful to report this to the user
|
||||||
/// instead of the string from data2.
|
/// instead of the string from data2.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "401"))]
|
#[strum(props(id = "401"))]
|
||||||
ErrorNetwork(String),
|
ErrorNetwork(String),
|
||||||
|
|
||||||
@@ -122,8 +152,6 @@ pub enum Event {
|
|||||||
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||||
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
||||||
/// dc_send_text_msg() or another sending function.
|
/// dc_send_text_msg() or another sending function.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "410"))]
|
#[strum(props(id = "410"))]
|
||||||
ErrorSelfNotInGroup(String),
|
ErrorSelfNotInGroup(String),
|
||||||
|
|
||||||
@@ -132,54 +160,41 @@ pub enum Event {
|
|||||||
/// - Messages sent, received or removed
|
/// - Messages sent, received or removed
|
||||||
/// - Chats created, deleted or archived
|
/// - Chats created, deleted or archived
|
||||||
/// - A draft has been set
|
/// - A draft has been set
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2000"))]
|
#[strum(props(id = "2000"))]
|
||||||
MsgsChanged { chat_id: u32, msg_id: MsgId },
|
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
|
||||||
|
|
||||||
/// There is a fresh message. Typically, the user will show an notification
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2005"))]
|
#[strum(props(id = "2005"))]
|
||||||
IncomingMsg { chat_id: u32, msg_id: MsgId },
|
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
|
||||||
|
|
||||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||||
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2010"))]
|
#[strum(props(id = "2010"))]
|
||||||
MsgDelivered { chat_id: u32, msg_id: MsgId },
|
MsgDelivered { chat_id: ChatId, msg_id: MsgId },
|
||||||
|
|
||||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
|
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2012"))]
|
#[strum(props(id = "2012"))]
|
||||||
MsgFailed { chat_id: u32, msg_id: MsgId },
|
MsgFailed { chat_id: ChatId, msg_id: MsgId },
|
||||||
|
|
||||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2015"))]
|
#[strum(props(id = "2015"))]
|
||||||
MsgRead { chat_id: u32, msg_id: MsgId },
|
MsgRead { chat_id: ChatId, msg_id: MsgId },
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
/// Or the verify state of a chat has changed.
|
/// Or the verify state of a chat has changed.
|
||||||
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||||
/// and dc_remove_contact_from_chat().
|
/// and dc_remove_contact_from_chat().
|
||||||
///
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2020"))]
|
#[strum(props(id = "2020"))]
|
||||||
ChatModified(u32),
|
ChatModified(ChatId),
|
||||||
|
|
||||||
/// Contact(s) created, renamed, blocked or deleted.
|
/// Contact(s) created, renamed, blocked or deleted.
|
||||||
///
|
///
|
||||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2030"))]
|
#[strum(props(id = "2030"))]
|
||||||
ContactsChanged(Option<u32>),
|
ContactsChanged(Option<u32>),
|
||||||
|
|
||||||
@@ -188,14 +203,12 @@ pub enum Event {
|
|||||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||||
/// If the locations of several contacts have been changed,
|
/// If the locations of several contacts have been changed,
|
||||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2035"))]
|
#[strum(props(id = "2035"))]
|
||||||
LocationChanged(Option<u32>),
|
LocationChanged(Option<u32>),
|
||||||
|
|
||||||
/// Inform about the configuration progress started by configure().
|
/// Inform about the configuration progress started by configure().
|
||||||
///
|
///
|
||||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2041"))]
|
#[strum(props(id = "2041"))]
|
||||||
ConfigureProgress(usize),
|
ConfigureProgress(usize),
|
||||||
|
|
||||||
@@ -203,7 +216,6 @@ pub enum Event {
|
|||||||
///
|
///
|
||||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
/// @param data2 0
|
/// @param data2 0
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2051"))]
|
#[strum(props(id = "2051"))]
|
||||||
ImexProgress(usize),
|
ImexProgress(usize),
|
||||||
|
|
||||||
@@ -214,7 +226,6 @@ pub enum Event {
|
|||||||
/// services.
|
/// services.
|
||||||
///
|
///
|
||||||
/// @param data2 0
|
/// @param data2 0
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2052"))]
|
#[strum(props(id = "2052"))]
|
||||||
ImexFileWritten(PathBuf),
|
ImexFileWritten(PathBuf),
|
||||||
|
|
||||||
@@ -230,7 +241,6 @@ pub enum Event {
|
|||||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||||
/// 1000=Protocol finished for this contact.
|
/// 1000=Protocol finished for this contact.
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2060"))]
|
#[strum(props(id = "2060"))]
|
||||||
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||||
|
|
||||||
@@ -242,14 +252,6 @@ pub enum Event {
|
|||||||
/// @param data2 (int) Progress as:
|
/// @param data2 (int) Progress as:
|
||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2061"))]
|
#[strum(props(id = "2061"))]
|
||||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||||
|
|
||||||
/// This event is sent out to the inviter when a joiner successfully joined a group.
|
|
||||||
/// @param data1 (int) chat_id
|
|
||||||
/// @param data2 (int) contact_id
|
|
||||||
/// @return 0
|
|
||||||
#[strum(props(id = "2062"))]
|
|
||||||
SecurejoinMemberAdded { chat_id: u32, contact_id: u32 },
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
|
use crate::strum::AsStaticRef;
|
||||||
|
use mailparse::{MailHeader, MailHeaderMap};
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||||
#[strum(serialize_all = "kebab_case")]
|
#[strum(serialize_all = "kebab_case")]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum HeaderDef {
|
pub enum HeaderDef {
|
||||||
@@ -10,16 +13,20 @@ pub enum HeaderDef {
|
|||||||
Cc,
|
Cc,
|
||||||
Disposition,
|
Disposition,
|
||||||
OriginalMessageId,
|
OriginalMessageId,
|
||||||
|
|
||||||
|
/// Delta Chat extension for message IDs in combined MDNs
|
||||||
|
AdditionalMessageIds,
|
||||||
|
|
||||||
ListId,
|
ListId,
|
||||||
References,
|
References,
|
||||||
InReplyTo,
|
InReplyTo,
|
||||||
Precedence,
|
Precedence,
|
||||||
|
ContentType,
|
||||||
ChatVersion,
|
ChatVersion,
|
||||||
ChatGroupId,
|
ChatGroupId,
|
||||||
ChatGroupName,
|
ChatGroupName,
|
||||||
ChatGroupNameChanged,
|
ChatGroupNameChanged,
|
||||||
ChatVerified,
|
ChatVerified,
|
||||||
ChatGroupImage, // deprecated
|
|
||||||
ChatGroupAvatar,
|
ChatGroupAvatar,
|
||||||
ChatUserAvatar,
|
ChatUserAvatar,
|
||||||
ChatVoiceMessage,
|
ChatVoiceMessage,
|
||||||
@@ -28,6 +35,7 @@ pub enum HeaderDef {
|
|||||||
ChatContent,
|
ChatContent,
|
||||||
ChatDuration,
|
ChatDuration,
|
||||||
ChatDispositionNotificationTo,
|
ChatDispositionNotificationTo,
|
||||||
|
Autocrypt,
|
||||||
AutocryptSetupMessage,
|
AutocryptSetupMessage,
|
||||||
SecureJoin,
|
SecureJoin,
|
||||||
SecureJoinGroup,
|
SecureJoinGroup,
|
||||||
@@ -39,8 +47,22 @@ pub enum HeaderDef {
|
|||||||
|
|
||||||
impl HeaderDef {
|
impl HeaderDef {
|
||||||
/// Returns the corresponding Event id.
|
/// Returns the corresponding Event id.
|
||||||
pub fn get_headername(&self) -> String {
|
pub fn get_headername(&self) -> &'static str {
|
||||||
self.to_string()
|
self.as_static()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HeaderDefMap {
|
||||||
|
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||||
|
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderDefMap for [MailHeader<'_>] {
|
||||||
|
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||||
|
self.get_first_value(headerdef.get_headername())
|
||||||
|
}
|
||||||
|
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
|
||||||
|
self.get_first_header(headerdef.get_headername())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +73,24 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
/// Test that kebab_case serialization works as expected
|
/// Test that kebab_case serialization works as expected
|
||||||
fn kebab_test() {
|
fn kebab_test() {
|
||||||
assert_eq!(HeaderDef::From_.to_string(), "from");
|
assert_eq!(HeaderDef::From_.get_headername(), "from");
|
||||||
|
|
||||||
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
|
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test that headers are parsed case-insensitively
|
||||||
|
fn test_get_header_value_case() {
|
||||||
|
let (headers, _) =
|
||||||
|
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||||
|
Some("v99".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
headers.get_header_value(HeaderDef::From_),
|
||||||
|
Some("Bob".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
src/imap/client.rs
Normal file
139
src/imap/client.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use async_imap::{
|
||||||
|
error::{Error as ImapError, Result as ImapResult},
|
||||||
|
Client as ImapClient,
|
||||||
|
};
|
||||||
|
use async_std::net::{self, TcpStream};
|
||||||
|
|
||||||
|
use super::session::Session;
|
||||||
|
use crate::login_param::dc_build_tls;
|
||||||
|
|
||||||
|
use super::session::SessionStream;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Client {
|
||||||
|
is_secure: bool,
|
||||||
|
inner: ImapClient<Box<dyn SessionStream>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Client {
|
||||||
|
type Target = ImapClient<Box<dyn SessionStream>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Client {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
|
||||||
|
self,
|
||||||
|
username: U,
|
||||||
|
password: P,
|
||||||
|
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||||
|
let Client { inner, is_secure } = self;
|
||||||
|
let session = inner
|
||||||
|
.login(username, password)
|
||||||
|
.await
|
||||||
|
.map_err(|(err, client)| {
|
||||||
|
(
|
||||||
|
err,
|
||||||
|
Client {
|
||||||
|
is_secure,
|
||||||
|
inner: client,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(Session { inner: session })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
||||||
|
self,
|
||||||
|
auth_type: S,
|
||||||
|
authenticator: &A,
|
||||||
|
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||||
|
let Client { inner, is_secure } = self;
|
||||||
|
let session =
|
||||||
|
inner
|
||||||
|
.authenticate(auth_type, authenticator)
|
||||||
|
.await
|
||||||
|
.map_err(|(err, client)| {
|
||||||
|
(
|
||||||
|
err,
|
||||||
|
Client {
|
||||||
|
is_secure,
|
||||||
|
inner: client,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(Session { inner: session })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
|
||||||
|
addr: A,
|
||||||
|
domain: S,
|
||||||
|
strict_tls: bool,
|
||||||
|
) -> ImapResult<Self> {
|
||||||
|
let stream = TcpStream::connect(addr).await?;
|
||||||
|
let tls = dc_build_tls(strict_tls);
|
||||||
|
let tls_stream: Box<dyn SessionStream> =
|
||||||
|
Box::new(tls.connect(domain.as_ref(), stream).await?);
|
||||||
|
let mut client = ImapClient::new(tls_stream);
|
||||||
|
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||||
|
client.debug = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _greeting = client
|
||||||
|
.read_response()
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||||
|
|
||||||
|
Ok(Client {
|
||||||
|
is_secure: true,
|
||||||
|
inner: client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
|
||||||
|
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
||||||
|
|
||||||
|
let mut client = ImapClient::new(stream);
|
||||||
|
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||||
|
client.debug = true;
|
||||||
|
}
|
||||||
|
let _greeting = client
|
||||||
|
.read_response()
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||||
|
|
||||||
|
Ok(Client {
|
||||||
|
is_secure: false,
|
||||||
|
inner: client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn secure<S: AsRef<str>>(self, domain: S, strict_tls: bool) -> ImapResult<Client> {
|
||||||
|
if self.is_secure {
|
||||||
|
Ok(self)
|
||||||
|
} else {
|
||||||
|
let Client { mut inner, .. } = self;
|
||||||
|
let tls = dc_build_tls(strict_tls);
|
||||||
|
inner.run_command_and_check_ok("STARTTLS", None).await?;
|
||||||
|
|
||||||
|
let stream = inner.into_inner();
|
||||||
|
let ssl_stream = tls.connect(domain.as_ref(), stream).await?;
|
||||||
|
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
|
||||||
|
|
||||||
|
Ok(Client {
|
||||||
|
is_secure: true,
|
||||||
|
inner: ImapClient::new(boxed),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
433
src/imap/idle.rs
433
src/imap/idle.rs
@@ -2,283 +2,230 @@ use super::Imap;
|
|||||||
|
|
||||||
use async_imap::extensions::idle::IdleResponse;
|
use async_imap::extensions::idle::IdleResponse;
|
||||||
use async_std::prelude::*;
|
use async_std::prelude::*;
|
||||||
use async_std::task;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use crate::context::Context;
|
use crate::{context::Context, scheduler::InterruptInfo};
|
||||||
use crate::imap_client::*;
|
|
||||||
|
|
||||||
use super::select_folder;
|
use super::select_folder;
|
||||||
|
use super::session::Session;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP IDLE protocol timeout during Termination")]
|
#[error("IMAP IDLE protocol timed out")]
|
||||||
IdleTimeout(#[cause] async_std::future::TimeoutError),
|
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||||
|
|
||||||
#[fail(display = "IMAP Error during Idle Wait")]
|
#[error("IMAP server does not have IDLE capability")]
|
||||||
IdleError(#[cause] async_imap::error::Error),
|
|
||||||
|
|
||||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
|
||||||
IdleAbilityMissing,
|
IdleAbilityMissing,
|
||||||
|
|
||||||
#[fail(display = "IMAP select folder error")]
|
#[error("IMAP select folder error")]
|
||||||
SelectFolderError(#[cause] select_folder::Error),
|
SelectFolderError(#[from] select_folder::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP error")]
|
#[error("Setup handle error")]
|
||||||
ImapError(#[cause] async_imap::error::Error),
|
SetupHandleError(#[from] super::Error),
|
||||||
|
|
||||||
#[fail(display = "Setup handle error")]
|
|
||||||
SetupHandleError(#[cause] super::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<select_folder::Error> for Error {
|
|
||||||
fn from(err: select_folder::Error) -> Error {
|
|
||||||
Error::SelectFolderError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Imap {
|
impl Imap {
|
||||||
pub fn can_idle(&self) -> bool {
|
pub fn can_idle(&self) -> bool {
|
||||||
task::block_on(async move { self.config.read().await.can_idle })
|
self.config.can_idle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
|
pub async fn idle(
|
||||||
task::block_on(async move {
|
&mut self,
|
||||||
if !self.can_idle() {
|
context: &Context,
|
||||||
return Err(Error::IdleAbilityMissing);
|
watch_folder: Option<String>,
|
||||||
|
) -> Result<InterruptInfo> {
|
||||||
|
use futures::future::FutureExt;
|
||||||
|
|
||||||
|
if !self.can_idle() {
|
||||||
|
return Err(Error::IdleAbilityMissing);
|
||||||
|
}
|
||||||
|
self.setup_handle_if_needed(context).await?;
|
||||||
|
|
||||||
|
self.select_folder(context, watch_folder.clone()).await?;
|
||||||
|
|
||||||
|
let session = self.session.take();
|
||||||
|
let timeout = Duration::from_secs(23 * 60);
|
||||||
|
let mut info = Default::default();
|
||||||
|
|
||||||
|
if let Some(session) = session {
|
||||||
|
let mut handle = session.idle();
|
||||||
|
if let Err(err) = handle.init().await {
|
||||||
|
return Err(Error::IdleProtocolFailed(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setup_handle_if_needed(context)
|
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||||
.await
|
|
||||||
.map_err(Error::SetupHandleError)?;
|
|
||||||
|
|
||||||
self.select_folder(context, watch_folder.clone()).await?;
|
enum Event {
|
||||||
|
IdleResponse(IdleResponse),
|
||||||
let session = self.session.lock().await.take();
|
Interrupt(InterruptInfo),
|
||||||
let timeout = Duration::from_secs(23 * 60);
|
|
||||||
if let Some(session) = session {
|
|
||||||
match session.idle() {
|
|
||||||
// BEWARE: If you change the Secure branch you
|
|
||||||
// typically also need to change the Insecure branch.
|
|
||||||
IdleHandle::Secure(mut handle) => {
|
|
||||||
if let Err(err) = handle.init().await {
|
|
||||||
return Err(Error::IdleProtocolFailed(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
|
||||||
*self.interrupt.lock().await = Some(interrupt);
|
|
||||||
|
|
||||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
|
||||||
// interrupt_idle has happened before we
|
|
||||||
// provided self.interrupt
|
|
||||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
|
||||||
std::mem::drop(idle_wait);
|
|
||||||
info!(context, "Idle wait was skipped");
|
|
||||||
} else {
|
|
||||||
info!(context, "Idle entering wait-on-remote state");
|
|
||||||
match idle_wait.await {
|
|
||||||
Ok(IdleResponse::NewData(_)) => {
|
|
||||||
info!(context, "Idle has NewData");
|
|
||||||
}
|
|
||||||
// TODO: idle_wait does not distinguish manual interrupts
|
|
||||||
// from Timeouts if we would know it's a Timeout we could bail
|
|
||||||
// directly and reconnect .
|
|
||||||
Ok(IdleResponse::Timeout) => {
|
|
||||||
info!(context, "Idle-wait timeout or interruption");
|
|
||||||
}
|
|
||||||
Ok(IdleResponse::ManualInterrupt) => {
|
|
||||||
info!(context, "Idle wait was interrupted");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.trigger_reconnect();
|
|
||||||
return Err(Error::IdleError(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if we can't properly terminate the idle
|
|
||||||
// protocol let's break the connection.
|
|
||||||
let res =
|
|
||||||
async_std::future::timeout(Duration::from_secs(15), handle.done())
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
self.trigger_reconnect();
|
|
||||||
Error::IdleTimeout(err)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(session) => {
|
|
||||||
*self.session.lock().await = Some(Session::Secure(session));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// if we cannot terminate IDLE it probably
|
|
||||||
// means that we waited long (with idle_wait)
|
|
||||||
// but the network went away/changed
|
|
||||||
self.trigger_reconnect();
|
|
||||||
return Err(Error::IdleProtocolFailed(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IdleHandle::Insecure(mut handle) => {
|
|
||||||
if let Err(err) = handle.init().await {
|
|
||||||
return Err(Error::IdleProtocolFailed(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
|
||||||
*self.interrupt.lock().await = Some(interrupt);
|
|
||||||
|
|
||||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
|
||||||
// interrupt_idle has happened before we
|
|
||||||
// provided self.interrupt
|
|
||||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
|
||||||
std::mem::drop(idle_wait);
|
|
||||||
info!(context, "Idle wait was skipped");
|
|
||||||
} else {
|
|
||||||
info!(context, "Idle entering wait-on-remote state");
|
|
||||||
match idle_wait.await {
|
|
||||||
Ok(IdleResponse::NewData(_)) => {
|
|
||||||
info!(context, "Idle has NewData");
|
|
||||||
}
|
|
||||||
// TODO: idle_wait does not distinguish manual interrupts
|
|
||||||
// from Timeouts if we would know it's a Timeout we could bail
|
|
||||||
// directly and reconnect .
|
|
||||||
Ok(IdleResponse::Timeout) => {
|
|
||||||
info!(context, "Idle-wait timeout or interruption");
|
|
||||||
}
|
|
||||||
Ok(IdleResponse::ManualInterrupt) => {
|
|
||||||
info!(context, "Idle wait was interrupted");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.trigger_reconnect();
|
|
||||||
return Err(Error::IdleError(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if we can't properly terminate the idle
|
|
||||||
// protocol let's break the connection.
|
|
||||||
let res =
|
|
||||||
async_std::future::timeout(Duration::from_secs(15), handle.done())
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
self.trigger_reconnect();
|
|
||||||
Error::IdleTimeout(err)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(session) => {
|
|
||||||
*self.session.lock().await = Some(Session::Insecure(session));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// if we cannot terminate IDLE it probably
|
|
||||||
// means that we waited long (with idle_wait)
|
|
||||||
// but the network went away/changed
|
|
||||||
self.trigger_reconnect();
|
|
||||||
return Err(Error::IdleProtocolFailed(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
if self.skip_next_idle_wait {
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
|
|
||||||
// Idle using polling. This is also needed if we're not yet configured -
|
|
||||||
// in this case, we're waiting for a configure job (and an interrupt).
|
|
||||||
task::block_on(async move {
|
|
||||||
let fake_idle_start_time = SystemTime::now();
|
|
||||||
|
|
||||||
info!(context, "IMAP-fake-IDLEing...");
|
|
||||||
|
|
||||||
let interrupt = stop_token::StopSource::new();
|
|
||||||
|
|
||||||
// check every minute if there are new messages
|
|
||||||
// TODO: grow sleep durations / make them more flexible
|
|
||||||
let interval = async_std::stream::interval(Duration::from_secs(60));
|
|
||||||
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
|
|
||||||
*self.interrupt.lock().await = Some(interrupt);
|
|
||||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
|
||||||
// interrupt_idle has happened before we
|
// interrupt_idle has happened before we
|
||||||
// provided self.interrupt
|
// provided self.interrupt
|
||||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
self.skip_next_idle_wait = false;
|
||||||
info!(context, "fake-idle wait was skipped");
|
drop(idle_wait);
|
||||||
} else {
|
drop(interrupt);
|
||||||
// loop until we are interrupted or if we fetched something
|
|
||||||
while let Some(_) = interrupt_interval.next().await {
|
|
||||||
// try to connect with proper login params
|
|
||||||
// (setup_handle_if_needed might not know about them if we
|
|
||||||
// never successfully connected)
|
|
||||||
if let Err(err) = self.connect_configured(context) {
|
|
||||||
warn!(context, "fake_idle: could not connect: {}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if self.config.read().await.can_idle {
|
|
||||||
// we only fake-idled because network was gone during IDLE, probably
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
info!(context, "fake_idle is connected");
|
|
||||||
// we are connected, let's see if fetching messages results
|
|
||||||
// in anything. If so, we behave as if IDLE had data but
|
|
||||||
// will have already fetched the messages so perform_*_fetch
|
|
||||||
// will not find any new.
|
|
||||||
|
|
||||||
if let Some(ref watch_folder) = watch_folder {
|
info!(context, "Idle wait was skipped");
|
||||||
match self.fetch_new_messages(context, watch_folder).await {
|
} else {
|
||||||
Ok(res) => {
|
info!(context, "Idle entering wait-on-remote state");
|
||||||
info!(context, "fetch_new_messages returned {:?}", res);
|
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
|
||||||
if res {
|
self.idle_interrupt.recv().map(|probe_network| {
|
||||||
break;
|
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||||
}
|
}),
|
||||||
}
|
);
|
||||||
Err(err) => {
|
|
||||||
error!(context, "could not fetch from folder: {}", err);
|
match fut.await {
|
||||||
self.trigger_reconnect()
|
Ok(Event::IdleResponse(IdleResponse::NewData(_))) => {
|
||||||
}
|
info!(context, "Idle has NewData");
|
||||||
}
|
}
|
||||||
|
// TODO: idle_wait does not distinguish manual interrupts
|
||||||
|
// from Timeouts if we would know it's a Timeout we could bail
|
||||||
|
// directly and reconnect .
|
||||||
|
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||||
|
info!(context, "Idle-wait timeout or interruption");
|
||||||
|
}
|
||||||
|
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||||
|
info!(context, "Idle wait was interrupted");
|
||||||
|
}
|
||||||
|
Ok(Event::Interrupt(i)) => {
|
||||||
|
info = i;
|
||||||
|
info!(context, "Idle wait was interrupted");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(context, "Idle wait errored: {:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.interrupt.lock().await.take();
|
|
||||||
|
|
||||||
info!(
|
// if we can't properly terminate the idle
|
||||||
context,
|
// protocol let's break the connection.
|
||||||
"IMAP-fake-IDLE done after {:.4}s",
|
let res = handle
|
||||||
SystemTime::now()
|
.done()
|
||||||
.duration_since(fake_idle_start_time)
|
.timeout(Duration::from_secs(15))
|
||||||
.unwrap()
|
.await
|
||||||
.as_millis() as f64
|
.map_err(|err| {
|
||||||
/ 1000.,
|
self.trigger_reconnect();
|
||||||
);
|
Error::IdleTimeout(err)
|
||||||
})
|
})?;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(session) => {
|
||||||
|
self.session = Some(Session { inner: session });
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// if we cannot terminate IDLE it probably
|
||||||
|
// means that we waited long (with idle_wait)
|
||||||
|
// but the network went away/changed
|
||||||
|
self.trigger_reconnect();
|
||||||
|
return Err(Error::IdleProtocolFailed(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn interrupt_idle(&self, context: &Context) {
|
pub(crate) async fn fake_idle(
|
||||||
task::block_on(async move {
|
&mut self,
|
||||||
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take();
|
context: &Context,
|
||||||
if interrupt.is_none() {
|
watch_folder: Option<String>,
|
||||||
// idle wait is not running, signal it needs to skip
|
) -> InterruptInfo {
|
||||||
self.skip_next_idle_wait.store(true, Ordering::SeqCst);
|
// Idle using polling. This is also needed if we're not yet configured -
|
||||||
|
// in this case, we're waiting for a configure job (and an interrupt).
|
||||||
|
|
||||||
// meanwhile idle-wait may have produced the StopSource
|
let fake_idle_start_time = SystemTime::now();
|
||||||
interrupt = self.interrupt.lock().await.take();
|
info!(context, "IMAP-fake-IDLEing...");
|
||||||
|
|
||||||
|
// Do not poll, just wait for an interrupt when no folder is passed in.
|
||||||
|
if watch_folder.is_none() {
|
||||||
|
return self.idle_interrupt.recv().await.unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut info: InterruptInfo = Default::default();
|
||||||
|
if self.skip_next_idle_wait {
|
||||||
|
// interrupt_idle has happened before we
|
||||||
|
// provided self.interrupt
|
||||||
|
self.skip_next_idle_wait = false;
|
||||||
|
info!(context, "fake-idle wait was skipped");
|
||||||
|
} else {
|
||||||
|
// check every minute if there are new messages
|
||||||
|
// TODO: grow sleep durations / make them more flexible
|
||||||
|
let mut interval = async_std::stream::interval(Duration::from_secs(60));
|
||||||
|
|
||||||
|
enum Event {
|
||||||
|
Tick,
|
||||||
|
Interrupt(InterruptInfo),
|
||||||
}
|
}
|
||||||
// let's manually drop the StopSource
|
// loop until we are interrupted or if we fetched something
|
||||||
if interrupt.is_some() {
|
info =
|
||||||
// the imap thread provided us a stop token but might
|
loop {
|
||||||
// not have entered idle_wait yet, give it some time
|
use futures::future::FutureExt;
|
||||||
// for that to happen. XXX handle this without extra wait
|
match interval
|
||||||
// https://github.com/deltachat/deltachat-core-rust/issues/925
|
.next()
|
||||||
std::thread::sleep(Duration::from_millis(200));
|
.map(|_| Event::Tick)
|
||||||
info!(context, "low-level: dropping stop-source to interrupt idle");
|
.race(self.idle_interrupt.recv().map(|probe_network| {
|
||||||
std::mem::drop(interrupt)
|
Event::Interrupt(probe_network.unwrap_or_default())
|
||||||
}
|
}))
|
||||||
});
|
.await
|
||||||
|
{
|
||||||
|
Event::Tick => {
|
||||||
|
// try to connect with proper login params
|
||||||
|
// (setup_handle_if_needed might not know about them if we
|
||||||
|
// never successfully connected)
|
||||||
|
if let Err(err) = self.connect_configured(context).await {
|
||||||
|
warn!(context, "fake_idle: could not connect: {}", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.config.can_idle {
|
||||||
|
// we only fake-idled because network was gone during IDLE, probably
|
||||||
|
break InterruptInfo::new(false, None);
|
||||||
|
}
|
||||||
|
info!(context, "fake_idle is connected");
|
||||||
|
// we are connected, let's see if fetching messages results
|
||||||
|
// in anything. If so, we behave as if IDLE had data but
|
||||||
|
// will have already fetched the messages so perform_*_fetch
|
||||||
|
// will not find any new.
|
||||||
|
|
||||||
|
if let Some(ref watch_folder) = watch_folder {
|
||||||
|
match self.fetch_new_messages(context, watch_folder).await {
|
||||||
|
Ok(res) => {
|
||||||
|
info!(context, "fetch_new_messages returned {:?}", res);
|
||||||
|
if res {
|
||||||
|
break InterruptInfo::new(false, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(context, "could not fetch from folder: {}", err);
|
||||||
|
self.trigger_reconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Interrupt(info) => {
|
||||||
|
// Interrupt
|
||||||
|
break info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"IMAP-fake-IDLE done after {:.4}s",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(fake_idle_start_time)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as f64
|
||||||
|
/ 1000.,
|
||||||
|
);
|
||||||
|
|
||||||
|
info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1668
src/imap/mod.rs
1668
src/imap/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -4,36 +4,63 @@ use crate::context::Context;
|
|||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "IMAP Could not obtain imap-session object.")]
|
#[error("IMAP Could not obtain imap-session object.")]
|
||||||
NoSession,
|
NoSession,
|
||||||
|
|
||||||
#[fail(display = "IMAP Connection Lost or no connection established")]
|
#[error("IMAP Connection Lost or no connection established")]
|
||||||
ConnectionLost,
|
ConnectionLost,
|
||||||
|
|
||||||
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
|
#[error("IMAP Folder name invalid: {0}")]
|
||||||
BadFolderName(String),
|
BadFolderName(String),
|
||||||
|
|
||||||
#[fail(display = "IMAP close/expunge failed: {}", _0)]
|
#[error("IMAP close/expunge failed")]
|
||||||
CloseExpungeFailed(#[cause] async_imap::error::Error),
|
CloseExpungeFailed(#[from] async_imap::error::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
#[error("IMAP other error: {0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Imap {
|
impl Imap {
|
||||||
|
/// Issues a CLOSE command to expunge selected folder.
|
||||||
|
///
|
||||||
|
/// CLOSE is considerably faster than an EXPUNGE, see
|
||||||
|
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
|
||||||
|
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
|
||||||
|
if let Some(ref folder) = self.config.selected_folder {
|
||||||
|
info!(context, "Expunge messages in \"{}\".", folder);
|
||||||
|
|
||||||
|
if let Some(ref mut session) = self.session {
|
||||||
|
match session.close().await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!(context, "close/expunge succeeded");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.trigger_reconnect();
|
||||||
|
return Err(Error::CloseExpungeFailed(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::NoSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.config.selected_folder = None;
|
||||||
|
self.config.selected_folder_needs_expunge = false;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// select a folder, possibly update uid_validity and, if needed,
|
/// select a folder, possibly update uid_validity and, if needed,
|
||||||
/// expunge the folder to remove delete-marked messages.
|
/// expunge the folder to remove delete-marked messages.
|
||||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||||
&self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
folder: Option<S>,
|
folder: Option<S>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if self.session.lock().await.is_none() {
|
if self.session.is_none() {
|
||||||
let mut cfg = self.config.write().await;
|
self.config.selected_folder = None;
|
||||||
cfg.selected_folder = None;
|
self.config.selected_folder_needs_expunge = false;
|
||||||
cfg.selected_folder_needs_expunge = false;
|
|
||||||
self.trigger_reconnect();
|
self.trigger_reconnect();
|
||||||
return Err(Error::NoSession);
|
return Err(Error::NoSession);
|
||||||
}
|
}
|
||||||
@@ -41,7 +68,7 @@ impl Imap {
|
|||||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||||
if let Some(ref folder) = folder {
|
if let Some(ref folder) = folder {
|
||||||
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
|
if let Some(ref selected_folder) = self.config.selected_folder {
|
||||||
if folder.as_ref() == selected_folder {
|
if folder.as_ref() == selected_folder {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -49,33 +76,14 @@ impl Imap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||||
let needs_expunge = { self.config.read().await.selected_folder_needs_expunge };
|
let needs_expunge = { self.config.selected_folder_needs_expunge };
|
||||||
if needs_expunge {
|
if needs_expunge {
|
||||||
if let Some(ref folder) = self.config.read().await.selected_folder {
|
self.close_folder(context).await?;
|
||||||
info!(context, "Expunge messages in \"{}\".", folder);
|
|
||||||
|
|
||||||
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
|
|
||||||
// https://tools.ietf.org/html/rfc3501#section-6.4.2
|
|
||||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
|
||||||
match session.close().await {
|
|
||||||
Ok(_) => {
|
|
||||||
info!(context, "close/expunge succeeded");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.trigger_reconnect();
|
|
||||||
return Err(Error::CloseExpungeFailed(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::NoSession);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.config.write().await.selected_folder_needs_expunge = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// select new folder
|
// select new folder
|
||||||
if let Some(ref folder) = folder {
|
if let Some(ref folder) = folder {
|
||||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
if let Some(ref mut session) = &mut self.session {
|
||||||
let res = session.select(folder).await;
|
let res = session.select(folder).await;
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc3501#section-6.3.1
|
// https://tools.ietf.org/html/rfc3501#section-6.3.1
|
||||||
@@ -84,21 +92,20 @@ impl Imap {
|
|||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(mailbox) => {
|
Ok(mailbox) => {
|
||||||
let mut config = self.config.write().await;
|
self.config.selected_folder = Some(folder.as_ref().to_string());
|
||||||
config.selected_folder = Some(folder.as_ref().to_string());
|
self.config.selected_mailbox = Some(mailbox);
|
||||||
config.selected_mailbox = Some(mailbox);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(async_imap::error::Error::ConnectionLost) => {
|
Err(async_imap::error::Error::ConnectionLost) => {
|
||||||
self.trigger_reconnect();
|
self.trigger_reconnect();
|
||||||
self.config.write().await.selected_folder = None;
|
self.config.selected_folder = None;
|
||||||
Err(Error::ConnectionLost)
|
Err(Error::ConnectionLost)
|
||||||
}
|
}
|
||||||
Err(async_imap::error::Error::Validate(_)) => {
|
Err(async_imap::error::Error::Validate(_)) => {
|
||||||
Err(Error::BadFolderName(folder.as_ref().to_string()))
|
Err(Error::BadFolderName(folder.as_ref().to_string()))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.config.write().await.selected_folder = None;
|
self.config.selected_folder = None;
|
||||||
self.trigger_reconnect();
|
self.trigger_reconnect();
|
||||||
Err(Error::Other(err.to_string()))
|
Err(Error::Other(err.to_string()))
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/imap/session.rs
Normal file
40
src/imap/session.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use async_imap::Session as ImapSession;
|
||||||
|
use async_native_tls::TlsStream;
|
||||||
|
use async_std::net::TcpStream;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Session {
|
||||||
|
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait SessionStream:
|
||||||
|
async_std::io::Read + async_std::io::Write + Unpin + Send + Sync + std::fmt::Debug
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
|
||||||
|
impl SessionStream for TlsStream<TcpStream> {}
|
||||||
|
impl SessionStream for TcpStream {}
|
||||||
|
|
||||||
|
impl Deref for Session {
|
||||||
|
type Target = ImapSession<Box<dyn SessionStream>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Session {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
|
||||||
|
let Session { inner } = self;
|
||||||
|
inner.idle()
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user