mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 13:32:11 +03:00
Compare commits
488 Commits
export_cha
...
blob-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
579e5dbfde | ||
|
|
f3a716fac6 | ||
|
|
72659580de | ||
|
|
30cb0cbcfd | ||
|
|
4136217249 | ||
|
|
246cae5d9e | ||
|
|
12313543ca | ||
|
|
87e3dead14 | ||
|
|
9af36460c2 | ||
|
|
2e2d881e01 | ||
|
|
db58946312 | ||
|
|
9a02a58273 | ||
|
|
147f5c1e0d | ||
|
|
f0ca50ba27 | ||
|
|
83137b5968 | ||
|
|
12823c2213 | ||
|
|
0b810d7d65 | ||
|
|
7aebdc9b7b | ||
|
|
6859b651a8 | ||
|
|
d47680733b | ||
|
|
273a38d781 | ||
|
|
93d1162caf | ||
|
|
8d550a66a3 | ||
|
|
4c58e05be3 | ||
|
|
500563054e | ||
|
|
2a11f8f59d | ||
|
|
9e7bdc579e | ||
|
|
61af0c9ac4 | ||
|
|
91f02ad553 | ||
|
|
b20d3dfc10 | ||
|
|
3e7666021c | ||
|
|
ae58bceeb9 | ||
|
|
6665b9e13c | ||
|
|
367a9705e9 | ||
|
|
8d3a1e84c3 | ||
|
|
d009835210 | ||
|
|
83a664ca68 | ||
|
|
d98d1857a4 | ||
|
|
33a514aa54 | ||
|
|
0aefdc85e6 | ||
|
|
3ff0964f02 | ||
|
|
cf33db3dcb | ||
|
|
dae80cbe35 | ||
|
|
f8b4ef26b3 | ||
|
|
db991453b0 | ||
|
|
c11ce4c8d4 | ||
|
|
92e300cb9f | ||
|
|
d210e0bffe | ||
|
|
4d4968f358 | ||
|
|
8dfede148a | ||
|
|
6d125028f5 | ||
|
|
7ff3cf4af0 | ||
|
|
bb3353397d | ||
|
|
572260ec29 | ||
|
|
0b1faa0523 | ||
|
|
3b6c3e10d7 | ||
|
|
d3909a5483 | ||
|
|
645fd10446 | ||
|
|
ee7e29fb3a | ||
|
|
bdc3a4d24c | ||
|
|
88ccda139e | ||
|
|
63e78bae37 | ||
|
|
b50f211c28 | ||
|
|
5efbd9c7f5 | ||
|
|
7f1d2ea11f | ||
|
|
46eb391a1b | ||
|
|
b166cc5bf4 | ||
|
|
1d0f6aad95 | ||
|
|
764aa71770 | ||
|
|
21e9206a77 | ||
|
|
5f29977c50 | ||
|
|
ee4c9bc01e | ||
|
|
4a8259cb12 | ||
|
|
a6441d70f0 | ||
|
|
01db8d0130 | ||
|
|
8ad9db5572 | ||
|
|
607cd23014 | ||
|
|
220758d244 | ||
|
|
7ab71bb468 | ||
|
|
a74377b620 | ||
|
|
c9effa3c06 | ||
|
|
8b8102334b | ||
|
|
5dedb86472 | ||
|
|
d93f77f991 | ||
|
|
5409bc575e | ||
|
|
576d31206d | ||
|
|
86e3297414 | ||
|
|
ab4a947456 | ||
|
|
5ce2581b4c | ||
|
|
c0d6c6b882 | ||
|
|
2bc8c967b1 | ||
|
|
fee08f0eeb | ||
|
|
078c3d05d7 | ||
|
|
7f97768c56 | ||
|
|
ecd548a7aa | ||
|
|
62efb0795b | ||
|
|
53f042ee08 | ||
|
|
6ce97bd0cd | ||
|
|
c29149e74c | ||
|
|
487f7593ce | ||
|
|
6b3b33d2a0 | ||
|
|
2d70ccc2bf | ||
|
|
e90fc9504a | ||
|
|
5108314c03 | ||
|
|
c1e144d4db | ||
|
|
f2b86a1c0f | ||
|
|
9583d41446 | ||
|
|
e594064f93 | ||
|
|
7c52fd95ec | ||
|
|
d0d5ac6e87 | ||
|
|
416bf3a829 | ||
|
|
cc78347293 | ||
|
|
27e4ae992a | ||
|
|
a1767dc153 | ||
|
|
eb610c27bf | ||
|
|
016fb2ceb2 | ||
|
|
5c571520a0 | ||
|
|
ddefd2cf09 | ||
|
|
30a3eeece8 | ||
|
|
5919388588 | ||
|
|
2cc738f481 | ||
|
|
babd405928 | ||
|
|
e885857875 | ||
|
|
a1d57a2645 | ||
|
|
a28aecd4d1 | ||
|
|
a3f1ff2827 | ||
|
|
c4d1a639b0 | ||
|
|
8732b7a55c | ||
|
|
1b9148f28e | ||
|
|
e0129c3b43 | ||
|
|
60d41022ea | ||
|
|
dd4f2ac671 | ||
|
|
eebb2a3b68 | ||
|
|
0d62069b67 | ||
|
|
56cf2e6596 | ||
|
|
59bd5481b9 | ||
|
|
6c8da526a0 | ||
|
|
13bc8b78d7 | ||
|
|
c7c68094d9 | ||
|
|
84f54b10dc | ||
|
|
cebc9e3e91 | ||
|
|
1379f8a055 | ||
|
|
53d049e5f5 | ||
|
|
4968f72dfb | ||
|
|
b24a0ed8fd | ||
|
|
c810347c7c | ||
|
|
cf8c4142c7 | ||
|
|
f71901b5f9 | ||
|
|
4419f9c4e7 | ||
|
|
4e5982b682 | ||
|
|
39e1510e64 | ||
|
|
64206160cc | ||
|
|
6376659348 | ||
|
|
43b2a4ad27 | ||
|
|
eaf06bb239 | ||
|
|
5e26b5bfdc | ||
|
|
60fbb6df5a | ||
|
|
3e60ee9d3e | ||
|
|
54e79409e6 | ||
|
|
31d113207b | ||
|
|
3a014477e7 | ||
|
|
90d8c8baf5 | ||
|
|
1dee17f980 | ||
|
|
6aeb21d3af | ||
|
|
4747ae2f1c | ||
|
|
7968f55191 | ||
|
|
33aa3556d2 | ||
|
|
b8b7563fca | ||
|
|
0d3f90770e | ||
|
|
51a4f0aa76 | ||
|
|
ebb89e20b4 | ||
|
|
8fc60e321b | ||
|
|
8ef6b6089f | ||
|
|
5b5b26122e | ||
|
|
300f5be4f3 | ||
|
|
c5d47ffcb0 | ||
|
|
3b7b8ea0f1 | ||
|
|
59739ee5c9 | ||
|
|
63207eb681 | ||
|
|
65f09c238b | ||
|
|
bb97d842df | ||
|
|
4dba5ab5f9 | ||
|
|
4c0e46fd44 | ||
|
|
ee3b40a59a | ||
|
|
e511b87955 | ||
|
|
53f51ad312 | ||
|
|
3878c4f041 | ||
|
|
499e4d3242 | ||
|
|
b5d0907090 | ||
|
|
6613fa67ee | ||
|
|
41ec380b55 | ||
|
|
7fb305e898 | ||
|
|
d4255a4979 | ||
|
|
b21dcd17b7 | ||
|
|
0caea85d16 | ||
|
|
42e0fb5eb9 | ||
|
|
6061d71492 | ||
|
|
dbd8814d2c | ||
|
|
a3562c5940 | ||
|
|
49b07c1c6a | ||
|
|
51d220f1e0 | ||
|
|
9f81a94d86 | ||
|
|
f6098fc931 | ||
|
|
6e3c2fc839 | ||
|
|
30e616f74f | ||
|
|
5e29cae81a | ||
|
|
1ee19bf3ca | ||
|
|
b18bdd1b00 | ||
|
|
6c59b0de85 | ||
|
|
c1d82ad417 | ||
|
|
ba931773d1 | ||
|
|
b6f88a9fca | ||
|
|
b0902102a2 | ||
|
|
4f19036408 | ||
|
|
fe1f9c0ed9 | ||
|
|
bcadd0cd5c | ||
|
|
30a3da97da | ||
|
|
a8b2a20146 | ||
|
|
82819a642f | ||
|
|
3960d4129e | ||
|
|
e405ddf080 | ||
|
|
1eadbbb7cd | ||
|
|
941b8caa8b | ||
|
|
95bce993ad | ||
|
|
acbf363fc8 | ||
|
|
2309c7ca13 | ||
|
|
89d8b26192 | ||
|
|
ee32a7b00a | ||
|
|
1dbbf6b3be | ||
|
|
f8a4a88fb2 | ||
|
|
3096193d58 | ||
|
|
d8b47dc4aa | ||
|
|
a5826d6a06 | ||
|
|
5df0be8311 | ||
|
|
66a5e0743d | ||
|
|
43d1d9b1b3 | ||
|
|
3e0f601212 | ||
|
|
085a899de2 | ||
|
|
b07e20b955 | ||
|
|
4e8724694a | ||
|
|
47bf67e658 | ||
|
|
7bb7748b6b | ||
|
|
b33ad05c3b | ||
|
|
398cea6466 | ||
|
|
1afd2f2d66 | ||
|
|
48f1ef3641 | ||
|
|
e95911a484 | ||
|
|
b1af486e10 | ||
|
|
bffb41326c | ||
|
|
c532055153 | ||
|
|
be595f8601 | ||
|
|
1d1d98e02b | ||
|
|
771e84af6e | ||
|
|
bbfed20d34 | ||
|
|
0f2095947c | ||
|
|
46956caf75 | ||
|
|
6f3dd7f0c2 | ||
|
|
15dcd62652 | ||
|
|
da2f30786b | ||
|
|
50a5e715d2 | ||
|
|
1bef623c89 | ||
|
|
7745db8310 | ||
|
|
1d1491c95d | ||
|
|
2a0f6f5cf7 | ||
|
|
b27793e852 | ||
|
|
8fb5e038a9 | ||
|
|
e518dc3331 | ||
|
|
ea1368a36b | ||
|
|
0aeb2bd6fb | ||
|
|
0263d0816a | ||
|
|
bb71f6ec98 | ||
|
|
02a1abc0d5 | ||
|
|
40fe65716f | ||
|
|
d05b399eac | ||
|
|
c31216f043 | ||
|
|
f66bde7275 | ||
|
|
7f819de49f | ||
|
|
5f065b245f | ||
|
|
3c43d790a3 | ||
|
|
d33177a721 | ||
|
|
aa2e03382b | ||
|
|
2a59e6121b | ||
|
|
1a438d61df | ||
|
|
444486f5df | ||
|
|
1eae2477c3 | ||
|
|
7b3eefc6c6 | ||
|
|
4dd0830baf | ||
|
|
8e3f062881 | ||
|
|
cf445f265a | ||
|
|
963c66b76c | ||
|
|
79df667e1e | ||
|
|
785c796bd6 | ||
|
|
6a2112ba66 | ||
|
|
3f170279da | ||
|
|
3408501a75 | ||
|
|
3b765cb3c9 | ||
|
|
8a9ea388ed | ||
|
|
77acf910bf | ||
|
|
c04c87658c | ||
|
|
fd784ec223 | ||
|
|
25f1b0c4af | ||
|
|
580ec6e6ce | ||
|
|
8e5195c4f6 | ||
|
|
729a1e1cd2 | ||
|
|
78b93f3621 | ||
|
|
4111489daf | ||
|
|
b7bd4c6ba7 | ||
|
|
83dc0bc2b0 | ||
|
|
51c6467feb | ||
|
|
6a60ae2f09 | ||
|
|
7be0583628 | ||
|
|
2b74a705ef | ||
|
|
9dedcad220 | ||
|
|
71e0493c4a | ||
|
|
1679ddddf0 | ||
|
|
de258645f4 | ||
|
|
b463b602a9 | ||
|
|
3aa2b57ac1 | ||
|
|
ab1de69fbc | ||
|
|
90703b0dd2 | ||
|
|
a163be9248 | ||
|
|
2b7bf11b05 | ||
|
|
f95e1db8e2 | ||
|
|
d0c97bce4c | ||
|
|
3440daca1a | ||
|
|
d0bfb555dd | ||
|
|
6ffaa38b37 | ||
|
|
339d46ecf0 | ||
|
|
5399c9151d | ||
|
|
53cd633e8d | ||
|
|
ade39fe026 | ||
|
|
b8dad1dbaf | ||
|
|
72d503fa32 | ||
|
|
223aeb7b1a | ||
|
|
b315c6f6d5 | ||
|
|
481276cf46 | ||
|
|
faab61b0d4 | ||
|
|
20bf41b4e6 | ||
|
|
5a5b80c960 | ||
|
|
ac245a6cb2 | ||
|
|
126beb62f3 | ||
|
|
1f642046bc | ||
|
|
d79e4a6571 | ||
|
|
cadc0b2c00 | ||
|
|
87071e6d4b | ||
|
|
057b004553 | ||
|
|
4071fe53a0 | ||
|
|
c3062976c0 | ||
|
|
85efc0ea26 | ||
|
|
4ef80aaea5 | ||
|
|
0f86800f5d | ||
|
|
9c2035538c | ||
|
|
0276938975 | ||
|
|
ee44a162b6 | ||
|
|
c09a83df2b | ||
|
|
8729d2c4aa | ||
|
|
fdf3397437 | ||
|
|
3712524765 | ||
|
|
0b5c4df432 | ||
|
|
0f0072f5a2 | ||
|
|
09066571be | ||
|
|
8963dab7a4 | ||
|
|
265d54e431 | ||
|
|
ffb17c4e61 | ||
|
|
44bd9f93b4 | ||
|
|
5287a3de40 | ||
|
|
6f644f5c7c | ||
|
|
31b930b2fa | ||
|
|
0574aeb768 | ||
|
|
bf68bc14a4 | ||
|
|
c380647c12 | ||
|
|
e22a9999d7 | ||
|
|
57870ec54a | ||
|
|
a6e1dc4f16 | ||
|
|
fc441d4a44 | ||
|
|
9a77a7b66f | ||
|
|
5856936f49 | ||
|
|
532060d8b7 | ||
|
|
0691aa3d2c | ||
|
|
ef9fbf9eba | ||
|
|
3647aac4e6 | ||
|
|
f88f4155ae | ||
|
|
065b574d93 | ||
|
|
5c36b6e119 | ||
|
|
cd0da723ce | ||
|
|
49fc72fa42 | ||
|
|
a1aaa1e0b4 | ||
|
|
1eab99df56 | ||
|
|
d9caf5853d | ||
|
|
8869c34539 | ||
|
|
05bb25c645 | ||
|
|
b340459752 | ||
|
|
980d2a9433 | ||
|
|
5f365b259b | ||
|
|
b070198063 | ||
|
|
6e7f63dba7 | ||
|
|
eff64ed9b0 | ||
|
|
49acfd90eb | ||
|
|
aec8332544 | ||
|
|
188353d581 | ||
|
|
3bd5b7e604 | ||
|
|
61e1e18088 | ||
|
|
a5065c21af | ||
|
|
cd958c6a33 | ||
|
|
308403ad99 | ||
|
|
599be61566 | ||
|
|
64088f02a2 | ||
|
|
77aa8b2c3f | ||
|
|
5bffdc6bbf | ||
|
|
350fe06ea9 | ||
|
|
e100dca348 | ||
|
|
f1c4c40aec | ||
|
|
f96d04e80f | ||
|
|
c1d3e9358d | ||
|
|
e77651f2f5 | ||
|
|
056f3ecf03 | ||
|
|
8700cf0aba | ||
|
|
a6ad457065 | ||
|
|
f113b43046 | ||
|
|
0b3eece26d | ||
|
|
8ce9a78d6c | ||
|
|
ad266fe82f | ||
|
|
15c38ba395 | ||
|
|
70e776e407 | ||
|
|
6b5ba35d5b | ||
|
|
7b9e54be56 | ||
|
|
6202f85a6f | ||
|
|
8ac2bd0298 | ||
|
|
3f00a6efbe | ||
|
|
a411fe1e01 | ||
|
|
8ea773628d | ||
|
|
a47c0486ae | ||
|
|
c08df8d3da | ||
|
|
1a830c23b5 | ||
|
|
18ace81842 | ||
|
|
838957badd | ||
|
|
f820671d53 | ||
|
|
bf61c16dc1 | ||
|
|
96f0e47091 | ||
|
|
514c4bc8a7 | ||
|
|
b53613d1e0 | ||
|
|
4c4f24fb35 | ||
|
|
475fa24876 | ||
|
|
cf8736da48 | ||
|
|
a638259c36 | ||
|
|
d821cdf1c8 | ||
|
|
62e9fbf68c | ||
|
|
15664be4f6 | ||
|
|
62388514dd | ||
|
|
ad7c7e90b3 | ||
|
|
b16785bb62 | ||
|
|
d12d9d94d6 | ||
|
|
991d15615e | ||
|
|
5dee1efa59 | ||
|
|
1870684c43 | ||
|
|
1803db2dfe | ||
|
|
7fee3d995c | ||
|
|
4b62500989 | ||
|
|
8f2cb1e8ab | ||
|
|
72ebd83479 | ||
|
|
2842042304 | ||
|
|
25fed9ab52 | ||
|
|
751b9add09 | ||
|
|
b727190da5 | ||
|
|
368fa9fc44 | ||
|
|
c07c5bb358 | ||
|
|
67f8fb4b66 | ||
|
|
056721b916 | ||
|
|
4fe3a80f96 | ||
|
|
6c530b4c77 | ||
|
|
c616b65ce4 | ||
|
|
50f680a00b | ||
|
|
47e0f224ca | ||
|
|
8b872b7e6f | ||
|
|
29356a6ca8 | ||
|
|
c5539de4da | ||
|
|
b017af78ce | ||
|
|
3b897eac53 | ||
|
|
d8a3014896 | ||
|
|
4209960c0f | ||
|
|
04c8622e94 | ||
|
|
002e33d28c | ||
|
|
35aeda3849 | ||
|
|
af287ee9a8 | ||
|
|
cc3e8c5117 | ||
|
|
1127521923 | ||
|
|
bf7f64d50b | ||
|
|
8380ac28c1 |
@@ -1,77 +0,0 @@
|
||||
version: 2.1
|
||||
executors:
|
||||
default:
|
||||
docker:
|
||||
- image: filecoin/rust:latest
|
||||
working_directory: /mnt/crate
|
||||
doxygen:
|
||||
docker:
|
||||
- image: hrektts/doxygen
|
||||
|
||||
jobs:
|
||||
build_doxygen:
|
||||
executor: doxygen
|
||||
steps:
|
||||
- checkout
|
||||
- run: bash scripts/run-doxygen.sh
|
||||
- run: mkdir -p workspace/c-docs
|
||||
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
- c-docs
|
||||
|
||||
remote_python_packaging:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
# the following commands on success produces
|
||||
# workspace/{wheelhouse,py-docs} as artefact directories
|
||||
- run:
|
||||
# building aarch64 packages under qemu is very slow
|
||||
no_output_timeout: 60m
|
||||
command: bash scripts/remote_python_packaging.sh
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
# - c-docs
|
||||
- py-docs
|
||||
- wheelhouse
|
||||
|
||||
upload_docs_wheels:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: workspace
|
||||
- run: ls -laR workspace
|
||||
- run: scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||
|
||||
workflows:
|
||||
version: 2.1
|
||||
|
||||
test:
|
||||
jobs:
|
||||
- remote_python_packaging:
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
- upload_docs_wheels:
|
||||
requires:
|
||||
- remote_python_packaging
|
||||
- build_doxygen
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
- staging
|
||||
- trying
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
@@ -18,9 +21,11 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.50.0
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
@@ -32,9 +37,11 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.50.0
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -68,12 +75,22 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.50.0
|
||||
python: 3.6
|
||||
rust: 1.54.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.50.0
|
||||
rust: 1.54.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.51.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.51.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
@@ -84,31 +101,14 @@ jobs:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl
|
||||
args: --all --bins --examples --tests --features repl --benches
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -140,4 +140,4 @@ jobs:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,doc,py3
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ deltachat-ffi/xml
|
||||
.rsynclist
|
||||
|
||||
coverage/
|
||||
.DS_Store
|
||||
|
||||
313
CHANGELOG.md
313
CHANGELOG.md
@@ -1,4 +1,315 @@
|
||||
# Changelog
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### API changes
|
||||
|
||||
- Removed `mvbox_watch` option. #2906
|
||||
It is automatically enabled whenever `mvbox_move` is enabled.
|
||||
|
||||
## 1.70.0
|
||||
|
||||
### Fixes
|
||||
- fix: do not abort Param parsing on unknown keys #2856
|
||||
- fix: execute `Chat-Group-Member-Removed:` even when arriving disordered #2857
|
||||
|
||||
|
||||
## 1.69.0
|
||||
|
||||
### Fixes
|
||||
- fix group-related system messages in multi-device setups #2848
|
||||
- fix "Google Workspace" (former "G Suite") issues related to bad resolvers #2852
|
||||
|
||||
|
||||
## 1.68.0
|
||||
|
||||
### Fixes
|
||||
- fix chat assignment when forwarding #2843
|
||||
- fix layout issues with the generated QR code svg #2842
|
||||
|
||||
|
||||
## 1.67.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_securejoin_qr_svg(chat_id)` added #2815
|
||||
- added stock-strings `DC_STR_SETUP_CONTACT_QR_DESC` and `DC_STR_SECURE_JOIN_GROUP_QR_DESC`
|
||||
|
||||
|
||||
## 1.66.0
|
||||
|
||||
### API changes
|
||||
- `dc_contact_get_last_seen()` added #2823
|
||||
- python: `Contact.last_seen` added #2823
|
||||
- removed `DC_STR_NEWGROUPDRAFT`, we don't set draft after creating group anymore #2805
|
||||
|
||||
### Changes
|
||||
- python: add cutil.from_optional_dc_charpointer() #2824
|
||||
- refactorings #2807 #2822 #2825
|
||||
|
||||
|
||||
## 1.65.0
|
||||
|
||||
### Changes
|
||||
- python: add mypy support and some type hints #2809
|
||||
|
||||
### Fixes
|
||||
- do not disable ephemeral timer when downloading a message partially #2811
|
||||
- apply existing ephemeral timer also to partially downloaded messages;
|
||||
after full download, the ephemeral timer starts over #2811
|
||||
- replace user-visible error on verification failure with warning;
|
||||
the error is logged to the corresponding chat anyway #2808
|
||||
|
||||
|
||||
## 1.64.0
|
||||
|
||||
### Fixes
|
||||
- add 'waiting for being added to the group' only for group-joins,
|
||||
not for setup-contact #2797
|
||||
- prioritize In-Reply-To: and References: headers over group IDs when assigning
|
||||
messages to chats to fix incorrect assignment of Delta Chat replies to
|
||||
classic email threads #2795
|
||||
|
||||
|
||||
## 1.63.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_last_error()` added #2788
|
||||
|
||||
### Changes
|
||||
- Optimize Autocrypt gossip #2743
|
||||
|
||||
### Fixes
|
||||
- fix permanently hiding of one-to-one chats after secure-join #2791
|
||||
|
||||
|
||||
## 1.62.0
|
||||
|
||||
### API Changes
|
||||
- `dc_join_securejoin()` now always returns immediately;
|
||||
the returned chat may not allow sending (`dc_chat_can_send()` returns false)
|
||||
which may change as usual on `DC_EVENT_CHAT_MODIFIED` #2508 #2767
|
||||
- introduce multi-device-sync-messages;
|
||||
as older cores display them as files in self-chat,
|
||||
they are currently only sent if config option `send_sync_msgs` is set #2669
|
||||
- add `DC_EVENT_SELFAVATAR_CHANGED` #2742
|
||||
|
||||
### Changes
|
||||
- use system DNS instead of google for MX queries #2780
|
||||
- improve error logging #2758
|
||||
- improve tests #2764 #2781
|
||||
- improve ci #2770
|
||||
- refactorings #2677 #2728 #2740 #2729 #2766 #2778
|
||||
|
||||
### Fixes
|
||||
- add Let's Encrypt certificate to core as it may be missing older devices #2752
|
||||
- prioritize certificate setting from user over the one from provider-db #2749
|
||||
- fix "QR process failed" error #2725
|
||||
- do not update quota in endless loop #2726
|
||||
|
||||
|
||||
## 1.61.0
|
||||
|
||||
### API Changes
|
||||
- download-on-demand added: `dc_msg_get_download_state()`, `dc_download_full_msg()`
|
||||
and `download_limit` config option #2631 #2696
|
||||
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
|
||||
- allow ui-specific configs using `ui.`-prefix in key (`dc_set_config(context, "ui.*", value)`) #2672
|
||||
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
|
||||
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
|
||||
- emit warnings and errors from account manager with account-id 0 #2712
|
||||
|
||||
### Changes
|
||||
- notify about incoming contact requests #2690
|
||||
- messages are marked as read on first read receipt #2699
|
||||
- quota warning reappears after import, rewarning at 95% #2702
|
||||
- lock strict TLS if certificate checks are automatic #2711
|
||||
- always check certificates strictly when connecting over SOCKS5 in Automatic mode #2657
|
||||
- `Accounts` is not cloneable anymore #2654 #2658
|
||||
- update chat/contact data only when there was no newer update #2642
|
||||
- better detection of mailing list names #2665 #2685
|
||||
- log all decisions when applying ephemeral timer to chats #2679
|
||||
- connectivity view now translatable #2694 #2723
|
||||
- improve Doxygen documentation #2647 #2668 #2684 #2688 #2705
|
||||
- refactorings #2656 #2659 #2677 #2673 #2678 #2675 #2663 #2692 #2706
|
||||
- update provider database #2618
|
||||
|
||||
### Fixes
|
||||
- ephemeral timer rollback protection #2693 #2709
|
||||
- recreate configured folders if they are deleted #2691
|
||||
- ignore MDNs sent to self #2674
|
||||
- recognize NDNs that put headers into "message/global-headers" part #2598
|
||||
- avoid `dc_get_contacts()` returning duplicate contact ids #2591
|
||||
- do not leak group names on forwarding messages #2719
|
||||
- in case of smtp-errors, iterate over all addresses to fix ipv6/v4 problems #2720
|
||||
- fix pkg-config file #2660
|
||||
- fix "QR process failed" error #2725
|
||||
|
||||
|
||||
## 1.60.0
|
||||
|
||||
### Added
|
||||
- add device message to warn about QUOTA #2621
|
||||
- add SOCKS5 support #2474 #2620
|
||||
|
||||
### Changes
|
||||
- don't emit multiple events with the same import/export progress number #2639
|
||||
- reduce message length limit to 5000 chars #2615
|
||||
|
||||
### Fixes
|
||||
- keep event emitter from closing when there are no accounts #2636
|
||||
|
||||
|
||||
## 1.59.0
|
||||
|
||||
### Added
|
||||
- add quota information to `dc_get_connectivity_html()`
|
||||
|
||||
### Changes
|
||||
- refactorings #2592 #2570 #2581
|
||||
- add 'device chat about' to now existing status #2613
|
||||
- update provider database #2608
|
||||
|
||||
### Fixes
|
||||
- provider database supports socket=PLAIN and dotless domains now #2604 #2608
|
||||
- add migrated accounts to events emitter #2607
|
||||
- fix forwarding quote-only mails #2600
|
||||
- do not set WantsMdn param for outgoing messages #2603
|
||||
- set timestamps for system messages #2593
|
||||
- do not treat gmail labels as folders #2587
|
||||
- avoid timing problems in `dc_maybe_network_lost()` #2551
|
||||
- only set smtp to "connected" if the last message was actually sent #2541
|
||||
|
||||
|
||||
## 1.58.0
|
||||
|
||||
### Fixes
|
||||
- move WAL file together with database
|
||||
and avoid using data if the database was not closed correctly before #2583
|
||||
|
||||
|
||||
## 1.57.0
|
||||
|
||||
### API Changes
|
||||
|
||||
- breaking change: removed deaddrop chat #2514 #2563
|
||||
|
||||
Contact request chats are not merged into a single virtual
|
||||
"deaddrop" chat anymore. Instead, they are shown in the chatlist the
|
||||
same way as other chats, but sending of messages to them is not
|
||||
allowed and MDNs are not sent automatically until the chat is
|
||||
"accepted" by the user.
|
||||
|
||||
New API:
|
||||
- `dc_chat_is_contact_request()`: returns true if chat is a contact
|
||||
request. In this case an option to accept the chat via
|
||||
`dc_accept_chat()` should be shown in the UI.
|
||||
- `dc_accept_chat()`: unblock the chat or accept contact request
|
||||
- `dc_block_chat()`: block the chat, currently works only for mailing
|
||||
lists.
|
||||
|
||||
Removed API:
|
||||
- `dc_create_chat_by_msg_id()`: deprecated 2021-02-07 in favor of
|
||||
`dc_decide_on_contact_request()`
|
||||
- `dc_marknoticed_contact()`: deprecated 2021-02-07 in favor of
|
||||
`dc_decide_on_contact_request()`
|
||||
- `dc_decide_on_contact_request()`: this call requires a message ID
|
||||
from deaddrop chat as input. As deaddrop chat is removed, this
|
||||
call can't be used anymore.
|
||||
- `dc_msg_get_real_chat_id()`: use `dc_msg_get_chat_id()` instead, the
|
||||
only difference between these calls was in handling of deaddrop
|
||||
chat
|
||||
- removed `DC_CHAT_ID_DEADDROP` and `DC_STR_DEADDROP` constants
|
||||
|
||||
- breaking change: removed `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE`
|
||||
Instead, there is a new api `dc_get_connectivity()`
|
||||
and `dc_get_connectivity_html()`;
|
||||
`DC_EVENT_CONNECTIVITY_CHANGED` is emitted on changes
|
||||
|
||||
- breaking change: removed `dc_accounts_import_account()`
|
||||
Instead you need to add an account and call `dc_imex(DC_IMEX_IMPORT_BACKUP)`
|
||||
on its context
|
||||
|
||||
- update account api, 2 new methods:
|
||||
`int dc_all_work_done (dc_context_t* context);`
|
||||
`int dc_accounts_all_work_done (dc_accounts_t* accounts);`
|
||||
|
||||
- add api to check if a message was `Auto-Submitted`
|
||||
cffi: `int dc_msg_is_bot (const dc_msg_t* msg);`
|
||||
python: `Message.is_bot()`
|
||||
|
||||
- `dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);`
|
||||
now returns `NULL` if there is no selected account
|
||||
|
||||
- added `dc_accounts_maybe_network_lost()` for systems core cannot find out
|
||||
connectivity loss on its own (eg. iOS) #2550
|
||||
|
||||
### Added
|
||||
- use Auto-Submitted: auto-generated header to identify bots #2502
|
||||
- allow sending stickers via repl tool
|
||||
- chat: make `get_msg_cnt()` and `get_fresh_msg_cnt()` work for deaddrop chat #2493
|
||||
- withdraw/revive own qr-codes #2512
|
||||
- add Connectivity view (a better api for getting the connection status) #2319 #2549 #2542
|
||||
|
||||
### Changes
|
||||
- updated spec: new `Chat-User-Avatar` usage, `Chat-Content: sticker`, structure, copyright year #2480
|
||||
- update documentation #2548 #2561 #2569
|
||||
- breaking: `Accounts::create` does not also create an default account anymore #2500
|
||||
- remove "forwarded" from stickers, as the primary way of getting stickers
|
||||
is by asking a bot and then forwarding them currently #2526
|
||||
- mimeparser: use mailparse to parse RFC 2231 filenames #2543
|
||||
- allow email addresses without dot in the domain part #2112
|
||||
- allow installing lib and include under different prefixes #2558
|
||||
- remove counter from name provided by `DC_CHAT_ID_ARCHIVED_LINK` #2566
|
||||
- improve tests #2487 #2491 #2497
|
||||
- refactorings #2492 #2503 #2504 #2506 #2515 #2520 #2567 #2575 #2577 #2579
|
||||
- improve ci #2494
|
||||
- update provider-database #2565
|
||||
|
||||
### Removed
|
||||
- remove `dc_accounts_import_account()` api #2521
|
||||
- remove `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE` #2319
|
||||
|
||||
### Fixes
|
||||
- allow stickers with gif-images #2481
|
||||
- fix database migration #2486
|
||||
- do not count hidden messages in get_msg_cnt(). #2493
|
||||
- improve drafts detection #2489
|
||||
- fix panic when removing last, selected account from account manager #2500
|
||||
- set_draft's message-changed-event returns now draft's msg id instead of 0 #2304
|
||||
- avoid hiding outgoing classic emails #2505
|
||||
- fixes for message timestamps #2517
|
||||
- do not process names, avatars, location XMLs, message signature etc.
|
||||
for duplicate messages #2513
|
||||
- fix `can_send` for users not in group #2479
|
||||
- fix receiving events for accounts added by `dc_accounts_add_account()` #2559
|
||||
- fix which chats messages are assigned to #2465
|
||||
- fix: don't create chats when MDNs are received #2578
|
||||
|
||||
|
||||
## 1.56.0
|
||||
|
||||
- fix downscaling images #2469
|
||||
|
||||
- fix outgoing messages popping up in selfchat #2456
|
||||
|
||||
- securejoin: display error reason if there is any #2470
|
||||
|
||||
- do not allow deleting contacts with ongoing chats #2458
|
||||
|
||||
- fix: ignore drafts folder when scanning #2454
|
||||
|
||||
- fix: scan folders also when inbox is not watched #2446
|
||||
|
||||
- more robust In-Reply-To parsing #2182
|
||||
|
||||
- update dependencies #2441 #2438 #2439 #2440 #2447 #2448 #2449 #2452 #2453 #2460 #2464 #2466
|
||||
|
||||
- update provider-database #2471
|
||||
|
||||
- refactorings #2459 #2457
|
||||
|
||||
- improve tests and ci #2445 #2450 #2451
|
||||
|
||||
|
||||
## 1.55.0
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat LANGUAGES C)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
@@ -8,7 +9,11 @@ add_custom_command(
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.so"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --release --no-default-features
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --release --no-default-features
|
||||
|
||||
# Build in `deltachat-ffi` directory instead of using
|
||||
# `--package deltachat_ffi` to avoid feature resolver version
|
||||
@@ -31,7 +36,6 @@ add_custom_target(
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
1480
Cargo.lock
generated
1480
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
115
Cargo.toml
115
Cargo.toml
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.55.0"
|
||||
version = "1.70.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -15,76 +16,76 @@ lto = true
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1.0.40"
|
||||
async-imap = "0.5.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
async-std-resolver = "0.20.3"
|
||||
async-std = { version = "~1.9.0", features = ["unstable"] }
|
||||
async-tar = "0.3.0"
|
||||
async-trait = "0.1.50"
|
||||
backtrace = "0.3.59"
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
||||
async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.1.0"
|
||||
byteorder = "1.3.1"
|
||||
charset = "0.1"
|
||||
chrono = "0.4.6"
|
||||
dirs = { version = "3.0.2", optional=true }
|
||||
bitflags = "1.3"
|
||||
byteorder = "1.3"
|
||||
chrono = "0.4"
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
escaper = "0.1.1"
|
||||
futures = "0.3.15"
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.3.0"
|
||||
itertools = "0.10.0"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2.95"
|
||||
libc = "0.2"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
mailparse = "0.13.4"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.13.0"
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
once_cell = "1.4.1"
|
||||
mailparse = "0.13"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.9.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
quick-xml = "0.22.0"
|
||||
r2d2 = "0.8.9"
|
||||
r2d2_sqlite = "0.18.0"
|
||||
rand = "0.7.0"
|
||||
regex = "1.4.6"
|
||||
rusqlite = "0.25"
|
||||
rust-hsluv = "0.1.4"
|
||||
rustyline = { version = "8.2.0", optional = true }
|
||||
sanitize-filename = "0.3.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.19"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.26", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9.6"
|
||||
sha2 = "0.9.5"
|
||||
smallvec = "1.0.0"
|
||||
stop-token = "0.2.0"
|
||||
strum = "0.20.0"
|
||||
strum_macros = "0.20.1"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1.0.25"
|
||||
toml = "0.5.6"
|
||||
url = "2.2.2"
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
url = "2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
zip = "0.5.12"
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "3.3.0"
|
||||
textwrap = "0.14.2"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
|
||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.7.0"
|
||||
log = "0.4.11"
|
||||
pretty_assertions = "0.7.2"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "1.0"
|
||||
tempfile = "3.0"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -116,8 +117,8 @@ name = "search_msgs"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
11
README.md
11
README.md
@@ -3,7 +3,6 @@
|
||||
> Deltachat-core written in Rust
|
||||
|
||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
||||
[](https://circleci.com/gh/deltachat/deltachat-core-rust/)
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
@@ -80,6 +79,16 @@ For more commands type:
|
||||
> help
|
||||
```
|
||||
|
||||
## Installing libdeltachat system wide
|
||||
|
||||
```
|
||||
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
$ cd deltachat-core-rust
|
||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
||||
$ cmake --build build
|
||||
$ sudo cmake --install build
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
|
||||
BIN
assets/icon-broadcast.png
Normal file
BIN
assets/icon-broadcast.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
149
assets/icon-broadcast.svg
Normal file
149
assets/icon-broadcast.svg
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new 0 0 128 128"
|
||||
viewBox="0 0 60 60"
|
||||
version="1.1"
|
||||
id="svg878"
|
||||
sodipodi:docname="icon-broadcast.svg"
|
||||
width="60"
|
||||
height="60"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-broadcast.png"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001">
|
||||
<metadata
|
||||
id="metadata884">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs882" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1329"
|
||||
inkscape:window-height="847"
|
||||
id="namedview880"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.21875"
|
||||
inkscape:cx="36.598802"
|
||||
inkscape:cy="32.191617"
|
||||
inkscape:window-x="111"
|
||||
inkscape:window-y="205"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg878"
|
||||
inkscape:document-rotation="0" />
|
||||
<radialGradient
|
||||
id="c"
|
||||
cx="65.25"
|
||||
cy="89"
|
||||
r="26.440001"
|
||||
gradientTransform="matrix(0.77611266,0.11996647,-0.18999676,1.2286617,-11.305867,-60.065999)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#FFC107"
|
||||
offset="0"
|
||||
id="stop833" />
|
||||
<stop
|
||||
stop-color="#FFBD06"
|
||||
offset=".3502"
|
||||
id="stop835" />
|
||||
<stop
|
||||
stop-color="#FFB104"
|
||||
offset=".6938"
|
||||
id="stop837" />
|
||||
<stop
|
||||
stop-color="#FFA000"
|
||||
offset="1"
|
||||
id="stop839" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="b"
|
||||
cx="52.5"
|
||||
cy="19.75"
|
||||
r="92.975998"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45.323856,68.997115,75.979538)">
|
||||
<stop
|
||||
stop-color="#EF5350"
|
||||
offset="0"
|
||||
id="stop848" />
|
||||
<stop
|
||||
stop-color="#EB4F4C"
|
||||
offset=".246"
|
||||
id="stop850" />
|
||||
<stop
|
||||
stop-color="#E04341"
|
||||
offset=".4878"
|
||||
id="stop852" />
|
||||
<stop
|
||||
stop-color="#CD302F"
|
||||
offset=".7272"
|
||||
id="stop854" />
|
||||
<stop
|
||||
stop-color="#C62828"
|
||||
offset=".8004"
|
||||
id="stop856" />
|
||||
<stop
|
||||
stop-color="#C62828"
|
||||
offset="1"
|
||||
id="stop858" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="a"
|
||||
cx="16.979"
|
||||
cy="92"
|
||||
r="24.165001"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45.323856,68.997115,75.979538)"
|
||||
xlink:href="#b">
|
||||
<stop
|
||||
stop-color="#E0E0E0"
|
||||
offset="0"
|
||||
id="stop863" />
|
||||
<stop
|
||||
stop-color="#CFCFCF"
|
||||
offset=".3112"
|
||||
id="stop865" />
|
||||
<stop
|
||||
stop-color="#A4A4A4"
|
||||
offset=".9228"
|
||||
id="stop867" />
|
||||
<stop
|
||||
stop-color="#9E9E9E"
|
||||
offset="1"
|
||||
id="stop869" />
|
||||
</radialGradient>
|
||||
<rect
|
||||
y="0"
|
||||
x="0"
|
||||
height="60"
|
||||
width="60"
|
||||
id="rect1420"
|
||||
style="fill:#7cc0bc;fill-opacity:1;stroke:none;stroke-width:1.29077" />
|
||||
<path
|
||||
id="path872"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.336872;stroke-opacity:1"
|
||||
d="m 8.6780027,35.573064 0.032831,-11.910176 c 0.00138,-0.476406 0.4881282,-0.794259 0.9235226,-0.604877 l 4.1144877,2.345752 -0.02386,8.656315 -4.1268029,2.122946 C 9.1617452,36.370003 8.6766889,36.049472 8.6780027,35.573064 Z m 5.0469633,-1.508222 0.02386,-8.656314 31.145424,-9.537653 c 0.841472,-0.219211 1.65915,0.41667 1.656755,1.283728 l -0.06929,25.139995 c -0.0024,0.867062 -0.825942,1.500799 -1.663803,1.274581 z m 3.8042,6.892234 C 16.681121,40.104348 16.315444,38.819414 16.69043,37.591308 l 2.252234,-7.347193 c 0.2644,-0.861571 0.845185,-1.567441 1.641953,-1.989251 0.796769,-0.421808 1.706956,-0.509819 2.568531,-0.245419 l 7.263888,2.225804 c 1.775518,0.543235 2.780299,2.432591 2.232297,4.208094 L 30.3971,41.790532 c -0.545627,1.777887 -2.432591,2.780297 -4.208095,2.232298 l -7.263891,-2.225804 c -0.545033,-0.165864 -1.01825,-0.460162 -1.395948,-0.83995 z m 12.377693,-7.976728 c -0.07601,-0.07642 -0.17114,-0.133864 -0.280621,-0.167516 l -7.263891,-2.225803 c -0.233244,-0.07209 -0.421626,0.0013 -0.512275,0.04861 -0.09064,0.0474 -0.25772,0.166033 -0.327435,0.396899 l -2.252234,7.347191 c -0.108166,0.354628 0.09088,0.731541 0.447888,0.842099 l 7.263891,2.225802 c 0.354626,0.108174 0.731539,-0.09088 0.842099,-0.447888 l 2.249845,-7.344814 c 0.07453,-0.245145 0.0014,-0.504991 -0.167267,-0.67458 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
10
assets/qrcode_logo_footer.svg
Normal file
10
assets/qrcode_logo_footer.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:bold;font-size:24.4118px;line-height:1.25;font-family:sans-serif;fill:#aaaaaa;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
x="42.325161"
|
||||
y="23.32255"
|
||||
id="text72398">get.delta.chat</text>
|
||||
<path
|
||||
id="path84310"
|
||||
style="opacity:0.25;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
d="M 17.13769,0.00129321 C 7.6753075,0.11650893 0,7.8915283 0,17.362467 c 0,9.47094 7.6753075,17.059745 17.13769,16.944599 8.99669,-0.03598 6.880074,-5.025654 16.824785,-0.405885 -5.447648,-8.510047 0.184241,-9.642482 0.311117,-16.955289 0,-9.4709395 -7.673512,-17.0597453 -17.135895,-16.94459879 z M 17.0769,4.9986797 c 1.84214,0 3.447355,0.253959 4.815003,0.7616693 1.381603,0.5076411 2.072253,1.207862 2.072253,2.0990711 0,0.4286855 -0.167495,0.7836052 -0.50242,1.0656242 -0.334921,0.2819844 -0.724544,0.4237724 -1.171121,0.4237724 -0.641952,0 -1.396532,-0.3909376 -2.261778,-1.169353 C 19.14963,7.3898036 18.402555,6.83791 17.788507,6.5220182 17.188416,6.1950547 16.484552,6.0321266 15.675129,6.0321266 c -1.032717,0 -1.883352,0.1854523 -2.553215,0.5578447 -0.655913,0.372254 -0.98517,0.8460916 -0.98517,1.4214436 0,0.5414792 0.272815,1.0495355 0.817093,1.5233385 0.544275,0.4738026 1.946291,1.3367446 4.207097,2.5889976 2.414319,1.342419 4.117377,2.390985 5.108232,3.146807 1.004795,0.755857 1.821505,1.675853 2.449514,2.758846 0.628002,1.082993 0.942253,2.227607 0.942253,3.434674 0,2.120834 -0.929555,3.993314 -2.785656,5.617786 -1.84214,1.613228 -3.99694,2.41915 -6.467082,2.41915 -2.246845,0 -4.145607,-0.647976 -5.694677,-1.945312 -1.5490699,-1.297336 -2.3225722,-3.028063 -2.3225722,-5.194049 0,-2.087031 0.8506345,-3.83094 2.5532182,-5.229825 1.716541,-1.398884 3.824203,-2.245599 6.322256,-2.538897 -0.697774,-0.631749 -1.668763,-1.387225 -2.910816,-2.267155 -1.367648,-0.970199 -2.287914,-1.73045 -2.762402,-2.283243 -0.474491,-0.5640381 -0.711618,-1.1795944 -0.711618,-1.8451814 0,-0.9927581 0.572093,-1.7710351 1.716451,-2.3351077 1.144362,-0.5753173 2.636724,-0.8635642 4.478865,-0.8635642 z m 1.110327,10.3738083 c -4.005262,0.5302 -6.007576,2.75279 -6.007576,6.667322 0,2.01932 0.49495,3.587291 1.485805,4.704157 1.004806,1.116832 2.169696,1.675299 3.495479,1.675299 1.381602,0 2.520072,-0.535632 3.413229,-1.60738 0.893168,-1.082959 1.339187,-2.545264 1.339187,-4.384079 0,-2.662348 -1.242022,-5.013441 -3.726124,-7.055319 z" />
|
||||
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
Binary file not shown.
@@ -8,9 +8,7 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::path::Path;
|
||||
async fn search_benchmark(path: impl AsRef<Path>) {
|
||||
let dbfile = path.as_ref();
|
||||
let id = 100;
|
||||
let context = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
for _ in 0..10u32 {
|
||||
context.search_msgs(None, "hello").await.unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.55.0"
|
||||
version = "1.70.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -17,13 +17,13 @@ crate-type = ["cdylib", "staticlib"]
|
||||
[dependencies]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
human-panic = "1"
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
async-std = "1.9.0"
|
||||
anyhow = "1.0.40"
|
||||
thiserror = "1.0.25"
|
||||
rand = "0.7.3"
|
||||
async-std = "1"
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -583,7 +583,7 @@ SORT_MEMBERS_CTORS_1ST = NO
|
||||
# appear in their defined order.
|
||||
# The default value is: NO.
|
||||
|
||||
SORT_GROUP_NAMES = NO
|
||||
SORT_GROUP_NAMES = YES
|
||||
|
||||
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
|
||||
# fully-qualified names, including namespaces. If set to NO, the class list will
|
||||
|
||||
@@ -4,4 +4,16 @@ div.fragment {
|
||||
background-color: #e0e0e0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #e0e0e0;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
@@ -23,11 +23,13 @@ fn main() {
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
|
||||
.unwrap()
|
||||
.write_all(&pkg_config.as_bytes())
|
||||
.write_all(pkg_config.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
prefix={prefix}
|
||||
libdir=${{prefix}}/lib
|
||||
includedir=${{prefix}}/include
|
||||
libdir={libdir}
|
||||
includedir={includedir}
|
||||
|
||||
Name: {name}
|
||||
Description: {description}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
245
deltachat-ffi/src/lot.rs
Normal file
245
deltachat-ffi/src/lot.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
//! # Legacy generic return values for C API.
|
||||
|
||||
use crate::message::MessageState;
|
||||
use crate::qr::Qr;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
use anyhow::Error;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
/// Lot objects are created
|
||||
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
|
||||
///
|
||||
/// *Lot* is used in the meaning *heap* here.
|
||||
#[derive(Debug)]
|
||||
pub enum Lot {
|
||||
Summary(Summary),
|
||||
Qr(Qr),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
Text1Username = 2,
|
||||
Text1Self = 3,
|
||||
}
|
||||
|
||||
impl Default for Meaning {
|
||||
fn default() -> Self {
|
||||
Meaning::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
Some(SummaryPrefix::Draft(text)) => Some(text),
|
||||
Some(SummaryPrefix::Username(username)) => Some(username),
|
||||
Some(SummaryPrefix::Me(text)) => Some(text),
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => None,
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { .. } => None,
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(_) => None,
|
||||
Self::Error(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text1_meaning(&self) -> Meaning {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => Meaning::None,
|
||||
Some(SummaryPrefix::Draft(_text)) => Meaning::Text1Draft,
|
||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
||||
},
|
||||
Self::Qr(_qr) => Meaning::None,
|
||||
Self::Error(_err) => Meaning::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> LotState {
|
||||
match self {
|
||||
Self::Summary(summary) => summary.state.into(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> u32 {
|
||||
match self {
|
||||
Self::Summary(_) => Default::default(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => *contact_id,
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id } => *contact_id,
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_timestamp(&self) -> i64 {
|
||||
match self {
|
||||
Self::Summary(summary) => summary.timestamp,
|
||||
Self::Qr(_) => Default::default(),
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
|
||||
// Qr States
|
||||
/// id=contact
|
||||
QrAskVerifyContact = 200,
|
||||
|
||||
/// text1=groupname
|
||||
QrAskVerifyGroup = 202,
|
||||
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// text1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
/// text1=text
|
||||
QrText = 330,
|
||||
|
||||
/// text1=URL
|
||||
QrUrl = 332,
|
||||
|
||||
/// text1=error string
|
||||
QrError = 400,
|
||||
|
||||
QrWithdrawVerifyContact = 500,
|
||||
|
||||
/// text1=groupname
|
||||
QrWithdrawVerifyGroup = 502,
|
||||
|
||||
QrReviveVerifyContact = 510,
|
||||
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
|
||||
// Message States
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
MsgInSeen = 16,
|
||||
MsgOutPreparing = 18,
|
||||
MsgOutDraft = 19,
|
||||
MsgOutPending = 20,
|
||||
MsgOutFailed = 24,
|
||||
MsgOutDelivered = 26,
|
||||
MsgOutMdnRcvd = 28,
|
||||
}
|
||||
|
||||
impl Default for LotState {
|
||||
fn default() -> Self {
|
||||
LotState::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageState> for LotState {
|
||||
fn from(s: MessageState) -> Self {
|
||||
use MessageState::*;
|
||||
match s {
|
||||
Undefined => LotState::Undefined,
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
OutDelivered => LotState::MsgOutDelivered,
|
||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Summary> for Lot {
|
||||
fn from(summary: Summary) -> Self {
|
||||
Lot::Summary(summary)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qr> for Lot {
|
||||
fn from(qr: Qr) -> Self {
|
||||
Lot::Qr(qr)
|
||||
}
|
||||
}
|
||||
|
||||
// Make it easy to convert errors into the final `Lot`.
|
||||
impl From<Error> for Lot {
|
||||
fn from(error: Error) -> Self {
|
||||
Lot::Error(error.to_string())
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,12 @@ use std::ptr;
|
||||
/// }
|
||||
/// ```
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
let ret: *mut libc::c_char;
|
||||
if !s.is_null() {
|
||||
ret = libc::strdup(s);
|
||||
assert!(!ret.is_null());
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
ret = libc::calloc(1, 1) as *mut libc::c_char;
|
||||
assert!(!ret.is_null());
|
||||
}
|
||||
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -170,15 +167,20 @@ pub(crate) trait Strdup {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> Strdup for T {
|
||||
impl Strdup for str {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self.as_ref());
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
// We can not implement for AsRef<OsStr> because we already implement
|
||||
// AsRev<str> and this conflicts. So implement for Path directly.
|
||||
impl Strdup for String {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let s: &str = self;
|
||||
s.strdup()
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for std::path::Path {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
@@ -186,6 +188,13 @@ impl Strdup for std::path::Path {
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for [u8] {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods to turn optional strings into C strings.
|
||||
///
|
||||
/// This is the same as the [Strdup] trait but a different trait name
|
||||
|
||||
@@ -9,5 +9,5 @@ license = "MPL-2.0"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.72"
|
||||
quote = "1.0.2"
|
||||
syn = "1"
|
||||
quote = "1"
|
||||
|
||||
33
draft/aeap-mvp.rst
Normal file
33
draft/aeap-mvp.rst
Normal file
@@ -0,0 +1,33 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one
|
||||
|
||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
||||
|
||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
||||
|
||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
@@ -2,7 +2,7 @@ extern crate dirs;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure, Error};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||
@@ -13,13 +13,11 @@ use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::error::Error;
|
||||
use deltachat::export_chat::export_chat_to_zip;
|
||||
use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::lot::LotState;
|
||||
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
@@ -100,10 +98,10 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
});
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -191,10 +189,18 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
MessageState::OutFailed => " !!",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let downloadstate = match msg.download_state() {
|
||||
DownloadState::Done => "",
|
||||
DownloadState::Available => " [⬇ Download available]",
|
||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
};
|
||||
|
||||
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
|
||||
prefix.as_ref(),
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
@@ -228,11 +234,12 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
""
|
||||
},
|
||||
statestr,
|
||||
downloadstate,
|
||||
&temp2,
|
||||
);
|
||||
}
|
||||
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
let mut lines_out = 0;
|
||||
for &msg_id in msglist {
|
||||
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||
@@ -260,59 +267,59 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
|
||||
for contact_id in contacts {
|
||||
let line;
|
||||
let mut line2 = "".to_string();
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
let verified_state = contact.is_verified(context).await;
|
||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
||||
if verified_state == VerifiedStatus::BidirectVerified {
|
||||
" √√"
|
||||
} else {
|
||||
" √"
|
||||
}
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
let verified_state = contact.is_verified(context).await?;
|
||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
||||
if verified_state == VerifiedStatus::BidirectVerified {
|
||||
" √√"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
line = format!(
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
&name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() {
|
||||
&addr
|
||||
} else {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
);
|
||||
" √"
|
||||
}
|
||||
|
||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||
} else {
|
||||
""
|
||||
};
|
||||
line = format!(
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
&name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() {
|
||||
&addr
|
||||
} else {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
);
|
||||
}
|
||||
|
||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn chat_prefix(chat: &Chat) -> &'static str {
|
||||
chat.typ.into()
|
||||
}
|
||||
|
||||
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
|
||||
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<()> {
|
||||
let mut sel_chat = if !chat_id.is_unset() {
|
||||
Chat::load_from_db(&context, *chat_id).await.ok()
|
||||
Some(Chat::load_from_db(&context, *chat_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -353,6 +360,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
connectivity\n\
|
||||
maybenetwork\n\
|
||||
housekeeping\n\
|
||||
help imex (Import/Export)\n\
|
||||
@@ -362,6 +370,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat [<chat-id>|0]\n\
|
||||
createchat <contact-id>\n\
|
||||
creategroup <name>\n\
|
||||
createbroadcast\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
@@ -374,8 +383,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendsticker <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
@@ -389,14 +400,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
protect <chat-id>\n\
|
||||
unprotect <chat-id>\n\
|
||||
delchat <chat-id>\n\
|
||||
export-chat <chat-id> <destination-file>\n\
|
||||
===========================Contact requests==\n\
|
||||
decidestartchat <msg-id>\n\
|
||||
decideblock <msg-id>\n\
|
||||
decidenotnow <msg-id>\n\
|
||||
accept <chat-id>\n\
|
||||
decline <chat-id>\n\
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
download <msg-id>\n\
|
||||
html <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
@@ -414,8 +423,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
listblocked\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getqrsvg [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
@@ -453,7 +464,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <setup-code> expected"
|
||||
);
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
@@ -500,19 +511,33 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"set" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let key = config::Config::from_str(arg1)?;
|
||||
let value = if arg2.is_empty() { None } else { Some(arg2) };
|
||||
context.set_config(key, value).await?;
|
||||
}
|
||||
"get" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let key = config::Config::from_str(arg1)?;
|
||||
let val = context.get_config(key).await;
|
||||
println!("{}={:?}", key, val);
|
||||
}
|
||||
"info" => {
|
||||
println!("{:#?}", context.get_info().await);
|
||||
}
|
||||
"connectivity" => {
|
||||
let file = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("connectivity.html");
|
||||
match context.get_connectivity_html().await {
|
||||
Ok(html) => {
|
||||
fs::write(&file, html)?;
|
||||
println!("Report written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get connectivity html: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
"maybenetwork" => {
|
||||
context.maybe_network().await;
|
||||
}
|
||||
@@ -540,7 +565,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}{}{}",
|
||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
@@ -552,27 +577,31 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
if chat.is_protected() { "🛡️" } else { "" },
|
||||
if chat.is_contact_request() {
|
||||
"🆕"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
|
||||
let summary = chatlist.get_summary(&context, i, Some(&chat)).await?;
|
||||
let statestr = if chat.visibility == ChatVisibility::Archived {
|
||||
" [Archived]"
|
||||
} else {
|
||||
match lot.get_state() {
|
||||
LotState::MsgOutPending => " o",
|
||||
LotState::MsgOutDelivered => " √",
|
||||
LotState::MsgOutMdnRcvd => " √√",
|
||||
LotState::MsgOutFailed => " !!",
|
||||
match summary.state {
|
||||
MessageState::OutPending => " o",
|
||||
MessageState::OutDelivered => " √",
|
||||
MessageState::OutMdnRcvd => " √√",
|
||||
MessageState::OutFailed => " !!",
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
let timestr = dc_timestamp_to_str(lot.get_timestamp());
|
||||
let text1 = lot.get_text1();
|
||||
let text2 = lot.get_text2();
|
||||
let timestr = dc_timestamp_to_str(summary.timestamp);
|
||||
println!(
|
||||
"{}{}{}{} [{}]{}",
|
||||
text1.unwrap_or(""),
|
||||
if text1.is_some() { ": " } else { "" },
|
||||
text2.unwrap_or(""),
|
||||
"{}{}{} [{}]{}",
|
||||
summary
|
||||
.prefix
|
||||
.map_or_else(String::new, |prefix| format!("{}: ", prefix)),
|
||||
summary.text,
|
||||
statestr,
|
||||
×tr,
|
||||
if chat.is_sending_locations() {
|
||||
@@ -586,7 +615,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending_locations_to_chat(&context, None).await {
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{} chats", cnt);
|
||||
@@ -682,35 +711,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
}
|
||||
"decidestartchat" | "createchatbymsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
match message::decide_on_contact_request(
|
||||
&context,
|
||||
msg_id,
|
||||
ContactRequestDecision::StartChat,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(chat_id) => {
|
||||
let chat = Chat::load_from_db(&context, chat_id).await?;
|
||||
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id);
|
||||
}
|
||||
None => println!("Cannot crate chat."),
|
||||
}
|
||||
}
|
||||
"decidenotnow" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::NotNow)
|
||||
.await;
|
||||
}
|
||||
"decideblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::Block)
|
||||
.await;
|
||||
}
|
||||
"creategroup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
@@ -718,6 +718,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!("Group#{} created successfully.", chat_id);
|
||||
}
|
||||
"createbroadcast" => {
|
||||
let chat_id = chat::create_broadcast_list(&context).await?;
|
||||
|
||||
println!("Broadcast#{} created successfully.", chat_id);
|
||||
}
|
||||
"createprotected" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
@@ -730,17 +735,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id_0: u32 = arg1.parse()?;
|
||||
if chat::add_contact_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Contact added to chat.");
|
||||
} else {
|
||||
bail!("Cannot add contact to chat.");
|
||||
}
|
||||
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
|
||||
.await?;
|
||||
println!("Contact added to chat.");
|
||||
}
|
||||
"removemember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -758,7 +755,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"groupname" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
chat::set_chat_name(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
&format!("{} {}", arg1, arg2).trim(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
@@ -778,7 +780,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!(
|
||||
"{} contacts\nLocation streaming: {}",
|
||||
contacts.len(),
|
||||
@@ -786,7 +788,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await,
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -831,7 +833,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -868,12 +870,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||
}
|
||||
"sendimage" | "sendfile" => {
|
||||
"sendimage" | "sendsticker" | "sendfile" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No file given.");
|
||||
|
||||
let mut msg = Message::new(if arg0 == "sendimage" {
|
||||
Viewtype::Image
|
||||
} else if arg0 == "sendsticker" {
|
||||
Viewtype::Sticker
|
||||
} else {
|
||||
Viewtype::File
|
||||
});
|
||||
@@ -899,6 +903,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}));
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||
None => println!("sync message not needed."),
|
||||
},
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
@@ -906,12 +914,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
let chat = if let Some(ref sel_chat) = sel_chat {
|
||||
Some(sel_chat.get_id())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist = context.search_msgs(chat, arg1).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
@@ -1025,12 +1028,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id.delete(&context).await?;
|
||||
}
|
||||
"export-chat" => {
|
||||
"accept" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
ensure!(!arg2.is_empty(), "Argument <destination file> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
// todo check if path is valid (dest dir exists) and ends in .zip
|
||||
export_chat_to_zip(&context, chat_id, arg2).await;
|
||||
chat_id.accept(&context).await?;
|
||||
}
|
||||
"blockchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id.block(&context).await?;
|
||||
}
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
@@ -1038,6 +1044,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
println!("{}", res);
|
||||
}
|
||||
"download" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
println!("Scheduling download for {:?}", id);
|
||||
id.download_full(&context).await?;
|
||||
}
|
||||
"html" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
@@ -1075,7 +1087,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut ids = [MsgId::new(0); 1];
|
||||
ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::delete_msgs(&context, &ids).await;
|
||||
message::delete_msgs(&context, &ids).await?;
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
@@ -1088,7 +1100,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Some(arg1),
|
||||
)
|
||||
.await?;
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts.", contacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
@@ -1144,28 +1156,22 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"block" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
Contact::block(&context, contact_id).await;
|
||||
Contact::block(&context, contact_id).await?;
|
||||
}
|
||||
"unblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
Contact::unblock(&context, contact_id).await;
|
||||
Contact::unblock(&context, contact_id).await?;
|
||||
}
|
||||
"listblocked" => {
|
||||
let contacts = Contact::get_all_blocked(&context).await?;
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} blocked contacts.", contacts.len());
|
||||
}
|
||||
"checkqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let res = check_qr(&context, arg1).await;
|
||||
println!(
|
||||
"state={}, id={}, text1={:?}, text2={:?}",
|
||||
res.get_state(),
|
||||
res.get_id(),
|
||||
res.get_text1(),
|
||||
res.get_text2()
|
||||
);
|
||||
let qr = check_qr(&context, arg1).await?;
|
||||
println!("qr={:?}", qr);
|
||||
}
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
@@ -1176,7 +1182,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
match provider::get_provider_info(arg1).await {
|
||||
let socks5_enabled = context
|
||||
.get_config_bool(config::Config::Socks5Enabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {}:", arg1);
|
||||
println!("status: {}", info.status as u32);
|
||||
|
||||
@@ -33,6 +33,8 @@ use rustyline::{
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use std::fs;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: EventType) {
|
||||
@@ -57,9 +59,6 @@ fn receive_event(event: EventType) {
|
||||
EventType::Error(msg) => {
|
||||
error!("{}", msg);
|
||||
}
|
||||
EventType::ErrorNetwork(msg) => {
|
||||
error!("[NETWORK] msg={}", msg);
|
||||
}
|
||||
EventType::ErrorSelfNotInGroup(msg) => {
|
||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||
}
|
||||
@@ -157,7 +156,7 @@ const IMEX_COMMANDS: [&str; 12] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 9] = [
|
||||
const DB_COMMANDS: [&str; 10] = [
|
||||
"info",
|
||||
"set",
|
||||
"get",
|
||||
@@ -165,6 +164,7 @@ const DB_COMMANDS: [&str; 9] = [
|
||||
"configure",
|
||||
"connect",
|
||||
"disconnect",
|
||||
"connectivity",
|
||||
"maybenetwork",
|
||||
"housekeeping",
|
||||
];
|
||||
@@ -174,11 +174,9 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"listarchived",
|
||||
"chat",
|
||||
"createchat",
|
||||
"decidestartchat",
|
||||
"decideblock",
|
||||
"decidenotnow",
|
||||
"creategroup",
|
||||
"createverified",
|
||||
"createbroadcast",
|
||||
"createprotected",
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
@@ -192,6 +190,7 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -204,15 +203,17 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"protect",
|
||||
"unprotect",
|
||||
"delchat",
|
||||
"export-chat",
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 6] = [
|
||||
const MESSAGE_COMMANDS: [&str; 7] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
@@ -225,10 +226,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
@@ -294,7 +297,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
@@ -329,7 +332,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
|
||||
loop {
|
||||
let p = "> ";
|
||||
let readline = rl.readline(&p);
|
||||
let readline = rl.readline(p);
|
||||
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
@@ -396,7 +399,7 @@ async fn handle_cmd(
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
} else {
|
||||
@@ -413,18 +416,31 @@ async fn handle_cmd(
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
if let Some(mut qr) = dc_get_securejoin_qr(&ctx, group).await {
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg)?;
|
||||
println!("QR code svg written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ fn cb(event: EventType) {
|
||||
EventType::Warning(msg) => {
|
||||
log::warn!("{}", msg);
|
||||
}
|
||||
EventType::Error(msg) | EventType::ErrorNetwork(msg) => {
|
||||
EventType::Error(msg) => {
|
||||
log::error!("{}", msg);
|
||||
}
|
||||
event => {
|
||||
@@ -36,7 +36,7 @@ async fn main() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), 0)
|
||||
let ctx = Context::new(dbfile.into(), 0)
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
@@ -86,7 +86,7 @@ async fn main() {
|
||||
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
||||
|
||||
for i in 0..chats.len() {
|
||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
|
||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
log::info!("[{}] msg: {:?}", i, msg);
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
|
||||
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
|
||||
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
|
||||
cc 9796807baeda701227dcdcfc9fdaa93ddd556da2bb1630381bfe2e037bee73f6 # shrinks to buf = " ꫛ®a\u{11300}a", approx_chars = 0
|
||||
cc 063a4c42ac1ec9aa37af54521b210ba9cd82dcc9cc3be296ca2fedf8240072d4 # shrinks to buf = "a᪠ 0A", approx_chars = 0
|
||||
|
||||
@@ -58,12 +58,13 @@ end-to-end tests that require accounts on real e-mail servers.
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
|
||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
|
||||
|
||||
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
|
||||
These accounts exists for one 1hour and then are removed completely.
|
||||
export DCC_NEW_TMP_EMAIL=<URL you got from us>
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests:
|
||||
|
||||
pytest
|
||||
|
||||
19
python/mypy.ini
Normal file
19
python/mypy.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-deltachat.capi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pluggy.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-cffi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-imapclient.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-_pytest.*]
|
||||
ignore_missing_imports = True
|
||||
8
python/pyproject.toml
Normal file
8
python/pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
@@ -8,17 +8,11 @@ def main():
|
||||
long_description = f.read()
|
||||
setuptools.setup(
|
||||
name='deltachat',
|
||||
setup_requires=['setuptools_scm', 'cffi>=1.0.0'],
|
||||
use_scm_version = {
|
||||
"root": "..",
|
||||
"relative_to": __file__,
|
||||
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
|
||||
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
|
||||
},
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -19,9 +19,9 @@ except DistributionNotFound:
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name, val in vars(const).items():
|
||||
for name in dir(const):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[val] = name
|
||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
configtracker = ac.configure()
|
||||
|
||||
@@ -150,6 +150,7 @@ def extract_defines(flags):
|
||||
| DC_PROVIDER
|
||||
| DC_KEY_GEN
|
||||
| DC_IMEX
|
||||
| DC_CONNECTIVITY
|
||||
) # End of prefix matching
|
||||
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
||||
) # Close the capturing group, this contains
|
||||
|
||||
@@ -8,13 +8,14 @@ import os
|
||||
from array import array
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker, ConfigureTracker
|
||||
from . import hookspec
|
||||
from .events import EventThread
|
||||
from typing import Union, Any, Dict, Optional, List, Generator
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
@@ -28,7 +29,7 @@ class Account(object):
|
||||
"""
|
||||
MissingCredentials = MissingCredentials
|
||||
|
||||
def __init__(self, db_path, os_name=None, logging=True):
|
||||
def __init__(self, db_path, os_name=None, logging=True) -> None:
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
@@ -58,11 +59,11 @@ class Account(object):
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def disable_logging(self):
|
||||
def disable_logging(self) -> None:
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self):
|
||||
def enable_logging(self) -> None:
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
@@ -73,12 +74,12 @@ class Account(object):
|
||||
if self._logging:
|
||||
self._pm.hook.ac_log_line(message=msg)
|
||||
|
||||
def _check_config_key(self, name):
|
||||
def _check_config_key(self, name: str) -> None:
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
name, self._configkeys))
|
||||
|
||||
def get_info(self):
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
@@ -105,19 +106,19 @@ class Account(object):
|
||||
cursor += len(entry) + 1
|
||||
log("")
|
||||
|
||||
def set_stock_translation(self, id, string):
|
||||
def set_stock_translation(self, id: int, string: str) -> None:
|
||||
""" set stock translation string.
|
||||
|
||||
:param id: id of stock string (const.DC_STR_*)
|
||||
:param value: string to set as new transalation
|
||||
:returns: None
|
||||
"""
|
||||
string = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, string)
|
||||
bytestring = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
|
||||
if res == 0:
|
||||
raise ValueError("could not set translation string")
|
||||
|
||||
def set_config(self, name, value):
|
||||
def set_config(self, name: str, value: Optional[str]) -> None:
|
||||
""" set configuration values.
|
||||
|
||||
:param name: config key name (unicode)
|
||||
@@ -125,16 +126,16 @@ class Account(object):
|
||||
:returns: None
|
||||
"""
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
if name == b"addr" and self.is_configured():
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if value is not None:
|
||||
value = value.encode("utf8")
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
value = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, name, value)
|
||||
valuebytes = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name):
|
||||
def get_config(self, name: str) -> str:
|
||||
""" return unicode string value.
|
||||
|
||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||
@@ -143,12 +144,12 @@ class Account(object):
|
||||
"""
|
||||
if name != "sys.config_keys":
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, name)
|
||||
namebytes = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, namebytes)
|
||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr, public, secret):
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -160,7 +161,7 @@ class Account(object):
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def update_config(self, kwargs):
|
||||
def update_config(self, kwargs: Dict[str, Any]) -> None:
|
||||
""" update config values.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
@@ -170,7 +171,7 @@ class Account(object):
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
|
||||
def is_configured(self):
|
||||
def is_configured(self) -> bool:
|
||||
""" determine if the account is configured already; an initial connection
|
||||
to SMTP/IMAP has been verified.
|
||||
|
||||
@@ -178,7 +179,7 @@ class Account(object):
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path):
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
:raises ValueError: if profile image could not be set
|
||||
@@ -190,20 +191,18 @@ class Account(object):
|
||||
assert os.path.exists(img_path), img_path
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
def check_is_configured(self):
|
||||
def check_is_configured(self) -> None:
|
||||
""" Raise ValueError if this account is not configured. """
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
def get_latest_backupfile(self, backupdir) -> Optional[str]:
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
if res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(res)
|
||||
return from_optional_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self):
|
||||
def get_blobdir(self) -> str:
|
||||
""" return the directory for files.
|
||||
|
||||
All sent files are copied to this directory if necessary.
|
||||
@@ -211,15 +210,15 @@ class Account(object):
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||
|
||||
def get_self_contact(self):
|
||||
def get_self_contact(self) -> Contact:
|
||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
|
||||
"""create a (new) Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
@@ -236,13 +235,13 @@ class Account(object):
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj):
|
||||
def get_contact(self, obj) -> Optional[Contact]:
|
||||
if isinstance(obj, Contact):
|
||||
return obj
|
||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
||||
return self.get_contact_by_addr(addr)
|
||||
|
||||
def get_contact_addr_and_name(self, obj, name=None):
|
||||
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("can only add addresses from configured accounts")
|
||||
@@ -260,7 +259,7 @@ class Account(object):
|
||||
name = displayname
|
||||
return (name, addr)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
def delete_contact(self, contact: Contact) -> bool:
|
||||
""" delete a Contact.
|
||||
|
||||
:param contact: contact object obtained
|
||||
@@ -271,22 +270,23 @@ class Account(object):
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email):
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
if contact_id:
|
||||
return self.get_contact_by_id(contact_id)
|
||||
return None
|
||||
|
||||
def get_contact_by_id(self, contact_id):
|
||||
""" return Contact instance or None.
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
""" return Contact instance or raise an exception.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self):
|
||||
def get_blocked_contacts(self) -> List[Contact]:
|
||||
""" return a list of all blocked contacts.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
@@ -297,8 +297,13 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||
""" get a (filtered) list of contacts.
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
only_verified: bool = False,
|
||||
) -> List[Contact]:
|
||||
"""get a (filtered) list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
@@ -318,7 +323,7 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self):
|
||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
||||
""" yield all fresh messages from all chats. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_fresh_msgs(self._dc_context),
|
||||
@@ -326,15 +331,17 @@ class Account(object):
|
||||
)
|
||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||
|
||||
def create_chat(self, obj):
|
||||
def create_chat(self, obj) -> Chat:
|
||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def _create_chat_by_message_id(self, msg_id):
|
||||
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
|
||||
|
||||
def create_group_chat(self, name, contacts=None, verified=False):
|
||||
""" create a new group chat object.
|
||||
def create_group_chat(
|
||||
self,
|
||||
name: str,
|
||||
contacts: Optional[List[Contact]] = None,
|
||||
verified: bool = False,
|
||||
) -> Chat:
|
||||
"""create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
@@ -350,7 +357,7 @@ class Account(object):
|
||||
chat.add_contact(contact)
|
||||
return chat
|
||||
|
||||
def get_chats(self):
|
||||
def get_chats(self) -> List[Chat]:
|
||||
""" return list of chats.
|
||||
|
||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||
@@ -367,20 +374,17 @@ class Account(object):
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
|
||||
def get_deaddrop_chat(self):
|
||||
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
||||
|
||||
def get_device_chat(self):
|
||||
def get_device_chat(self) -> Chat:
|
||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
||||
|
||||
def get_message_by_id(self, msg_id):
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id):
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
:returns: :class:`deltachat.chat.Chat` instance.
|
||||
@@ -392,19 +396,21 @@ class Account(object):
|
||||
lib.dc_chat_unref(res)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def mark_seen_messages(self, messages):
|
||||
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
|
||||
""" mark the given set of messages as seen.
|
||||
|
||||
:param messages: a list of message ids or Message instances.
|
||||
"""
|
||||
arr = array("i")
|
||||
for msg in messages:
|
||||
msg = getattr(msg, "id", msg)
|
||||
arr.append(msg)
|
||||
if isinstance(msg, Message):
|
||||
arr.append(msg.id)
|
||||
else:
|
||||
arr.append(msg)
|
||||
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||
|
||||
def forward_messages(self, messages, chat):
|
||||
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
|
||||
""" Forward list of messages to a chat.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -414,7 +420,7 @@ class Account(object):
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def delete_messages(self, messages):
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
""" delete messages (local and remote).
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -472,7 +478,7 @@ class Account(object):
|
||||
def imex(self, path, imex_cmd):
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
@@ -483,7 +489,7 @@ class Account(object):
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self):
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -533,7 +539,9 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
def set_location(
|
||||
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
|
||||
) -> None:
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
|
||||
@@ -574,6 +582,15 @@ class Account(object):
|
||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
def get_connectivity(self):
|
||||
return lib.dc_get_connectivity(self._dc_context)
|
||||
|
||||
def get_connectivity_html(self) -> str:
|
||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
||||
|
||||
def all_work_done(self):
|
||||
return lib.dc_all_work_done(self._dc_context)
|
||||
|
||||
def start_io(self):
|
||||
""" start this account's IO scheduling (Rust-core async scheduler)
|
||||
|
||||
@@ -618,7 +635,7 @@ class Account(object):
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure=False):
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
""" Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
@@ -631,11 +648,11 @@ class Account(object):
|
||||
lib.dc_configure(self._dc_context)
|
||||
return configtracker
|
||||
|
||||
def wait_shutdown(self):
|
||||
def wait_shutdown(self) -> None:
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
|
||||
def stop_io(self):
|
||||
def stop_io(self) -> None:
|
||||
""" stop core IO scheduler if it is running. """
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
@@ -643,7 +660,7 @@ class Account(object):
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
if self._dc_context is None:
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import mimetypes
|
||||
import calendar
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .message import Message
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Chat(object):
|
||||
@@ -17,20 +18,20 @@ class Chat(object):
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, id):
|
||||
def __init__(self, account, id) -> None:
|
||||
from .account import Account
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.id == getattr(other, "id", None) and \
|
||||
self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
|
||||
@property
|
||||
@@ -40,7 +41,7 @@ class Chat(object):
|
||||
lib.dc_chat_unref
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -50,29 +51,37 @@ class Chat(object):
|
||||
"""
|
||||
lib.dc_delete_chat(self.account._dc_context, self.id)
|
||||
|
||||
def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
lib.dc_block_chat(self.account._dc_context, self.id)
|
||||
|
||||
def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
lib.dc_accept_chat(self.account._dc_context, self.id)
|
||||
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self):
|
||||
def is_group(self) -> bool:
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_deaddrop(self):
|
||||
""" return true if this chat is a deaddrop chat.
|
||||
|
||||
:returns: True if chat is the deaddrop chat, False otherwise.
|
||||
"""
|
||||
return self.id == const.DC_CHAT_ID_DEADDROP
|
||||
|
||||
def is_muted(self):
|
||||
def is_muted(self) -> bool:
|
||||
""" return true if this chat is muted.
|
||||
|
||||
:returns: True if chat is muted, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_muted(self._dc_chat)
|
||||
|
||||
def is_contact_request(self):
|
||||
""" return True if this chat is a contact request chat.
|
||||
|
||||
:returns: True if chat is a contact request chat, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_contact_request(self._dc_chat)
|
||||
|
||||
def is_promoted(self):
|
||||
""" return True if this chat is promoted, i.e.
|
||||
the member contacts are aware of their membership,
|
||||
@@ -82,38 +91,38 @@ class Chat(object):
|
||||
"""
|
||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||
|
||||
def can_send(self):
|
||||
def can_send(self) -> bool:
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the deaddrop or for the device-talk
|
||||
This is not true eg. for the contact requests or for the device-talk
|
||||
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
return lib.dc_chat_can_send(self._dc_chat)
|
||||
|
||||
def is_protected(self):
|
||||
def is_protected(self) -> bool:
|
||||
""" return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_protected(self._dc_chat)
|
||||
|
||||
def get_name(self):
|
||||
def get_name(self) -> Optional[str]:
|
||||
""" return name of this chat.
|
||||
|
||||
:returns: unicode name
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||
|
||||
def set_name(self, name):
|
||||
def set_name(self, name: str) -> bool:
|
||||
""" set name of this chat.
|
||||
|
||||
:param name: as a unicode string.
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
|
||||
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
|
||||
|
||||
def mute(self, duration=None):
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
""" mutes the chat
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
@@ -127,7 +136,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self):
|
||||
def unmute(self) -> None:
|
||||
""" unmutes the chat
|
||||
|
||||
:returns: None
|
||||
@@ -136,7 +145,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
def get_mute_duration(self):
|
||||
def get_mute_duration(self) -> int:
|
||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||
|
||||
:param duration:
|
||||
@@ -144,37 +153,37 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
def get_ephemeral_timer(self) -> int:
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer):
|
||||
def set_ephemeral_timer(self, timer: int) -> bool:
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
|
||||
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
|
||||
|
||||
def get_type(self):
|
||||
def get_type(self) -> int:
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat)
|
||||
|
||||
def get_encryption_info(self):
|
||||
def get_encryption_info(self) -> Optional[str]:
|
||||
"""Return encryption info for this chat.
|
||||
|
||||
:returns: a string with encryption preferences of all chat members"""
|
||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_join_qr(self):
|
||||
def get_join_qr(self) -> Optional[str]:
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -186,7 +195,7 @@ class Chat(object):
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
|
||||
def send_msg(self, msg):
|
||||
def send_msg(self, msg: Message) -> Message:
|
||||
"""send a message by using a ready Message object.
|
||||
|
||||
:param msg: a :class:`deltachat.message.Message` instance
|
||||
@@ -504,8 +513,9 @@ class Chat(object):
|
||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
||||
timestamp=datetime.utcfromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i)
|
||||
timestamp=datetime.fromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i),
|
||||
timezone.utc
|
||||
),
|
||||
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from typing import Any, List
|
||||
|
||||
from .capi import lib
|
||||
|
||||
|
||||
for name in dir(lib):
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name.startswith("DC_"):
|
||||
globals()[name] = getattr(lib, name)
|
||||
del name
|
||||
return getattr(lib, name)
|
||||
return globals()[name]
|
||||
|
||||
|
||||
def __dir__() -> List[str]:
|
||||
return sorted(name for name in dir(lib) if name.startswith("DC_"))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
""" Contact object. """
|
||||
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .chat import Chat
|
||||
from . import const
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
||||
|
||||
|
||||
class Contact(object):
|
||||
@@ -35,18 +37,25 @@ class Contact(object):
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
def addr(self) -> str:
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
display_name = name
|
||||
|
||||
@props.with_doc
|
||||
def last_seen(self) -> date:
|
||||
"""Last seen timestamp."""
|
||||
return datetime.fromtimestamp(
|
||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
||||
)
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
@@ -67,15 +76,13 @@ class Contact(object):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_profile_image(self):
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
"""
|
||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
return from_optional_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, TypeVar, Generator, Callable
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
@@ -11,21 +14,28 @@ def as_dc_charpointer(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor):
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
def from_dc_charpointer(obj) -> str:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
raise ValueError
|
||||
|
||||
|
||||
def from_optional_dc_charpointer(obj) -> Optional[str]:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
return None
|
||||
|
||||
|
||||
class DCLot:
|
||||
def __init__(self, dc_lot):
|
||||
def __init__(self, dc_lot) -> None:
|
||||
self._dc_lot = dc_lot
|
||||
|
||||
def id(self):
|
||||
def id(self) -> int:
|
||||
return lib.dc_lot_get_id(self._dc_lot)
|
||||
|
||||
def state(self):
|
||||
@@ -44,4 +54,4 @@ class DCLot:
|
||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
||||
if ts == 0:
|
||||
return None
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@@ -11,7 +11,7 @@ from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const
|
||||
from deltachat import const, Account
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
@@ -62,7 +62,7 @@ def dc_account_after_shutdown(account):
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account):
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
@@ -251,7 +251,16 @@ class DirectImap:
|
||||
return res
|
||||
|
||||
def append(self, folder, msg):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(folder, msg)
|
||||
|
||||
def get_uid_by_message_id(self, message_id):
|
||||
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
@@ -9,11 +9,11 @@ from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_dc_charpointer
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name, data1, data2):
|
||||
def __init__(self, name: str, data1, data2):
|
||||
self.name = name
|
||||
self.data1 = data1
|
||||
self.data2 = data2
|
||||
@@ -29,18 +29,18 @@ class FFIEventLogger:
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
||||
self.account.log(str(ffi_event))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_log_line(self, message):
|
||||
t = threading.currentThread()
|
||||
t = threading.current_thread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
@@ -69,7 +69,7 @@ class FFIEventTracker:
|
||||
self._event_queue = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent):
|
||||
self._event_queue.put(ffi_event)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
@@ -96,7 +96,7 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_contains(self, regex):
|
||||
def get_info_contains(self, regex: str) -> FFIEvent:
|
||||
rex = re.compile(regex)
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
@@ -111,6 +111,33 @@ class FFIEventTracker:
|
||||
if m is not None:
|
||||
return m.groups()
|
||||
|
||||
def wait_for_connectivity(self, connectivity):
|
||||
"""Wait for the specified connectivity.
|
||||
This only works reliably if the connectivity doesn't change
|
||||
again too quickly, otherwise we might miss it."""
|
||||
while 1:
|
||||
if self.account.get_connectivity() == connectivity:
|
||||
return
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def wait_for_connectivity_change(self, previous, expected_next):
|
||||
"""Wait until the connectivity changes to `expected_next`.
|
||||
Fails the test if it changes to something else."""
|
||||
while 1:
|
||||
current = self.account.get_connectivity()
|
||||
if current == expected_next:
|
||||
return
|
||||
elif current != previous:
|
||||
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
|
||||
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def wait_for_all_work_done(self):
|
||||
while 1:
|
||||
if self.account.all_work_done():
|
||||
return
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
@@ -149,6 +176,7 @@ class FFIEventTracker:
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_msg_delivered(self, msg):
|
||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
@@ -162,10 +190,10 @@ class EventThread(threading.Thread):
|
||||
|
||||
With each Account init this callback thread is started.
|
||||
"""
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self.daemon = True
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
@@ -175,17 +203,17 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def mark_shutdown(self):
|
||||
def mark_shutdown(self) -> None:
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self, timeout=None) -> None:
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
""" get and run events until shutdown. """
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
self._inner_run()
|
||||
@@ -207,7 +235,7 @@ class EventThread(threading.Thread):
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
data2 = lib.dc_event_get_data2_int(event)
|
||||
|
||||
@@ -223,7 +251,7 @@ class EventThread(threading.Thread):
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
account = self.account
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
|
||||
@@ -43,7 +43,7 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_incoming_message(self, message):
|
||||
""" Called on any incoming message (to deaddrop or chat). """
|
||||
""" Called on any incoming message (both existing chats and contact requests). """
|
||||
|
||||
@account_hookspec
|
||||
def ac_outgoing_message(self, message):
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -61,16 +62,13 @@ class Message(object):
|
||||
def create_chat(self):
|
||||
""" create or get an existing chat (group) object for this message.
|
||||
|
||||
If the message is a deaddrop contact request
|
||||
If the message is a contact request
|
||||
the sender will become an accepted contact.
|
||||
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
from .chat import Chat
|
||||
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
|
||||
ctx = self.account._dc_context
|
||||
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
|
||||
return Chat(self.account, chat_id)
|
||||
self.chat.accept()
|
||||
return self.chat
|
||||
|
||||
@props.with_doc
|
||||
def id(self):
|
||||
@@ -78,7 +76,7 @@ class Message(object):
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
def text(self) -> str:
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||
|
||||
@@ -87,9 +85,9 @@ class Message(object):
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
def html(self):
|
||||
def html(self) -> str:
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
|
||||
def has_html(self):
|
||||
@@ -116,12 +114,13 @@ class Message(object):
|
||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||
|
||||
@props.with_doc
|
||||
def basename(self):
|
||||
def basename(self) -> str:
|
||||
"""basename of the attachment if it exists, otherwise empty string. """
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self):
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
@@ -133,7 +132,7 @@ class Message(object):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self):
|
||||
def get_setupcodebegin(self) -> str:
|
||||
""" return the first characters of a setup code in a setup message. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
@@ -141,11 +140,15 @@ class Message(object):
|
||||
""" return True if this message was encrypted. """
|
||||
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
|
||||
|
||||
def is_bot(self):
|
||||
""" return True if this message is submitted automatically. """
|
||||
return bool(lib.dc_msg_is_bot(self._dc_msg))
|
||||
|
||||
def is_forwarded(self):
|
||||
""" return True if this message was forwarded. """
|
||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
||||
|
||||
def get_message_info(self):
|
||||
def get_message_info(self) -> str:
|
||||
""" Return informational text for a single message.
|
||||
|
||||
The text is multiline and may contain eg. the raw text of the message.
|
||||
@@ -169,7 +172,7 @@ class Message(object):
|
||||
:returns: naive datetime.datetime() object.
|
||||
"""
|
||||
ts = lib.dc_msg_get_timestamp(self._dc_msg)
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@props.with_doc
|
||||
def time_received(self):
|
||||
@@ -179,7 +182,7 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
@@ -199,14 +202,14 @@ class Message(object):
|
||||
"""
|
||||
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
|
||||
:returns: Quoted text"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
@@ -222,6 +225,10 @@ class Message(object):
|
||||
"""Quote setter"""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -239,9 +246,9 @@ class Message(object):
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
def chat(self):
|
||||
@@ -254,12 +261,12 @@ class Message(object):
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def override_sender_name(self):
|
||||
def override_sender_name(self) -> Optional[str]:
|
||||
"""the name that should be shown over the message instead of the contact display name.
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
|
||||
def set_override_sender_name(self, name):
|
||||
|
||||
@@ -14,7 +14,7 @@ class Provider(object):
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, account, addr):
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
@@ -24,19 +24,19 @@ class Provider(object):
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self):
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self):
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_before_login_hints(self._provider))
|
||||
lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
|
||||
@@ -9,6 +9,7 @@ import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
from typing import List, Dict, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
def __init__(self, fn) -> None:
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
configlist: List[Dict[str, str]]
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@@ -179,7 +182,7 @@ def session_liveconfig(request):
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# trying to find test data heuristically
|
||||
# because we are run from a dev-setup with pytest direct,
|
||||
# through tox, and then maybe also from deltachat-binding
|
||||
@@ -210,7 +213,10 @@ def data(request):
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
@@ -297,21 +303,20 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
def get_online_configuring_account(self, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_watch"] = str(int(mvbox))
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
def get_one_online_account(self, pre_generated_key=True, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
pre_generated_key=pre_generated_key, move=move)
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
@@ -330,7 +335,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
|
||||
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
|
||||
direct_imap object of an online account. This simulates the user setting
|
||||
up a new device without importing a backup.
|
||||
|
||||
@@ -345,7 +350,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_watch=account.get_config("mvbox_watch"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
@@ -423,7 +427,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
pass
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1, ac2):
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
@@ -451,7 +455,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
|
||||
class BotProcess:
|
||||
def __init__(self, popen, bot_cfg):
|
||||
stdout_queue: queue.Queue
|
||||
|
||||
def __init__(self, popen, bot_cfg) -> None:
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
|
||||
@@ -459,10 +465,10 @@ class BotProcess:
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.setDaemon(1)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self):
|
||||
def _run_stdout_thread(self) -> None:
|
||||
try:
|
||||
while 1:
|
||||
line = self.popen.stdout.readline()
|
||||
@@ -474,10 +480,10 @@ class BotProcess:
|
||||
finally:
|
||||
self.stdout_queue.put(None)
|
||||
|
||||
def kill(self):
|
||||
def kill(self) -> None:
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30):
|
||||
def wait(self, timeout=30) -> None:
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
@@ -509,14 +515,14 @@ def tmp_db_path(tmpdir):
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
def sec(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg):
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -10,7 +10,7 @@ from deltachat.tracker import ImexTracker
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
@@ -57,7 +57,7 @@ class TestOfflineAccountBasic:
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.com", alice_public, alice_secret)
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -265,23 +265,23 @@ class TestOfflineChat:
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
msg = chat.get_draft()
|
||||
assert msg.text == "xyz title1"
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
@@ -447,7 +447,7 @@ class TestOfflineChat:
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
past1s = datetime.utcnow() - timedelta(seconds=1)
|
||||
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
msg = chat1.send_text("msg1")
|
||||
ts = msg.time_sent
|
||||
assert msg.time_received is None
|
||||
@@ -860,7 +860,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_mvbox_sentbox_threads(self, acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True, sentbox=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True, sentbox=True)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
@@ -874,16 +874,20 @@ class TestOnlineAccount:
|
||||
|
||||
def test_move_works(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
def test_move_works_on_self_sent(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
@@ -909,12 +913,11 @@ class TestOnlineAccount:
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
assert msg_in.text == "message2"
|
||||
|
||||
lp.sec("ac2: check that the message arrive in deaddrop")
|
||||
lp.sec("ac2: check that the message arrived in a chat")
|
||||
chat2 = msg_in.chat
|
||||
assert msg_in in chat2.get_messages()
|
||||
assert not msg_in.is_forwarded()
|
||||
assert chat2.is_deaddrop()
|
||||
assert chat2 == ac2.get_deaddrop_chat()
|
||||
assert chat2.is_contact_request()
|
||||
|
||||
lp.sec("ac2: create new chat and forward message to it")
|
||||
chat3 = ac2.create_group_chat("newgroup")
|
||||
@@ -954,7 +957,7 @@ class TestOnlineAccount:
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
def test_send_self_message(self, acfactory, lp):
|
||||
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
|
||||
ac1 = acfactory.get_one_online_account(move=True)
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
chat.send_text("hello")
|
||||
@@ -974,21 +977,22 @@ class TestOnlineAccount:
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert not msg2.is_forwarded()
|
||||
assert msg2.get_sender_contact().display_name == ac1.get_config("displayname")
|
||||
|
||||
lp.sec("check the message arrived in contact-requests/deaddrop")
|
||||
lp.sec("check the message arrived in contact request chat")
|
||||
chat2 = msg2.chat
|
||||
assert msg2 in chat2.get_messages()
|
||||
assert chat2.is_deaddrop()
|
||||
assert chat2.count_fresh_messages() == 0
|
||||
assert msg2.time_received >= msg1.time_sent
|
||||
assert chat2.is_contact_request()
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
# Like it or not, this assert is flaky
|
||||
# assert msg2.time_received >= msg1.time_sent
|
||||
|
||||
lp.sec("create new chat with contact and verify it's proper")
|
||||
chat2b = msg2.create_chat()
|
||||
assert not chat2b.is_deaddrop()
|
||||
assert not chat2b.is_contact_request()
|
||||
assert chat2b.count_fresh_messages() == 1
|
||||
|
||||
lp.sec("mark chat as noticed")
|
||||
@@ -1035,27 +1039,34 @@ class TestOnlineAccount:
|
||||
|
||||
def test_moved_markseen(self, acfactory, lp):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, config={"inbox_watch": "0"})
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
acfactory.wait_configure_and_start_io([ac1, ac2])
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.stop_io()
|
||||
ac2.direct_imap.idle_start()
|
||||
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.stop_io()
|
||||
|
||||
# Wait for the message to arrive.
|
||||
ac2.direct_imap.idle_check(terminate=True)
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
# mailcow server contains this rule by default.
|
||||
ac1.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac1.start_io()
|
||||
ac1.direct_imap.idle_wait_for_seen()
|
||||
ac1.direct_imap.idle_done()
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.idle_start()
|
||||
ac2.start_io()
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
fetch = list(ac1.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
# Accept the contact request.
|
||||
msg.chat.accept()
|
||||
ac2.mark_seen_messages([msg])
|
||||
ac2.direct_imap.idle_wait_for_seen()
|
||||
ac2.direct_imap.idle_done()
|
||||
|
||||
fetch = list(ac2.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
flags = fetch[-1][b'FLAGS']
|
||||
is_seen = b'\\Seen' in flags
|
||||
assert is_seen
|
||||
@@ -1096,8 +1107,8 @@ class TestOnlineAccount:
|
||||
def test_markseen_message_and_mdn(self, acfactory, mvbox_move):
|
||||
# Please only change this test if you are very sure that it will still catch the issues it catches now.
|
||||
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
@@ -1126,7 +1137,7 @@ class TestOnlineAccount:
|
||||
group1.add_contact(ac2)
|
||||
group1.send_text("hello")
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
group2 = msg2.create_chat()
|
||||
assert group2.get_name() == group1.get_name()
|
||||
|
||||
@@ -1143,7 +1154,7 @@ class TestOnlineAccount:
|
||||
assert not msg_reply1.chat.is_group()
|
||||
assert msg_reply1.chat.id == private_chat1.id
|
||||
|
||||
def test_mdn_asymetric(self, acfactory, lp):
|
||||
def test_mdn_asymmetric(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(move=True)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
@@ -1167,6 +1178,9 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
@@ -1176,6 +1190,9 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
assert ac1.direct_imap.idle_wait_for_seen()
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
@@ -1189,7 +1206,7 @@ class TestOnlineAccount:
|
||||
chat.send_text("message1")
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
|
||||
lp.sec("create new chat with contact and send back (encrypted) message")
|
||||
@@ -1222,6 +1239,40 @@ class TestOnlineAccount:
|
||||
assert not msg.is_encrypted()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
def test_gossip_optimization(self, acfactory, lp):
|
||||
"""Test that gossip timestamp is updated when someone else sends gossip,
|
||||
so we don't have to send gossip ourselves.
|
||||
"""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
acfactory.introduce_each_other([ac2, ac3])
|
||||
|
||||
lp.sec("ac1 creates a group chat with ac2")
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
# No Autocrypt gossip was sent yet.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == 0
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.text == "hi"
|
||||
|
||||
lp.sec("ac2 adds ac3 to the group")
|
||||
msg.chat.add_contact(ac3)
|
||||
|
||||
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
|
||||
# ac1 does not need to send gossip because ac2 already did it.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == int(msg.time_sent.timestamp())
|
||||
|
||||
def test_gossip_encryption_preference(self, acfactory, lp):
|
||||
"""Test that encryption preference of group members is gossiped to new members.
|
||||
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
|
||||
@@ -1319,12 +1370,14 @@ class TestOnlineAccount:
|
||||
assert not device_chat.can_send()
|
||||
assert device_chat.get_draft() is None
|
||||
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory):
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email."""
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
|
||||
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.com").create_chat()
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
@@ -1338,16 +1391,16 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.append("Drafts", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.com
|
||||
To: alice@example.org
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Sent", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.com
|
||||
To: alice@example.org
|
||||
Message-ID: <hsabaeni@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
@@ -1355,6 +1408,7 @@ class TestOnlineAccount:
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
msg = ac1._evtracker.wait_next_messages_changed()
|
||||
@@ -1365,6 +1419,47 @@ class TestOnlineAccount:
|
||||
assert msg.text == "subj – message in Sent"
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.stop_io()
|
||||
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
|
||||
ac1.direct_imap.select_folder("Drafts")
|
||||
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1.direct_imap.conn.move(uid, "Sent")
|
||||
|
||||
ac1.start_io()
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
def test_no_old_msg_is_fresh(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1_clone = acfactory.clone_online_account(ac1)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac1_clone.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "0")
|
||||
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone.create_chat(ac2)
|
||||
|
||||
lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg_id = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
assert ev.data1 == first_msg_id.id
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
def test_prefer_encrypt(self, acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
@@ -1415,6 +1510,33 @@ class TestOnlineAccount:
|
||||
# Majority prefers encryption now
|
||||
assert msg5.is_encrypted()
|
||||
|
||||
def test_bot(self, acfactory, lp):
|
||||
"""Test that bot messages can be identified as such"""
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("bot", "0")
|
||||
ac2.set_config("bot", "1")
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("sending a message from ac1 to ac2")
|
||||
text1 = "hello"
|
||||
chat.send_text(text1)
|
||||
|
||||
lp.sec("wait for ac2 to receive a message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == text1
|
||||
assert not msg_in.is_bot()
|
||||
|
||||
lp.sec("sending a message from ac2 to ac1")
|
||||
text2 = "reply"
|
||||
msg_in.chat.send_text(text2)
|
||||
|
||||
lp.sec("wait for ac1 to receive a message")
|
||||
msg_in = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == text2
|
||||
assert msg_in.is_bot()
|
||||
|
||||
def test_quote_encrypted(self, acfactory, lp):
|
||||
"""Test that replies to encrypted messages with quotes are encrypted."""
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -1427,7 +1549,7 @@ class TestOnlineAccount:
|
||||
assert not msg1.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert not msg2.is_encrypted()
|
||||
|
||||
@@ -1476,7 +1598,7 @@ class TestOnlineAccount:
|
||||
chat1.send_text("hi")
|
||||
|
||||
lp.sec("ac2 receives contact request from ac1")
|
||||
received_message = ac2._evtracker.wait_next_messages_changed()
|
||||
received_message = ac2._evtracker.wait_next_incoming_message()
|
||||
assert received_message.text == "hi"
|
||||
|
||||
basename = "attachment.txt"
|
||||
@@ -1511,7 +1633,7 @@ class TestOnlineAccount:
|
||||
assert msg_out.get_mime_headers() is None
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
in_id = ev.data2
|
||||
mime = ac2.get_message_by_id(in_id).get_mime_headers()
|
||||
assert mime.get_all("From")
|
||||
@@ -1810,8 +1932,8 @@ class TestOnlineAccount:
|
||||
ac1.create_chat(ac2).send_text("with avatar!")
|
||||
|
||||
lp.sec("ac2: wait for receiving message and avatar from ac1")
|
||||
msg2 = ac2._evtracker.wait_next_messages_changed()
|
||||
assert msg2.chat.is_deaddrop()
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.chat.is_contact_request()
|
||||
received_path = msg2.get_sender_contact().get_profile_image()
|
||||
assert open(received_path, "rb").read() == open(p, "rb").read()
|
||||
|
||||
@@ -1886,6 +2008,8 @@ class TestOnlineAccount:
|
||||
ev = in_list.get(timeout=10)
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
assert ev.action == "added"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
assert ev.contact.addr == "devnull@testrun.org"
|
||||
@@ -2000,6 +2124,82 @@ class TestOnlineAccount:
|
||||
assert msg_back.chat == chat
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_connectivity(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec("Test stop_io() and start_io()")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " +
|
||||
"all messages are fetched")
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
|
||||
ac1.direct_imap.idle_check(terminate=False)
|
||||
ac1.maybe_network()
|
||||
|
||||
ac1._evtracker.wait_for_all_work_done()
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
ac2.create_chat(ac1).send_text("Hi 2")
|
||||
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
|
||||
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
|
||||
|
||||
ac1.maybe_network()
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
|
||||
ac1.create_contact(ac2).block()
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.maybe_network()
|
||||
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
|
||||
|
||||
ac1.set_config("configured_mail_pw", "abc")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
def test_fetch_deleted_msg(self, acfactory, lp):
|
||||
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
|
||||
hundreds of times, because uid_next was not updated.
|
||||
@@ -2042,7 +2242,7 @@ class TestOnlineAccount:
|
||||
break # DC is done with reading messages
|
||||
|
||||
def test_send_receive_locations(self, acfactory, lp):
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
@@ -2165,7 +2365,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
ac2 = acfactory.get_online_configuring_account(move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
@@ -2426,31 +2626,26 @@ class TestOnlineAccount:
|
||||
assert received_reply.quoted_text == "hello"
|
||||
assert received_reply.quote.id == out_msg.id
|
||||
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,inbox_watch,", [
|
||||
("xyz", False, "xyz", "1"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat", "1"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX", "1"), # ...emails are moved from the spam folder to the Inbox
|
||||
("INBOX", False, "INBOX", "0"), # ...emails are found in the `Inbox` folder even if `inbox_watch` is "0"
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,", [
|
||||
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox
|
||||
])
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination, inbox_watch):
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
lp.sec("Testing variant " + variant)
|
||||
ac1 = acfactory.get_online_configuring_account(move=move)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("inbox_watch", inbox_watch)
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder(folder)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
ac1.stop_io()
|
||||
|
||||
# Send a message to ac1 and move it to the mvbox:
|
||||
@@ -2466,11 +2661,7 @@ class TestOnlineAccount:
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
# Wait until the message was moved (if at all) and we are IDLEing again:
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
@@ -2493,7 +2684,7 @@ class TestOnlineAccount:
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac1.set_config("sentbox_move", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
|
||||
@@ -2560,7 +2751,7 @@ class TestOnlineAccount:
|
||||
|
||||
lp.sec("receive a message")
|
||||
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
|
||||
ac1._evtracker.wait_next_messages_changed()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.direct_imap.idle_start()
|
||||
@@ -2591,6 +2782,22 @@ class TestOnlineAccount:
|
||||
# We can't decrypt the message in this chat, so the chat is empty:
|
||||
assert len(private_messages) == 0
|
||||
|
||||
def test_delete_deltachat_folder(self, acfactory):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure(ac1)
|
||||
|
||||
ac1.direct_imap.conn.delete_folder("DeltaChat")
|
||||
assert len(ac1.direct_imap.conn.list_folders(pattern="DeltaChat")) == 0
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac2.create_chat(ac1).send_text("hello")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert len(ac1.direct_imap.conn.list_folders(pattern="DeltaChat")) == 1
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
@@ -2705,7 +2912,8 @@ class TestGroupStressTests:
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert msg.chat.num_contacts() == chat.num_contacts()
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
@@ -2716,7 +2924,6 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
|
||||
def test_invalid_user(self, acfactory):
|
||||
ac1, configdict = acfactory.get_online_config()
|
||||
@@ -2724,7 +2931,6 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
|
||||
def test_invalid_domain(self, acfactory):
|
||||
ac1, configdict = acfactory.get_online_config()
|
||||
@@ -2732,4 +2938,3 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
|
||||
@@ -6,8 +6,6 @@ import shutil
|
||||
import pytest
|
||||
from filecmp import cmp
|
||||
|
||||
from deltachat import const
|
||||
|
||||
|
||||
def wait_msg_delivered(account, msg_list):
|
||||
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """
|
||||
@@ -102,14 +100,10 @@ class TestOnlineInCreation:
|
||||
])
|
||||
|
||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
received_original = ac2.get_message_by_id(ev1.data2)
|
||||
received_original = ac2._evtracker.wait_next_incoming_message()
|
||||
assert cmp(received_original.filename, orig, shallow=False)
|
||||
|
||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev2.data1 != ev1.data1
|
||||
received_copy = ac2.get_message_by_id(ev2.data2)
|
||||
received_copy = ac2._evtracker.wait_next_incoming_message()
|
||||
assert received_copy.id != received_original.id
|
||||
assert cmp(received_copy.filename, orig, shallow=False)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[tox]
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py3
|
||||
lint
|
||||
mypy
|
||||
auditwheels
|
||||
|
||||
[testenv]
|
||||
@@ -42,12 +44,20 @@ commands =
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
mypy
|
||||
typing
|
||||
types-setuptools
|
||||
types-requests
|
||||
commands =
|
||||
mypy --no-incremental src/
|
||||
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# Pin dependencies to the versions which actually work with Python 3.5.
|
||||
sphinx==3.4.3
|
||||
breathe==4.28.0
|
||||
sphinx
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.50.0
|
||||
1.54.0
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
# Continuous Integration Scripts for Delta Chat
|
||||
|
||||
Continuous Integration, run through [GitHub
|
||||
Actions](https://docs.github.com/actions),
|
||||
[CircleCI](https://app.circleci.com/) and an own build machine.
|
||||
Actions](https://docs.github.com/actions)
|
||||
and an own build machine.
|
||||
|
||||
## Description of scripts
|
||||
|
||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
||||
|
||||
- `../.circleci/config.yml` describing the build jobs that are run
|
||||
by CircleCI.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
`run-python-test.sh` remotely on the build machine.
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$DEVPI_LOGIN" ] ; then
|
||||
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -xe
|
||||
|
||||
PYDOCDIR=${1:?directory with python docs}
|
||||
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
||||
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
|
||||
SSHTARGET=ci@b1.delta.chat
|
||||
|
||||
|
||||
# if CIRCLE_BRANCH is not set we are called for a tag with empty CIRCLE_BRANCH variable.
|
||||
export BRANCH=${CIRCLE_BRANCH:master}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
|
||||
|
||||
|
||||
# python docs to py.delta.chat
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
|
||||
rsync -avz \
|
||||
--delete \
|
||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||
"$PYDOCDIR/html/" \
|
||||
delta@py.delta.chat:build/${BRANCH}
|
||||
|
||||
# C docs to c.delta.chat
|
||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
|
||||
rsync -avz \
|
||||
--delete \
|
||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||
"$DOXYDOCDIR/html/" \
|
||||
delta@c.delta.chat:build-c/${BRANCH}
|
||||
|
||||
echo -----------------------
|
||||
echo upload wheels
|
||||
echo -----------------------
|
||||
|
||||
# Bundle external shared libraries into the wheels
|
||||
|
||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSHTARGET mkdir -p $BUILDDIR
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null scripts/cleanup_devpi_indices.py $SSHTARGET:$BUILDDIR
|
||||
rsync -avz \
|
||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||
$WHEELHOUSEDIR \
|
||||
$SSHTARGET:$BUILDDIR
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
|
||||
# we rely on the "venv" virtualenv on the remote account to exist
|
||||
source venv/bin/activate
|
||||
cd $BUILDDIR
|
||||
|
||||
devpi use https://m.devpi.net
|
||||
devpi login dc --password $DEVPI_LOGIN
|
||||
|
||||
N_BRANCH=${BRANCH//[\/]}
|
||||
|
||||
devpi use dc/\$N_BRANCH || {
|
||||
devpi index -c \$N_BRANCH
|
||||
devpi use dc/\$N_BRANCH
|
||||
}
|
||||
devpi index \$N_BRANCH bases=/root/pypi
|
||||
devpi upload wheelhouse/deltachat*
|
||||
|
||||
# remove devpi non-master dc indices if thy are too old
|
||||
# this script was copied above
|
||||
python cleanup_devpi_indices.py
|
||||
_HERE
|
||||
21
scripts/concourse/README.md
Normal file
21
scripts/concourse/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Concourse CI pipeline
|
||||
|
||||
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
|
||||
that builds C documentation, Python documentation, Python wheels for `x86_64`
|
||||
and `aarch64` and Python source packages, and uploads them.
|
||||
|
||||
To setup the pipeline run
|
||||
```
|
||||
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
|
||||
```
|
||||
where `secret.yml` contains the following secrets:
|
||||
```
|
||||
c.delta.chat:
|
||||
private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----
|
||||
devpi:
|
||||
login: dc
|
||||
password: ...
|
||||
```
|
||||
232
scripts/concourse/docs_wheels.yml
Normal file
232
scripts/concourse/docs_wheels.yml
Normal file
@@ -0,0 +1,232 @@
|
||||
resources:
|
||||
- name: deltachat-core-rust
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: master
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: master
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
tag_filter: "py-*"
|
||||
|
||||
jobs:
|
||||
- name: doxygen
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
trigger: true
|
||||
|
||||
# Build Doxygen documentation
|
||||
- task: build-doxygen
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust
|
||||
outputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: hrektts/doxygen
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
cd deltachat-core-rust
|
||||
bash scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
|
||||
|
||||
- name: python-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: vito/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-docs
|
||||
path: ./python/doc/_build/
|
||||
# Source packages
|
||||
- name: py-dist
|
||||
path: ./python/.docker-tox/dist/
|
||||
# Binary wheels
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload python docs to py.delta.chat
|
||||
- task: upload-py-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: py-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
|
||||
|
||||
# Upload x86_64 wheels and source packages
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
- name: py-dist
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
devpi upload py-dist/*
|
||||
|
||||
- name: python-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: vito/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
|
||||
UNPACK_ROOTFS: "true"
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if ! which grcov 2>/dev/null 1>&2; then
|
||||
if ! command -v grcov >/dev/null; then
|
||||
echo >&2 '`grcov` not found. Check README at https://github.com/mozilla/grcov for setup instructions.'
|
||||
echo >&2 'Run `cargo install grcov` to build `grcov` from source.'
|
||||
exit 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.30.0
|
||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||
PERL_VERSION=5.34.0
|
||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -8,9 +8,11 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
curl "https://static.rust-lang.org/dist/rust-1.52.1-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-1.52.1-$(uname -m)-unknown-linux-gnu"
|
||||
RUST_VERSION=1.54.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-1.52.1-$(uname -m)-unknown-linux-gnu"
|
||||
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.30.0
|
||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||
PERL_VERSION=5.34.0
|
||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain "1.50.0-$(uname -m)-unknown-linux-gnu" -y
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
rustc --version
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
|
||||
# remove some 300-400 MB that we don't need for automated builds
|
||||
rm -rf "/root/.rustup/toolchains/1.50.0-$(uname -m)-unknown-linux-gnu/share"
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
set -xe
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
# we seem to need .git for setuptools_scm versioning
|
||||
find .git >>.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR-arm64"
|
||||
|
||||
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.
|
||||
|
||||
for arch in "" "-arm64"; do
|
||||
|
||||
ssh $SSHTARGET bash -c "cat >${BUILDDIR}${arch}/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
shopt -s huponexit
|
||||
cd ${BUILDDIR}${arch}
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps${arch} scripts/run_all.sh
|
||||
|
||||
_HERE
|
||||
|
||||
done
|
||||
|
||||
echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
echo "--- Building aarch64 wheels"
|
||||
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR-arm64/exec_docker_run"
|
||||
|
||||
echo "--- Building x86_64 wheels"
|
||||
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||
|
||||
mkdir -p workspace
|
||||
|
||||
# Wheels
|
||||
for arch in "" "-arm64"; do
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||
done
|
||||
|
||||
# Source packages
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||
|
||||
# Documentation
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||
@@ -19,11 +19,9 @@ export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp36-cp36m/bin
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.6
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
rm -f python3.7
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
rm -f python3.8
|
||||
|
||||
@@ -86,6 +86,8 @@ def main():
|
||||
print("")
|
||||
print(" git tag -a {}".format(newversion))
|
||||
print(" git push origin {}".format(newversion))
|
||||
print(" git tag -a py-{}".format(newversion))
|
||||
print(" git push origin py-{}".format(newversion))
|
||||
print("")
|
||||
|
||||
|
||||
|
||||
63
spec.md
63
spec.md
@@ -1,6 +1,6 @@
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.32.0
|
||||
Version: 0.33.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -301,9 +301,9 @@ to add a `Chat-Group-Avatar` only on image changes.
|
||||
|
||||
A user MAY have a profile-image that MAY be distributed to their contacts.
|
||||
To change or set the profile-image,
|
||||
the messenger MUST attach an image file to a message
|
||||
and MUST add the header `Chat-User-Avatar`
|
||||
with the value set to the image name.
|
||||
the messenger MUST add the header `Chat-User-Avatar: base64:IMAGEDATA`.
|
||||
To bypass limits of headers, it is recommended not to use the outer header
|
||||
and to limit the size to 20k.
|
||||
|
||||
To remove the profile-image,
|
||||
the messenger MUST add the header `Chat-User-Avatar: 0`.
|
||||
@@ -320,19 +320,14 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Chat-User-Avatar: photo.jpg
|
||||
Subject: Chat: Hello, ...
|
||||
Content-Type: multipart/mixed; boundary="==break=="
|
||||
|
||||
--==break==
|
||||
Content-Type: text/plain
|
||||
Chat-User-Avatar: base64:AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQY ...
|
||||
|
||||
Hello, I've changed my profile image.
|
||||
--==break==
|
||||
Content-Type: image/jpeg
|
||||
Content-Disposition: attachment; filename="photo.jpg"
|
||||
|
||||
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
|
||||
--==break==--
|
||||
|
||||
The image format SHOULD be image/jpeg or image/png.
|
||||
@@ -342,6 +337,11 @@ in the same message.
|
||||
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
|
||||
only on image changes.
|
||||
|
||||
In older specs, the profile-image was sent as an attachment
|
||||
and `Chat-User-Avatar:` specified its name.
|
||||
However, it turned out that these attachements are kind of unuexpected to users,
|
||||
therefore the profile-image go to the header now.
|
||||
|
||||
|
||||
# Locations
|
||||
|
||||
@@ -401,9 +401,41 @@ it is fine if the location is detected on forwarding etc.
|
||||
</kml>
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
# Stickers
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
Stickers are send as normal images
|
||||
with the additional header `Chat-Content: sticker`.
|
||||
|
||||
It is discouraged to send stickers together with user generated text,
|
||||
however, stickers can be used as a reply to a message
|
||||
and also the footer should be set as usual.
|
||||
|
||||
From: alice@example.org
|
||||
To: bob@example.com
|
||||
Chat-Version: 1.0
|
||||
Chat-Content: sticker
|
||||
Message-ID: Mr.12345uvwxyZ.0005@example.org
|
||||
Subject: Message from Alice
|
||||
Content-Type: multipart/mixed; boundary="==break=="
|
||||
|
||||
--==break==
|
||||
Content-Type: text/plain
|
||||
|
||||
--
|
||||
Hi there! I am using this new messenger!
|
||||
--==break==
|
||||
Content-Type: image/png
|
||||
Content-Disposition: attachment; filename="sticker.png"
|
||||
|
||||
R0lGODlhpAGkAfe9AP+zd2eQkZhrI//z9v++PMb///+scrdDT3BtbtrZ2f/LQSsREcdIVf9 ...
|
||||
--==break==--
|
||||
|
||||
Typical sticker formats are `image/png`, `image/gif` and `image/webp`.
|
||||
Animated stickers are supported
|
||||
by just using an image format that supports animation.
|
||||
|
||||
|
||||
# Voice messages
|
||||
|
||||
Messengers SHOULD add a `Chat-Voice-message: 1` header
|
||||
if an attached audio file is a voice message.
|
||||
@@ -417,6 +449,11 @@ This allows the receiver to show the time without knowing the file format.
|
||||
Chat-Voice-Message: 1
|
||||
Chat-Duration: 10000
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
Messengers MAY send and receive Message Disposition Notifications
|
||||
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
|
||||
[RFC 3503](https://tools.ietf.org/html/rfc3503))
|
||||
@@ -437,4 +474,4 @@ as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
Copyright © 2017-2020 Delta Chat contributors.
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
488
src/accounts.rs
488
src/accounts.rs
@@ -1,5 +1,8 @@
|
||||
//! # Account manager module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use async_std::channel::{self, Receiver, Sender};
|
||||
use async_std::fs;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
@@ -10,39 +13,37 @@ use anyhow::{ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::events::{Event, EventType, Events};
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
|
||||
accounts: BTreeMap<u32, Context>,
|
||||
emitter: EventEmitter,
|
||||
|
||||
/// Event channel to emit account manager errors.
|
||||
events: Events,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(os_name: String, dir: PathBuf) -> Result<Self> {
|
||||
pub async fn new(dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists().await {
|
||||
Accounts::create(os_name, &dir).await?;
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure, including a default account.
|
||||
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
// create default account
|
||||
let config = Config::new(os_name.clone(), dir).await?;
|
||||
let account_config = config.new_account(dir).await?;
|
||||
|
||||
Context::new(os_name, account_config.dbfile().into(), account_config.id)
|
||||
.await
|
||||
.context("failed to create default account")?;
|
||||
Config::new(dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -58,50 +59,65 @@ impl Accounts {
|
||||
let config = Config::from_file(config_file).await?;
|
||||
let accounts = config.load_accounts().await?;
|
||||
|
||||
let emitter = EventEmitter::new();
|
||||
|
||||
let events = Events::default();
|
||||
|
||||
emitter.sender.send(events.get_emitter()).await?;
|
||||
|
||||
for account in accounts.values() {
|
||||
emitter.add_account(account).await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
dir,
|
||||
config,
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
accounts,
|
||||
emitter,
|
||||
events,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.read().await.get(&id).cloned()
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub async fn get_selected_account(&self) -> Context {
|
||||
pub async fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account().await;
|
||||
self.accounts
|
||||
.read()
|
||||
.await
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.expect("inconsistent state")
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the currently selected account's id or None if no account is selected.
|
||||
pub async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account().await {
|
||||
0 => None,
|
||||
id => Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the given account.
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
self.config.select_account(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account.
|
||||
pub async fn add_account(&self) -> Result<u32> {
|
||||
let os_name = self.config.os_name().await;
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.write().await.remove(&id);
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.remove(&id);
|
||||
ensure!(ctx.is_some(), "no account with this id: {}", id);
|
||||
let ctx = ctx.unwrap();
|
||||
ctx.stop_io().await;
|
||||
@@ -118,8 +134,9 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Migrate an existing account into this structure.
|
||||
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
|
||||
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
|
||||
let blobdir = Context::derive_blobdir(&dbfile);
|
||||
let walfile = Context::derive_walfile(&dbfile);
|
||||
|
||||
ensure!(
|
||||
dbfile.exists().await,
|
||||
@@ -143,6 +160,7 @@ impl Accounts {
|
||||
|
||||
let new_dbfile = account_config.dbfile().into();
|
||||
let new_blobdir = Context::derive_blobdir(&new_dbfile);
|
||||
let new_walfile = Context::derive_walfile(&new_dbfile);
|
||||
|
||||
let res = {
|
||||
fs::create_dir_all(&account_config.dir)
|
||||
@@ -154,19 +172,19 @@ impl Accounts {
|
||||
fs::rename(&blobdir, &new_blobdir)
|
||||
.await
|
||||
.context("failed to rename blobdir")?;
|
||||
if walfile.exists().await {
|
||||
fs::rename(&walfile, &new_walfile)
|
||||
.await
|
||||
.context("failed to rename walfile")?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(
|
||||
self.config.os_name().await,
|
||||
new_dbfile,
|
||||
new_blobdir,
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
let ctx = Context::with_blobdir(new_dbfile, new_blobdir, account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -187,75 +205,114 @@ impl Accounts {
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.read().await.keys().copied().collect()
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Import a backup using a new account and selects it.
|
||||
pub async fn import_account(&self, file: PathBuf) -> Result<u32> {
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
let id = self.add_account().await?;
|
||||
let ctx = self.get_account(id).await.expect("just added");
|
||||
|
||||
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
|
||||
Ok(_) => Ok(id),
|
||||
Err(err) => {
|
||||
// remove temp account
|
||||
self.remove_account(id).await?;
|
||||
// set selection back
|
||||
self.select_account(old_id).await?;
|
||||
Err(err)
|
||||
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
///
|
||||
/// Returns whether all accounts finished their background work.
|
||||
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
///
|
||||
/// iOS can:
|
||||
/// - call dc_start_io() (in case IO was not running)
|
||||
/// - call dc_maybe_network()
|
||||
/// - while dc_accounts_all_work_done() returns false:
|
||||
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
for account in self.accounts.values() {
|
||||
if !account.all_work_done().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn start_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.stop_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_network(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
let emitters: Vec<_> = self
|
||||
.accounts
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(_id, a)| a.get_event_emitter())
|
||||
.collect();
|
||||
pub async fn maybe_network_lost(&self) {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network_lost().await;
|
||||
}
|
||||
}
|
||||
|
||||
EventEmitter(futures::stream::select_all(emitters))
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
self.events.emit(Event { id: 0, typ: event })
|
||||
}
|
||||
|
||||
/// Returns unified event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.emitter.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
|
||||
/// Unified event emitter for multiple accounts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventEmitter {
|
||||
/// Aggregate stream of events from all accounts.
|
||||
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
|
||||
|
||||
/// Sender for the channel where new account emitters will be pushed.
|
||||
sender: Sender<crate::events::EventEmitter>,
|
||||
|
||||
/// Receiver for the channel where new account emitters will be pushed.
|
||||
receiver: Receiver<crate::events::EventEmitter>,
|
||||
}
|
||||
|
||||
impl EventEmitter {
|
||||
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
|
||||
pub fn recv_sync(&mut self) -> Option<Event> {
|
||||
async_std::task::block_on(self.recv())
|
||||
pub fn new() -> Self {
|
||||
let (sender, receiver) = channel::unbounded();
|
||||
Self {
|
||||
stream: Arc::new(RwLock::new(futures::stream::SelectAll::new())),
|
||||
sender,
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
|
||||
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
|
||||
pub async fn recv(&mut self) -> Option<Event> {
|
||||
self.0.next().await
|
||||
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
|
||||
pub fn recv_sync(&mut self) -> Option<Event> {
|
||||
async_std::task::block_on(self.recv()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Async recv of an event. Return `None` if all `Sender`s have been dropped.
|
||||
pub async fn recv(&mut self) -> Result<Option<Event>> {
|
||||
let mut stream = self.stream.write().await;
|
||||
loop {
|
||||
match futures::future::select(self.receiver.recv(), stream.next()).await {
|
||||
futures::future::Either::Left((emitter, _)) => {
|
||||
stream.push(emitter?);
|
||||
}
|
||||
futures::future::Either::Right((ev, _)) => return Ok(ev),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add event emitter of a new account to the aggregate event emitter.
|
||||
pub async fn add_account(&self, context: &Context) -> Result<()> {
|
||||
self.sender.send(context.get_event_emitter()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventEmitter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,22 +323,25 @@ impl async_std::stream::Stream for EventEmitter {
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
std::pin::Pin::new(&mut self.0).poll_next(cx)
|
||||
std::pin::Pin::new(&mut self).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
pub const DB_NAME: &str = "dc.db";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
file: PathBuf,
|
||||
inner: Arc<RwLock<InnerConfig>>,
|
||||
inner: InnerConfig,
|
||||
}
|
||||
|
||||
/// Account manager configuration file contents.
|
||||
///
|
||||
/// This is serialized into TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct InnerConfig {
|
||||
pub os_name: String,
|
||||
/// The currently selected account.
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
@@ -289,15 +349,15 @@ struct InnerConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
|
||||
pub async fn new(dir: &PathBuf) -> Result<Self> {
|
||||
let inner = InnerConfig {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
let cfg = Config {
|
||||
file: dir.join(CONFIG_NAME),
|
||||
inner: Arc::new(RwLock::new(InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
})),
|
||||
inner,
|
||||
};
|
||||
|
||||
cfg.sync().await?;
|
||||
@@ -305,18 +365,11 @@ impl Config {
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub async fn os_name(&self) -> String {
|
||||
self.inner.read().await.os_name.clone()
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(
|
||||
&self.file,
|
||||
toml::to_string_pretty(&*self.inner.read().await)?,
|
||||
)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
@@ -324,22 +377,13 @@ impl Config {
|
||||
let bytes = fs::read(&file).await.context("failed to read file")?;
|
||||
let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?;
|
||||
|
||||
Ok(Config {
|
||||
file,
|
||||
inner: Arc::new(RwLock::new(inner)),
|
||||
})
|
||||
Ok(Config { file, inner })
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let cfg = &*self.inner.read().await;
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &cfg.accounts {
|
||||
let ctx = Context::new(
|
||||
cfg.os_name.clone(),
|
||||
account_config.dbfile().into(),
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
@@ -347,19 +391,18 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Create a new account in the given root directory.
|
||||
pub async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
async fn new_account(&mut self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
let id = {
|
||||
let inner = &mut self.inner.write().await;
|
||||
let id = inner.next_id;
|
||||
let id = self.inner.next_id;
|
||||
let uuid = Uuid::new_v4();
|
||||
let target_dir = dir.join(uuid.to_simple_ref().to_string());
|
||||
|
||||
inner.accounts.push(AccountConfig {
|
||||
self.inner.accounts.push(AccountConfig {
|
||||
id,
|
||||
dir: target_dir.into(),
|
||||
uuid,
|
||||
});
|
||||
inner.next_id += 1;
|
||||
self.inner.next_id += 1;
|
||||
id
|
||||
};
|
||||
|
||||
@@ -371,46 +414,39 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Removes an existing acccount entirely.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
if let Some(idx) = inner.accounts.iter().position(|e| e.id == id) {
|
||||
if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
|
||||
// remove account from the configs
|
||||
inner.accounts.remove(idx);
|
||||
self.inner.accounts.remove(idx);
|
||||
}
|
||||
if inner.selected_account == id {
|
||||
if self.inner.selected_account == id {
|
||||
// reset selected account
|
||||
inner.selected_account = inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
self.inner.selected_account =
|
||||
self.inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
self.sync().await
|
||||
}
|
||||
|
||||
pub async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.cloned()
|
||||
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner.accounts.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
self.inner.read().await.selected_account
|
||||
self.inner.selected_account
|
||||
}
|
||||
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
ensure!(
|
||||
inner.accounts.iter().any(|e| e.id == id),
|
||||
self.inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {}",
|
||||
id
|
||||
);
|
||||
|
||||
inner.selected_account = id;
|
||||
self.inner.selected_account = id;
|
||||
}
|
||||
|
||||
self.sync().await?;
|
||||
@@ -418,8 +454,9 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration of a single account.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct AccountConfig {
|
||||
struct AccountConfig {
|
||||
/// Unique id.
|
||||
pub id: u32,
|
||||
/// Root directory for all data for this account.
|
||||
@@ -443,21 +480,17 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1").into();
|
||||
|
||||
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(
|
||||
&*accounts1.config.inner.read().await,
|
||||
&*accounts2.config.inner.read().await,
|
||||
);
|
||||
assert_eq!(
|
||||
accounts1.accounts.read().await.len(),
|
||||
accounts2.accounts.read().await.len()
|
||||
);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -465,22 +498,47 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
assert_eq!(accounts.accounts.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_accounts_remove_last() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let id = accounts.add_account().await?;
|
||||
assert!(accounts.get_selected_account().await.is_some());
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
|
||||
accounts.remove_account(id).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -488,14 +546,12 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other").into();
|
||||
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
|
||||
.await
|
||||
.unwrap();
|
||||
let ctx = Context::new(extern_dbfile.clone(), 0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -506,10 +562,10 @@ mod tests {
|
||||
.migrate_account(extern_dbfile.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let ctx = accounts.get_selected_account().await;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
@@ -525,9 +581,9 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..10 {
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
@@ -537,4 +593,112 @@ mod tests {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
|
||||
.await?;
|
||||
|
||||
let id1 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id1).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
|
||||
.await?;
|
||||
|
||||
// add and remove some accounts and force a gap (ids must not be reused)
|
||||
for _ in 0..dummy_accounts {
|
||||
let to_delete = accounts.add_account().await?;
|
||||
accounts.remove_account(to_delete).await?;
|
||||
}
|
||||
|
||||
let id2 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
|
||||
.await?;
|
||||
|
||||
accounts.select_account(id1).await?;
|
||||
|
||||
(id0, id1, id2)
|
||||
};
|
||||
assert!(id0 > 0);
|
||||
assert!(id1 > id0);
|
||||
assert!(id2 > id1 + dummy_accounts);
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 3);
|
||||
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("one@example.org".to_string())
|
||||
);
|
||||
|
||||
let id1 = *ids.get(1).unwrap();
|
||||
let t = accounts.get_account(id1).await.unwrap();
|
||||
assert_eq!(
|
||||
t.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let id2 = *ids.get(2).unwrap();
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("three@example.org".to_string())
|
||||
);
|
||||
|
||||
(id0, id1, id2)
|
||||
};
|
||||
assert_eq!(id0, id0_reopened);
|
||||
assert_eq!(id1, id1_reopened);
|
||||
assert_eq!(id2, id2_reopened);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_no_accounts_event_emitter() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
// Create event emitter.
|
||||
let mut event_emitter = accounts.get_event_emitter().await;
|
||||
|
||||
// Test that event emitter does not return `None` immediately.
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
assert!(async_std::future::timeout(duration, event_emitter.recv())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// When account manager is dropped, event emitter is exhausted.
|
||||
drop(accounts);
|
||||
assert_eq!(event_emitter.recv().await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! # Autocrypt header module
|
||||
//! # Autocrypt header module.
|
||||
//!
|
||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||
|
||||
use anyhow::{bail, format_err, Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
@@ -37,13 +37,13 @@ impl fmt::Display for EncryptPreference {
|
||||
}
|
||||
|
||||
impl str::FromStr for EncryptPreference {
|
||||
type Err = ();
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
_ => Err(()),
|
||||
_ => bail!("Cannot parse encryption preference {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,28 +70,27 @@ impl Aheader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to parse Autocrypt header.
|
||||
///
|
||||
/// If there is none, returns None. If the header is present but cannot be parsed, returns an
|
||||
/// error.
|
||||
pub fn from_headers(
|
||||
context: &Context,
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Option<Self> {
|
||||
) -> Result<Option<Self>> {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Self::from_str(&value) {
|
||||
Ok(header) => {
|
||||
if addr_cmp(&header.addr, wanted_from) {
|
||||
return Some(header);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"found invalid autocrypt header {}: {:?}", value, err
|
||||
);
|
||||
}
|
||||
let header = Self::from_str(&value)?;
|
||||
if !addr_cmp(&header.addr, wanted_from) {
|
||||
bail!(
|
||||
"Autocrypt header address {:?} is not {:?}",
|
||||
header.addr,
|
||||
wanted_from
|
||||
);
|
||||
}
|
||||
Ok(Some(header))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +119,9 @@ impl fmt::Display for Aheader {
|
||||
}
|
||||
|
||||
impl str::FromStr for Aheader {
|
||||
type Err = ();
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut attributes: BTreeMap<String, String> = s
|
||||
.split(';')
|
||||
.filter_map(|a| {
|
||||
@@ -136,15 +135,20 @@ impl str::FromStr for Aheader {
|
||||
|
||||
let addr = match attributes.remove("addr") {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
return Err(());
|
||||
}
|
||||
None => bail!("Autocrypt header has no addr"),
|
||||
};
|
||||
let public_key: SignedPublicKey = attributes
|
||||
.remove("keydata")
|
||||
.ok_or(())
|
||||
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
|
||||
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
|
||||
.ok_or_else(|| format_err!("keydata attribute is not found"))
|
||||
.and_then(|raw| {
|
||||
SignedPublicKey::from_base64(&raw)
|
||||
.map_err(|_| format_err!("Autocrypt key cannot be decoded"))
|
||||
})
|
||||
.and_then(|key| {
|
||||
key.verify()
|
||||
.and(Ok(key))
|
||||
.map_err(|_| format_err!("Autocrypt key cannot be verified"))
|
||||
})?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
@@ -154,7 +158,7 @@ impl str::FromStr for Aheader {
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
return Err(());
|
||||
bail!("Unknown Autocrypt attribute found");
|
||||
}
|
||||
|
||||
Ok(Aheader {
|
||||
@@ -172,38 +176,40 @@ mod tests {
|
||||
const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
fn test_from_str() -> Result<()> {
|
||||
let h: Aheader = format!(
|
||||
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
|
||||
RAWKEY
|
||||
)
|
||||
.parse()
|
||||
.expect("failed to parse");
|
||||
.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
#[test]
|
||||
fn test_from_str_reset() {
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!(
|
||||
"addr=reset@example.com; prefer-encrypt=reset; keydata={}",
|
||||
RAWKEY
|
||||
);
|
||||
let h: Aheader = raw.parse().expect("failed to parse");
|
||||
let h: Aheader = raw.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "reset@example.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_non_critical() {
|
||||
fn test_from_str_non_critical() -> Result<()> {
|
||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", RAWKEY);
|
||||
let h: Aheader = raw.parse().expect("failed to parse");
|
||||
let h: Aheader = raw.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -216,7 +222,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_headers() {
|
||||
fn test_good_headers() -> Result<()> {
|
||||
let fixed_header = concat!(
|
||||
"addr=a@b.example.org; prefer-encrypt=mutual; ",
|
||||
"keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
|
||||
@@ -242,7 +248,7 @@ mod tests {
|
||||
" wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
|
||||
);
|
||||
|
||||
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
|
||||
let ah = Aheader::from_str(fixed_header)?;
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(format!("{}", ah), fixed_header);
|
||||
@@ -250,18 +256,17 @@ mod tests {
|
||||
let rendered = ah.to_string();
|
||||
assert_eq!(rendered, fixed_header);
|
||||
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY)).expect("failed to parse");
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY))?;
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Aheader::from_str(&format!(
|
||||
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
|
||||
RAWKEY
|
||||
))
|
||||
.expect("failed to parse");
|
||||
))?;
|
||||
|
||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))
|
||||
.expect("failed to parse");
|
||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
204
src/blob.rs
204
src/blob.rs
@@ -1,4 +1,4 @@
|
||||
//! # Blob directory management
|
||||
//! # Blob directory management.
|
||||
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
@@ -74,7 +74,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
// workaround a bug in async-std
|
||||
// (the executor does not handle blocking operation in Drop correctly,
|
||||
// see https://github.com/async-rs/async-std/issues/900 )
|
||||
// see <https://github.com/async-rs/async-std/issues/900>)
|
||||
let _ = file.flush().await;
|
||||
|
||||
let blob = BlobObject {
|
||||
@@ -452,7 +452,7 @@ impl<'a> BlobObject<'a> {
|
||||
img.write_to(encoded, image::ImageFormat::Jpeg)?;
|
||||
Ok(())
|
||||
}
|
||||
fn encode_img_exceeds_bytes(
|
||||
fn encoded_img_exceeds_bytes(
|
||||
context: &Context,
|
||||
img: &DynamicImage,
|
||||
max_bytes: Option<usize>,
|
||||
@@ -477,7 +477,7 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
|
||||
|
||||
let do_scale =
|
||||
exceeds_width || encode_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
|
||||
|
||||
if do_scale || do_rotate {
|
||||
@@ -500,7 +500,7 @@ impl<'a> BlobObject<'a> {
|
||||
loop {
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encode_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B",
|
||||
@@ -511,6 +511,10 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
} else {
|
||||
if encoded.is_empty() {
|
||||
encode_img(&new_img, &mut encoded)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px)",
|
||||
@@ -617,7 +621,14 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::{
|
||||
chat,
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
@@ -865,11 +876,10 @@ mod tests {
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -915,4 +925,182 @@ mod tests {
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_recode_image() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
|
||||
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
bytes,
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE,
|
||||
WORSE_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
bytes,
|
||||
2000,
|
||||
1800,
|
||||
270,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut bytes = vec![];
|
||||
img_rotated
|
||||
.write_to(&mut bytes, image::ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE * 1800 / 2000,
|
||||
WORSE_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
fn assert_correct_rotation(img: &DynamicImage) {
|
||||
// The test images are black in the bottom left corner after correctly applying
|
||||
// the EXIF orientation
|
||||
|
||||
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img
|
||||
.get_pixel(img.width() - 10, img.height() - 10)
|
||||
.to_luma()
|
||||
.0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
async fn send_image_check_mediaquality(
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
original_width: u32,
|
||||
original_height: u32,
|
||||
orientation: i32,
|
||||
compressed_width: u32,
|
||||
compressed_height: u32,
|
||||
) -> anyhow::Result<DynamicImage> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, media_quality_config)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file.jpg");
|
||||
|
||||
File::create(&file).await?.write_all(bytes).await?;
|
||||
let img = image::open(&file)?;
|
||||
assert_eq!(img.width(), original_width);
|
||||
assert_eq!(img.height(), original_height);
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
|
||||
|
||||
let img = image::open(file)?;
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
assert_eq!(prepared_id, msg.id);
|
||||
assert!(msg.is_increation());
|
||||
|
||||
let msg = Message::load_from_db(&t, prepared_id).await?;
|
||||
assert!(msg.is_increation());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_not_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
2093
src/chat.rs
2093
src/chat.rs
File diff suppressed because it is too large
Load Diff
231
src/chatlist.rs
231
src/chatlist.rs
@@ -1,19 +1,19 @@
|
||||
//! # Chat list module
|
||||
//! # Chat list module.
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::constants::{
|
||||
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
|
||||
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT,
|
||||
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CONTACT_ID_DEVICE,
|
||||
DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT, DC_GCL_ARCHIVED_ONLY,
|
||||
DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
|
||||
};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::stock_str;
|
||||
use crate::summary::Summary;
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
///
|
||||
@@ -34,15 +34,12 @@ use crate::stock_str;
|
||||
/// and for each messages that is scrolled into view, dc_get_msg() is called then.
|
||||
///
|
||||
/// Why no listflags?
|
||||
/// Without listflags, dc_get_chatlist() adds the deaddrop and the archive "link" automatically as needed.
|
||||
/// The UI can just render these items differently then. Although the deaddrop link is currently always the
|
||||
/// first entry and only present on new messages, there is the rough idea that it can be optionally always
|
||||
/// present and sorted into the list by date. Rendering the deaddrop in the described way
|
||||
/// would not add extra work in the UI then.
|
||||
/// Without listflags, dc_get_chatlist() adds the archive "link" automatically as needed.
|
||||
/// The UI can just render these items differently then.
|
||||
#[derive(Debug)]
|
||||
pub struct Chatlist {
|
||||
/// Stores pairs of `chat_id, message_id`
|
||||
ids: Vec<(ChatId, MsgId)>,
|
||||
ids: Vec<(ChatId, Option<MsgId>)>,
|
||||
}
|
||||
|
||||
impl Chatlist {
|
||||
@@ -58,12 +55,6 @@ impl Chatlist {
|
||||
///
|
||||
/// By default, the function adds some special entries to the list.
|
||||
/// These special entries can be identified by the ID returned by chatlist.get_chat_id():
|
||||
/// - DC_CHAT_ID_DEADDROP (1) - this special chat is present if there are
|
||||
/// messages from addresses that have no relationship to the configured account.
|
||||
/// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
|
||||
/// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
|
||||
/// and offers the options "Start chat", "Block" and "Not now";
|
||||
/// The decision should be passed to dc_decide_on_contact_request().
|
||||
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
||||
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
|
||||
/// "Show archived chats", if the user clicks this item, the UI should show a
|
||||
@@ -79,9 +70,9 @@ impl Chatlist {
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat,
|
||||
/// and hides the device-chat and contact requests
|
||||
/// 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, archive link is not added
|
||||
/// 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)
|
||||
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||
@@ -111,7 +102,7 @@ impl Chatlist {
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
||||
let msg_id: Option<MsgId> = row.get(1)?;
|
||||
Ok((chat_id, msg_id))
|
||||
};
|
||||
|
||||
@@ -136,13 +127,8 @@ impl Chatlist {
|
||||
// timestamp
|
||||
// - the list starts with the newest chats
|
||||
//
|
||||
// nb: the query currently shows messages from blocked
|
||||
// contacts in groups. however, for normal-groups, this is
|
||||
// okay as the message is also returned by dc_get_chat_msgs()
|
||||
// (otherwise it would be hard to follow conversations, wa and
|
||||
// tg do the same) for the deaddrop, however, they should
|
||||
// really be hidden, however, _currently_ the deaddrop is not
|
||||
// shown at all permanent in the chatlist.
|
||||
// The query shows messages from blocked contacts in
|
||||
// groups. Otherwise it would be hard to follow conversations.
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
@@ -157,7 +143,7 @@ impl Chatlist {
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
@@ -184,7 +170,7 @@ impl Chatlist {
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.blocked!=1
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
@@ -218,7 +204,7 @@ impl Chatlist {
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND c.blocked!=1
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
@@ -236,7 +222,7 @@ impl Chatlist {
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
let ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -248,22 +234,15 @@ impl Chatlist {
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?3
|
||||
AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
|
||||
AND NOT c.archived=?4
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) =
|
||||
get_last_deaddrop_fresh_msg(context).await?
|
||||
{
|
||||
if !flag_for_forwarding {
|
||||
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
|
||||
}
|
||||
}
|
||||
add_archived_link_item = true;
|
||||
}
|
||||
ids
|
||||
@@ -271,9 +250,9 @@ impl Chatlist {
|
||||
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
}
|
||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
|
||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, None));
|
||||
}
|
||||
|
||||
Ok(Chatlist { ids })
|
||||
@@ -302,91 +281,75 @@ impl Chatlist {
|
||||
/// Get a single message ID of a chatlist.
|
||||
///
|
||||
/// 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<Option<MsgId>> {
|
||||
match self.ids.get(index) {
|
||||
Some((_chat_id, msg_id)) => Ok(*msg_id),
|
||||
None => bail!("Chatlist index out of range"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a summary for a chatlist index.
|
||||
///
|
||||
/// The summary is returned by a dc_lot_t object with the following fields:
|
||||
///
|
||||
/// - dc_lot_t::text1: contains the username or the strings "Me", "Draft" and so on.
|
||||
/// The string may be colored by having a look at text1_meaning.
|
||||
/// If there is no such name or it should not be displayed, the element is NULL.
|
||||
/// - dc_lot_t::text1_meaning: one of DC_TEXT1_USERNAME, DC_TEXT1_SELF or DC_TEXT1_DRAFT.
|
||||
/// Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable.
|
||||
/// - dc_lot_t::text2: contains an excerpt of the message text or strings as
|
||||
/// "No messages". May be NULL of there is no such text (eg. for the archive link)
|
||||
/// - 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()).
|
||||
// 0 if not applicable.
|
||||
pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
|
||||
/// Returns a summary for a given chatlist index.
|
||||
pub async fn get_summary(
|
||||
&self,
|
||||
context: &Context,
|
||||
index: usize,
|
||||
chat: Option<&Chat>,
|
||||
) -> Result<Summary> {
|
||||
// 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
|
||||
// "is typing".
|
||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||
let (chat_id, lastmsg_id) = match self.ids.get(index) {
|
||||
Some(ids) => ids,
|
||||
None => {
|
||||
let mut ret = Lot::new();
|
||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||
return Lot::new();
|
||||
}
|
||||
None => bail!("Chatlist index out of range"),
|
||||
};
|
||||
|
||||
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
|
||||
}
|
||||
|
||||
/// Returns a summary for a given chatlist item.
|
||||
pub async fn get_summary2(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
lastmsg_id: MsgId,
|
||||
lastmsg_id: Option<MsgId>,
|
||||
chat: Option<&Chat>,
|
||||
) -> Lot {
|
||||
let mut ret = Lot::new();
|
||||
|
||||
) -> Result<Summary> {
|
||||
let chat_loaded: Chat;
|
||||
let chat = if let Some(chat) = chat {
|
||||
chat
|
||||
} else if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
|
||||
} else {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat_loaded = chat;
|
||||
&chat_loaded
|
||||
} else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let (lastmsg, lastcontact) =
|
||||
if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
|
||||
if lastmsg.from_id == DC_CONTACT_ID_SELF {
|
||||
(Some(lastmsg), None)
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
let lastcontact =
|
||||
Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||
(Some(lastmsg), lastcontact)
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
}
|
||||
}
|
||||
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
|
||||
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
|
||||
if lastmsg.from_id == DC_CONTACT_ID_SELF {
|
||||
(Some(lastmsg), None)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if chat.id.is_archived_link() {
|
||||
ret.text2 = None;
|
||||
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
||||
{
|
||||
ret.text2 = Some(stock_str::no_messages(context).await);
|
||||
Ok(Default::default())
|
||||
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) {
|
||||
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
|
||||
} else {
|
||||
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
|
||||
.await;
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context).await,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
|
||||
@@ -399,35 +362,13 @@ pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||
paramsv![],
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
|
||||
paramsv![Blocked::Yes, ChatVisibility::Archived],
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
|
||||
// We have an index over the state-column, this should be
|
||||
// sufficient as there are typically only few fresh messages.
|
||||
let id = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
),
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -435,8 +376,6 @@ mod tests {
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message;
|
||||
use crate::message::ContactRequestDecision;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
@@ -460,10 +399,20 @@ mod tests {
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2), chat_id1);
|
||||
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
// New drafts are sorted to the top
|
||||
// We have to set a draft on the other two messages, too, as
|
||||
// chat timestamps are only exact to the second and sorting by timestamp
|
||||
// would not work.
|
||||
// Message timestamps are "smeared" and unique, so we don't have this problem
|
||||
// if we have any message (can be a draft) in all chats.
|
||||
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
@@ -546,11 +495,11 @@ mod tests {
|
||||
async fn test_search_single_chat() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// receive a one-to-one-message, accept contact request
|
||||
// receive a one-to-one-message
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -558,21 +507,18 @@ mod tests {
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
// Contact request should be searchable
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.get_chat_id(), DC_CHAT_ID_DEADDROP);
|
||||
let chat_id = msg.get_chat_id();
|
||||
chat_id.accept(&t).await.unwrap();
|
||||
|
||||
let chat_id =
|
||||
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
|
||||
.await
|
||||
.unwrap();
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = *contacts.first().unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
@@ -610,11 +556,11 @@ mod tests {
|
||||
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// receive a one-to-one-message without authname set, accept contact request
|
||||
// receive a one-to-one-message without authname set
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg5678@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -622,16 +568,13 @@ mod tests {
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat_id =
|
||||
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = msg.get_chat_id();
|
||||
chat_id.accept(&t).await.unwrap();
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = *contacts.first().unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
@@ -684,7 +627,7 @@ mod tests {
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
let summary = chats.get_summary(&t, 0, None).await;
|
||||
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||
let summary = chats.get_summary(&t, 0, None).await.unwrap();
|
||||
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Implementation of Consistent Color Generation
|
||||
//! Implementation of Consistent Color Generation.
|
||||
//!
|
||||
//! Consistent Color Generation is defined in XEP-0392.
|
||||
//!
|
||||
@@ -35,6 +35,10 @@ pub(crate) fn str_to_color(s: &str) -> u32 {
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
}
|
||||
|
||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
||||
format!("{:#08x}", color).replace("0x", "#")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -42,7 +46,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_str_to_angle() {
|
||||
// Test against test vectors from
|
||||
// https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd
|
||||
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
|
||||
assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
|
||||
assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
|
||||
assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # Key-value configuration management
|
||||
//! # Key-value configuration management.
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{ensure, Result};
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
@@ -18,7 +18,18 @@ use crate::stock_str;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr, EnumIter, EnumProperty,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
EnumString,
|
||||
AsRefStr,
|
||||
EnumIter,
|
||||
EnumProperty,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Config {
|
||||
@@ -37,6 +48,12 @@ pub enum Config {
|
||||
SmtpCertificateChecks,
|
||||
ServerFlags,
|
||||
|
||||
Socks5Enabled,
|
||||
Socks5Host,
|
||||
Socks5Port,
|
||||
Socks5User,
|
||||
Socks5Password,
|
||||
|
||||
Displayname,
|
||||
Selfstatus,
|
||||
Selfavatar,
|
||||
@@ -50,15 +67,9 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
InboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
SentboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxMove,
|
||||
|
||||
@@ -139,6 +150,11 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// If a warning about exceeding quota was shown recently,
|
||||
/// this is the percentage of quota at the time the warning was given.
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
|
||||
@@ -148,6 +164,16 @@ pub enum Config {
|
||||
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
/// Send sync messages, requires `BccSelf` to be set as well.
|
||||
/// In a future versions, this switch may be removed.
|
||||
#[strum(props(default = "0"))]
|
||||
SendSyncMsgs,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -252,13 +278,13 @@ impl Context {
|
||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
self.emit_event(EventType::SelfavatarChanged);
|
||||
Ok(())
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
let def = stock_str::status_line(self).await;
|
||||
@@ -295,7 +321,7 @@ impl Context {
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
job::schedule_resync(self).await;
|
||||
job::schedule_resync(self).await?;
|
||||
ret
|
||||
}
|
||||
_ => {
|
||||
@@ -310,6 +336,21 @@ impl Context {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets an ui-specific key-value pair.
|
||||
/// Keys must be prefixed by `ui.`
|
||||
/// and should be followed by the name of the system and maybe subsystem,
|
||||
/// eg. `ui.desktop.linux.foo`, `ui.desktop.macos.bar`, `ui.ios.foobar`.
|
||||
pub async fn set_ui_config(&self, key: &str, value: Option<&str>) -> Result<()> {
|
||||
ensure!(key.starts_with("ui."), "set_ui_config(): prefix missing.");
|
||||
self.sql.set_raw_config(key, value).await
|
||||
}
|
||||
|
||||
/// Gets an ui-specific value set by set_ui_config().
|
||||
pub async fn get_ui_config(&self, key: &str) -> Result<Option<String>> {
|
||||
ensure!(key.starts_with("ui."), "get_ui_config(): prefix missing.");
|
||||
self.sql.get_raw_config(key).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all available configuration keys concated together.
|
||||
@@ -362,4 +403,25 @@ mod tests {
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Worse);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ui_config() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);
|
||||
|
||||
t.set_ui_config("ui.android.screen_security", Some("safe"))
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_ui_config("ui.android.screen_security").await?,
|
||||
Some("safe".to_string())
|
||||
);
|
||||
|
||||
t.set_ui_config("ui.android.screen_security", None).await?;
|
||||
assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);
|
||||
|
||||
assert!(t.set_ui_config("configured", Some("bar")).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
166
src/configure.rs
166
src/configure.rs
@@ -1,4 +1,4 @@
|
||||
//! Email accounts autoconfiguration process module
|
||||
//! Email accounts autoconfiguration process module.
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
@@ -8,13 +8,12 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use itertools::Itertools;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::imap::Imap;
|
||||
use crate::login_param::{LoginParam, ServerLoginParam};
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::dc_get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
@@ -170,12 +169,17 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
DC_LP_AUTH_NORMAL as i32
|
||||
};
|
||||
|
||||
let socks5_config = param.socks5_config.clone();
|
||||
let socks5_enabled = socks5_config.is_some();
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
// Step 1: Load the parameters and check email-address and password
|
||||
|
||||
if oauth2 {
|
||||
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
|
||||
// socks5 requests, this can work with socks5 too
|
||||
if oauth2 && !socks5_enabled {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
@@ -217,7 +221,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m_domain).await {
|
||||
if let Some(provider) =
|
||||
provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await
|
||||
{
|
||||
param.provider = Some(provider);
|
||||
match provider.status {
|
||||
provider::Status::Ok | provider::Status::Preparation => {
|
||||
@@ -244,6 +250,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
}
|
||||
},
|
||||
strict_tls: Some(provider.strict_tls),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -256,9 +263,16 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try receiving autoconfig
|
||||
info!(ctx, "no offline autoconfig found");
|
||||
param_autoconfig =
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await;
|
||||
param_autoconfig = if socks5_enabled {
|
||||
// Currently we can't do http requests through socks5, to not leak
|
||||
// the ip, just don't do online autoconfig
|
||||
info!(ctx, "socks5 enabled, skipping autoconfig");
|
||||
None
|
||||
} else {
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
param_autoconfig = None;
|
||||
@@ -277,6 +291,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
port: param.imap.port,
|
||||
socket: param.imap.security,
|
||||
username: param.imap.user.clone(),
|
||||
strict_tls: None,
|
||||
})
|
||||
}
|
||||
if !servers
|
||||
@@ -289,8 +304,24 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
port: param.smtp.port,
|
||||
socket: param.smtp.security,
|
||||
username: param.smtp.user.clone(),
|
||||
strict_tls: None,
|
||||
})
|
||||
}
|
||||
|
||||
// respect certificate setting from function parameters
|
||||
for mut server in &mut servers {
|
||||
let certificate_checks = match server.protocol {
|
||||
Protocol::Imap => param.imap.certificate_checks,
|
||||
Protocol::Smtp => param.smtp.certificate_checks,
|
||||
};
|
||||
server.strict_tls = match certificate_checks {
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::Automatic => server.strict_tls,
|
||||
};
|
||||
}
|
||||
|
||||
let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
|
||||
|
||||
progress!(ctx, 550);
|
||||
@@ -306,7 +337,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
.filter(|params| params.protocol == Protocol::Smtp)
|
||||
.cloned()
|
||||
.collect();
|
||||
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
|
||||
let provider_strict_tls = param
|
||||
.provider
|
||||
.map_or(socks5_config.is_some(), |provider| provider.strict_tls);
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp_configured = false;
|
||||
@@ -316,10 +349,16 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
smtp_param.server = smtp_server.hostname.clone();
|
||||
smtp_param.port = smtp_server.port;
|
||||
smtp_param.security = smtp_server.socket;
|
||||
smtp_param.certificate_checks = match smtp_server.strict_tls {
|
||||
Some(true) => CertificateChecks::Strict,
|
||||
Some(false) => CertificateChecks::AcceptInvalidCertificates,
|
||||
None => CertificateChecks::Automatic,
|
||||
};
|
||||
|
||||
match try_smtp_one_param(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&socks5_config,
|
||||
&smtp_addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
@@ -345,10 +384,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
let (_s, r) = async_std::channel::bounded(1);
|
||||
let mut imap = Imap::new(r);
|
||||
|
||||
let mut imap_configured = false;
|
||||
let mut imap: Option<Imap> = None;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::Imap)
|
||||
@@ -360,19 +397,24 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
param.imap.server = imap_server.hostname.clone();
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
param.imap.certificate_checks = match imap_server.strict_tls {
|
||||
Some(true) => CertificateChecks::Strict,
|
||||
Some(false) => CertificateChecks::AcceptInvalidCertificates,
|
||||
None => CertificateChecks::Automatic,
|
||||
};
|
||||
|
||||
match try_imap_one_param(
|
||||
ctx,
|
||||
¶m.imap,
|
||||
¶m.socks5_config,
|
||||
¶m.addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
&mut imap,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
imap_configured = true;
|
||||
Ok(configured_imap) => {
|
||||
imap = Some(configured_imap);
|
||||
break;
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
@@ -382,9 +424,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
if !imap_configured {
|
||||
bail!(nicer_configuration_error(ctx, errors).await);
|
||||
}
|
||||
let mut imap = match imap {
|
||||
Some(imap) => imap,
|
||||
None => bail!(nicer_configuration_error(ctx, errors).await),
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
@@ -400,8 +443,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
@@ -429,7 +471,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
ctx,
|
||||
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await?;
|
||||
@@ -463,7 +505,7 @@ async fn get_autoconfig(
|
||||
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
|
||||
&format!(
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
¶m_domain, ¶m_addr_urlencoded
|
||||
@@ -517,48 +559,88 @@ async fn get_autoconfig(
|
||||
async fn try_imap_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
imap: &mut Imap,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
) -> Result<Imap, ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = imap
|
||||
.connect(context, param, addr, oauth2, provider_strict_tls)
|
||||
.await
|
||||
let (_s, r) = async_std::channel::bounded(1);
|
||||
|
||||
let mut imap = match Imap::new(
|
||||
param,
|
||||
socks5_config.clone(),
|
||||
addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
r,
|
||||
)
|
||||
.await
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
Ok(())
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
return Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(imap) => imap,
|
||||
};
|
||||
|
||||
match imap.connect(context).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
})
|
||||
}
|
||||
Ok(()) => {
|
||||
info!(context, "success: {}", inf);
|
||||
Ok(imap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_smtp_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
} else {
|
||||
"None".to_string()
|
||||
}
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp
|
||||
.connect(context, param, addr, oauth2, provider_strict_tls)
|
||||
.connect(
|
||||
context,
|
||||
param,
|
||||
socks5_config,
|
||||
addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
)
|
||||
.await
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
@@ -600,7 +682,11 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
return first_err.msg.to_string();
|
||||
}
|
||||
|
||||
errors.iter().map(|e| e.to_string()).join("\n\n")
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -616,10 +702,10 @@ pub enum Error {
|
||||
},
|
||||
|
||||
#[error("Failed to get URL: {0}")]
|
||||
ReadUrlError(#[from] self::read_url::Error),
|
||||
ReadUrl(#[from] self::read_url::Error),
|
||||
|
||||
#[error("Number of redirection is exceeded")]
|
||||
RedirectionError,
|
||||
Redirection,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # 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::events::{BytesStart, Event};
|
||||
|
||||
use std::io::BufRead;
|
||||
@@ -243,6 +243,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
|
||||
hostname: server.hostname,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
strict_tls: None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -15,27 +15,27 @@ use super::{Error, ServerParams};
|
||||
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
|
||||
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox>
|
||||
#[derive(Debug)]
|
||||
struct ProtocolTag {
|
||||
/// Server type, such as "IMAP", "SMTP" or "POP3".
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
|
||||
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox>
|
||||
pub typ: String,
|
||||
|
||||
/// Server identifier, hostname or IP address for IMAP and SMTP.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
|
||||
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox>
|
||||
pub server: String,
|
||||
|
||||
/// Network port.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
|
||||
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox>
|
||||
pub port: u16,
|
||||
|
||||
/// Whether connection should be secure, "on" or "off", default is "on".
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
|
||||
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox>
|
||||
pub ssl: bool,
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
|
||||
hostname: protocol.server,
|
||||
port: protocol.port,
|
||||
username: String::new(),
|
||||
strict_tls: None,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -203,7 +204,7 @@ pub(crate) async fn outlk_autodiscover(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Error::RedirectionError)
|
||||
Err(Error::Redirection)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -22,19 +22,26 @@ pub(crate) struct ServerParams {
|
||||
|
||||
/// Username, empty if unknown.
|
||||
pub username: String,
|
||||
|
||||
/// Whether TLS certificates should be strictly checked or not, `None` for automatic.
|
||||
pub strict_tls: Option<bool>,
|
||||
}
|
||||
|
||||
impl ServerParams {
|
||||
fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
|
||||
fn expand_usernames(self, addr: &str) -> Vec<ServerParams> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
if self.username.is_empty() {
|
||||
self.username = addr.to_string();
|
||||
res.push(self.clone());
|
||||
res.push(Self {
|
||||
username: addr.to_string(),
|
||||
..self.clone()
|
||||
});
|
||||
|
||||
if let Some(at) = addr.find('@') {
|
||||
self.username = addr.split_at(at).0.to_string();
|
||||
res.push(self);
|
||||
res.push(Self {
|
||||
username: addr.split_at(at).0.to_string(),
|
||||
..self
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.push(self)
|
||||
@@ -42,24 +49,28 @@ impl ServerParams {
|
||||
res
|
||||
}
|
||||
|
||||
fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
|
||||
let mut res = Vec::new();
|
||||
fn expand_hostnames(self, param_domain: &str) -> Vec<ServerParams> {
|
||||
if self.hostname.is_empty() {
|
||||
self.hostname = param_domain.to_string();
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = match self.protocol {
|
||||
Protocol::Imap => "imap.".to_string() + param_domain,
|
||||
Protocol::Smtp => "smtp.".to_string() + param_domain,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = "mail.".to_string() + param_domain;
|
||||
res.push(self);
|
||||
vec![
|
||||
Self {
|
||||
hostname: param_domain.to_string(),
|
||||
..self.clone()
|
||||
},
|
||||
Self {
|
||||
hostname: match self.protocol {
|
||||
Protocol::Imap => "imap.".to_string() + param_domain,
|
||||
Protocol::Smtp => "smtp.".to_string() + param_domain,
|
||||
},
|
||||
..self.clone()
|
||||
},
|
||||
Self {
|
||||
hostname: "mail.".to_string() + param_domain,
|
||||
..self
|
||||
},
|
||||
]
|
||||
} else {
|
||||
res.push(self);
|
||||
vec![self]
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn expand_ports(mut self) -> Vec<ServerParams> {
|
||||
@@ -78,39 +89,64 @@ impl ServerParams {
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = Vec::new();
|
||||
if self.port == 0 {
|
||||
// Neither port nor security is set.
|
||||
//
|
||||
// Try common secure combinations.
|
||||
|
||||
// Try STARTTLS
|
||||
self.socket = Socket::Starttls;
|
||||
self.port = match self.protocol {
|
||||
Protocol::Imap => 143,
|
||||
Protocol::Smtp => 587,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
// Try TLS
|
||||
self.socket = Socket::Ssl;
|
||||
self.port = match self.protocol {
|
||||
Protocol::Imap => 993,
|
||||
Protocol::Smtp => 465,
|
||||
};
|
||||
res.push(self);
|
||||
vec![
|
||||
// Try STARTTLS
|
||||
Self {
|
||||
socket: Socket::Starttls,
|
||||
port: match self.protocol {
|
||||
Protocol::Imap => 143,
|
||||
Protocol::Smtp => 587,
|
||||
},
|
||||
..self.clone()
|
||||
},
|
||||
// Try TLS
|
||||
Self {
|
||||
socket: Socket::Ssl,
|
||||
port: match self.protocol {
|
||||
Protocol::Imap => 993,
|
||||
Protocol::Smtp => 465,
|
||||
},
|
||||
..self
|
||||
},
|
||||
]
|
||||
} else if self.socket == Socket::Automatic {
|
||||
// Try TLS over user-provided port.
|
||||
self.socket = Socket::Ssl;
|
||||
res.push(self.clone());
|
||||
|
||||
// Try STARTTLS over user-provided port.
|
||||
self.socket = Socket::Starttls;
|
||||
res.push(self);
|
||||
vec![
|
||||
// Try TLS over user-provided port.
|
||||
Self {
|
||||
socket: Socket::Ssl,
|
||||
..self.clone()
|
||||
},
|
||||
// Try STARTTLS over user-provided port.
|
||||
Self {
|
||||
socket: Socket::Starttls,
|
||||
..self
|
||||
},
|
||||
]
|
||||
} else {
|
||||
res.push(self);
|
||||
vec![self]
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_strict_tls(self) -> Vec<ServerParams> {
|
||||
if self.strict_tls.is_none() {
|
||||
vec![
|
||||
Self {
|
||||
strict_tls: Some(true), // Strict.
|
||||
..self.clone()
|
||||
},
|
||||
Self {
|
||||
strict_tls: None, // Automatic.
|
||||
..self
|
||||
},
|
||||
]
|
||||
} else {
|
||||
vec![self]
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +158,32 @@ pub(crate) fn expand_param_vector(
|
||||
domain: &str,
|
||||
) -> Vec<ServerParams> {
|
||||
v.into_iter()
|
||||
// The order of expansion is important: ports are expanded the
|
||||
// last, so they are changed the first. Username is only
|
||||
// changed if default value (address with domain) didn't work
|
||||
// for all available hosts and ports.
|
||||
.map(|params| {
|
||||
if params.socket == Socket::Plain {
|
||||
ServerParams {
|
||||
// Avoid expanding plaintext configuration into configuration with and without
|
||||
// `strict_tls` if `strict_tls` is set to `None` as `strict_tls` is not used for
|
||||
// plaintext connections. Always setting it to "enabled", just in case.
|
||||
strict_tls: Some(true),
|
||||
..params
|
||||
}
|
||||
} else {
|
||||
params
|
||||
}
|
||||
})
|
||||
// The order of expansion is important.
|
||||
//
|
||||
// Ports are expanded the last, so they are changed the first. Username is only changed if
|
||||
// default value (address with domain) didn't work for all available hosts and ports.
|
||||
//
|
||||
// Strict TLS must be expanded first, so we try all configurations with strict TLS first
|
||||
// and only then try again without strict TLS. Otherwise we may lock to wrong hostname
|
||||
// without strict TLS when another hostname with strict TLS is available. For example, if
|
||||
// both smtp.example.net and mail.example.net are running an SMTP server, but both use a
|
||||
// certificate that is only valid for mail.example.net, we want to skip smtp.example.net
|
||||
// and use mail.example.net with strict TLS instead of using smtp.example.net without
|
||||
// strict TLS.
|
||||
.flat_map(|params| params.expand_strict_tls().into_iter())
|
||||
.flat_map(|params| params.expand_usernames(addr).into_iter())
|
||||
.flat_map(|params| params.expand_hostnames(domain).into_iter())
|
||||
.flat_map(|params| params.expand_ports().into_iter())
|
||||
@@ -145,6 +203,7 @@ mod tests {
|
||||
port: 0,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: Some(true),
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
@@ -158,6 +217,7 @@ mod tests {
|
||||
port: 993,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: Some(true)
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -168,6 +228,7 @@ mod tests {
|
||||
port: 123,
|
||||
socket: Socket::Automatic,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None,
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
@@ -181,16 +242,59 @@ mod tests {
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string()
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: Some(true),
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Starttls,
|
||||
username: "foobar".to_string()
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: Some(true)
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None,
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Starttls,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
// Test that strict_tls is not expanded for plaintext connections.
|
||||
let v = expand_param_vector(
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Plain,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: None,
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
);
|
||||
assert_eq!(
|
||||
v,
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 123,
|
||||
socket: Socket::Plain,
|
||||
username: "foobar".to_string(),
|
||||
strict_tls: Some(true)
|
||||
}],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! # Constants
|
||||
//! # Constants.
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -24,8 +24,8 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
|
||||
#[repr(i8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Manually = 1,
|
||||
Deaddrop = 2,
|
||||
Yes = 1,
|
||||
Request = 2,
|
||||
}
|
||||
|
||||
impl Default for Blocked {
|
||||
@@ -123,8 +123,6 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
// do not use too small value that will annoy users checking for nonexistant updates.
|
||||
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
|
||||
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
||||
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1);
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
|
||||
/// only an indicator in a chatlist
|
||||
@@ -155,6 +153,7 @@ pub enum Chattype {
|
||||
Single = 100,
|
||||
Group = 120,
|
||||
Mailinglist = 140,
|
||||
Broadcast = 160,
|
||||
}
|
||||
|
||||
impl Default for Chattype {
|
||||
@@ -167,36 +166,18 @@ pub const DC_MSG_ID_MARKER1: u32 = 1;
|
||||
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
/// string that indicates sth. is left out or truncated
|
||||
pub const DC_ELLIPSE: &str = "[...]";
|
||||
/// String that indicates that something is left out or truncated.
|
||||
pub const DC_ELLIPSIS: &str = "[...]";
|
||||
|
||||
/// to keep bubbles and chat flow usable,
|
||||
/// and to avoid problems with controls using very long texts,
|
||||
/// we limit the text length to DC_DESIRED_TEXT_LEN.
|
||||
/// if the text is longer, the full text can be retrieved usind has_html()/get_html().
|
||||
/// Message length limit.
|
||||
///
|
||||
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
|
||||
/// (a bit less as truncation may not be exact and ellipses may be added).
|
||||
/// To keep bubbles and chat flow usable and to avoid problems with controls using very long texts,
|
||||
/// we limit the text length to `DC_DESIRED_TEXT_LEN`. If the text is longer, the full text can be
|
||||
/// retrieved using has_html()/get_html().
|
||||
///
|
||||
/// note, that DC_DESIRED_TEXT_LEN and DC_MAX_GET_TEXT_LEN
|
||||
/// define max. number of bytes, _not_ unicode graphemes.
|
||||
/// in general, that seems to be okay for such an upper limit,
|
||||
/// esp. as calculating the number of graphemes is not simple
|
||||
/// (one graphemes may be a sequence of code points which is a sequence of bytes).
|
||||
/// also even if we have the exact number of graphemes,
|
||||
/// that would not always help on getting an idea about the screen space used
|
||||
/// (to keep bubbles and chat flow usable).
|
||||
///
|
||||
/// therefore, the number of bytes is only a very rough estimation,
|
||||
/// however, the ~30K seems to work okayish for a while,
|
||||
/// if it turns out, it is too few for some alphabet, we can still increase.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = 29_000;
|
||||
|
||||
/// approx. max. length (number of bytes) returned by dc_msg_get_text()
|
||||
pub const DC_MAX_GET_TEXT_LEN: usize = 30_000;
|
||||
|
||||
/// approx. max. length returned by dc_get_msg_info()
|
||||
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
|
||||
/// Note that for simplicity maximum length is defined as the number of Unicode Scalar Values (Rust
|
||||
/// `char`s), not Unicode Grapheme Clusters.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
|
||||
|
||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
||||
@@ -368,6 +349,7 @@ mod tests {
|
||||
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
||||
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
|
||||
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
|
||||
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -403,8 +385,8 @@ mod tests {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Blocked::Not, Blocked::default());
|
||||
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
|
||||
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Deaddrop, Blocked::from_i32(2).unwrap());
|
||||
assert_eq!(Blocked::Yes, Blocked::from_i32(1).unwrap());
|
||||
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
468
src/contact.rs
468
src/contact.rs
@@ -2,10 +2,9 @@
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
@@ -14,8 +13,8 @@ use crate::chat::ChatId;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR,
|
||||
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
|
||||
Blocked, Chattype, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR, DC_CONTACT_ID_LAST_SPECIAL,
|
||||
DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
|
||||
@@ -67,6 +66,9 @@ pub struct Contact {
|
||||
/// Blocked state. Use dc_contact_is_blocked to access this field.
|
||||
pub blocked: bool,
|
||||
|
||||
/// Time when the contact was seen last time, Unix time in seconds.
|
||||
last_seen: i64,
|
||||
|
||||
/// The origin/source of the contact.
|
||||
pub origin: Origin,
|
||||
|
||||
@@ -174,12 +176,19 @@ pub enum VerifiedStatus {
|
||||
BidirectVerified = 2,
|
||||
}
|
||||
|
||||
impl Default for VerifiedStatus {
|
||||
fn default() -> Self {
|
||||
Self::Unverified
|
||||
}
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
|
||||
let mut contact = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
@@ -188,15 +197,17 @@ impl Contact {
|
||||
let addr: String = row.get(1)?;
|
||||
let origin: Origin = row.get(2)?;
|
||||
let blocked: Option<bool> = row.get(3)?;
|
||||
let authname: String = row.get(4)?;
|
||||
let param: String = row.get(5)?;
|
||||
let status: Option<String> = row.get(6)?;
|
||||
let last_seen: i64 = row.get(4)?;
|
||||
let authname: String = row.get(5)?;
|
||||
let param: String = row.get(6)?;
|
||||
let status: Option<String> = row.get(7)?;
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name,
|
||||
authname,
|
||||
addr,
|
||||
blocked: blocked.unwrap_or_default(),
|
||||
last_seen,
|
||||
origin,
|
||||
param: param.parse().unwrap_or_default(),
|
||||
status: status.unwrap_or_default(),
|
||||
@@ -218,6 +229,7 @@ impl Contact {
|
||||
} else if contact_id == DC_CONTACT_ID_DEVICE {
|
||||
contact.name = stock_str::device_messages(context).await;
|
||||
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
|
||||
contact.status = stock_str::device_messages_hint(context).await;
|
||||
}
|
||||
Ok(contact)
|
||||
}
|
||||
@@ -227,22 +239,25 @@ impl Contact {
|
||||
self.blocked
|
||||
}
|
||||
|
||||
/// Returns last seen timestamp.
|
||||
pub fn last_seen(&self) -> i64 {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: u32) -> bool {
|
||||
Self::load_from_db(context, id)
|
||||
.await
|
||||
.map(|contact| contact.blocked)
|
||||
.unwrap_or_default()
|
||||
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
Ok(blocked)
|
||||
}
|
||||
|
||||
/// Block the given contact.
|
||||
pub async fn block(context: &Context, id: u32) {
|
||||
set_block_contact(context, id, true).await;
|
||||
pub async fn block(context: &Context, id: u32) -> Result<()> {
|
||||
set_block_contact(context, id, true).await
|
||||
}
|
||||
|
||||
/// Unblock the given contact.
|
||||
pub async fn unblock(context: &Context, id: u32) {
|
||||
set_block_contact(context, id, false).await;
|
||||
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
|
||||
set_block_contact(context, id, false).await
|
||||
}
|
||||
|
||||
/// Add a single contact as a result of an _explicit_ user action.
|
||||
@@ -262,7 +277,7 @@ impl Contact {
|
||||
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?;
|
||||
let blocked = Contact::is_blocked_load(context, contact_id).await;
|
||||
let blocked = Contact::is_blocked_load(context, contact_id).await?;
|
||||
match sth_modified {
|
||||
Modifier::None => {}
|
||||
Modifier::Modified | Modifier::Created => {
|
||||
@@ -270,27 +285,22 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
if blocked {
|
||||
Contact::unblock(context, contact_id).await;
|
||||
Contact::unblock(context, contact_id).await?;
|
||||
}
|
||||
|
||||
Ok(contact_id)
|
||||
}
|
||||
|
||||
/// Mark messages from a contact as noticed.
|
||||
/// The contact is expected to belong to the deaddrop,
|
||||
/// therefore, DC_EVENT_MSGS_NOTICED(DC_CHAT_ID_DEADDROP) is emitted.
|
||||
pub async fn mark_noticed(context: &Context, id: u32) {
|
||||
if context
|
||||
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_DEADDROP));
|
||||
}
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked contact.
|
||||
@@ -301,17 +311,17 @@ impl Contact {
|
||||
/// use `dc_may_be_valid_addr()`.
|
||||
pub async fn lookup_id_by_addr(
|
||||
context: &Context,
|
||||
addr: impl AsRef<str>,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
) -> Result<Option<u32>> {
|
||||
if addr.as_ref().is_empty() {
|
||||
if addr.is_empty() {
|
||||
bail!("lookup_id_by_addr: empty address");
|
||||
}
|
||||
|
||||
let addr_normalized = addr_normalize(addr.as_ref());
|
||||
let addr_normalized = addr_normalize(addr);
|
||||
|
||||
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
if addr_cmp(addr_normalized, &addr_self) {
|
||||
return Ok(Some(DC_CONTACT_ID_SELF));
|
||||
}
|
||||
}
|
||||
@@ -373,7 +383,7 @@ impl Contact {
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(&addr, addr_self) {
|
||||
if addr_cmp(&addr, &addr_self) {
|
||||
return Ok((DC_CONTACT_ID_SELF, sth_modified));
|
||||
}
|
||||
|
||||
@@ -572,7 +582,7 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book).into_iter() {
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let name = normalize_name(name);
|
||||
let name = normalize_name(&name);
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -711,7 +721,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
|
||||
paramsv![Chattype::Mailinglist, Blocked::Manually],
|
||||
paramsv![Chattype::Mailinglist, Blocked::Yes],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -758,12 +768,9 @@ impl Contact {
|
||||
|
||||
/// Get blocked contacts.
|
||||
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
|
||||
if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot update blocked mailinglist contacts: {:?}", e
|
||||
);
|
||||
}
|
||||
Contact::update_blocked_mailinglist_contacts(context)
|
||||
.await
|
||||
.context("cannot update blocked mailinglist contacts")?;
|
||||
|
||||
let list = context
|
||||
.sql
|
||||
@@ -860,7 +867,7 @@ impl Contact {
|
||||
"Can not delete special contact"
|
||||
);
|
||||
|
||||
let count_contacts = context
|
||||
let count_chats = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
@@ -868,19 +875,7 @@ impl Contact {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let count_msgs = if count_contacts > 0 {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||
paramsv![contact_id as i32, contact_id as i32],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if count_msgs == 0 {
|
||||
if count_chats == 0 {
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -902,9 +897,9 @@ impl Contact {
|
||||
|
||||
info!(
|
||||
context,
|
||||
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
|
||||
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
|
||||
);
|
||||
bail!("Could not delete contact with messages in it");
|
||||
bail!("Could not delete contact with ongoing chats");
|
||||
}
|
||||
|
||||
/// Get a single contact object. For a list, see eg. dc_get_contacts().
|
||||
@@ -1034,7 +1029,7 @@ impl Contact {
|
||||
///
|
||||
/// The UI may draw a checkbox or something like that beside verified contacts.
|
||||
///
|
||||
pub async fn is_verified(&self, context: &Context) -> VerifiedStatus {
|
||||
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
|
||||
self.is_verified_ex(context, None).await
|
||||
}
|
||||
|
||||
@@ -1045,54 +1040,46 @@ impl Contact {
|
||||
&self,
|
||||
context: &Context,
|
||||
peerstate: Option<&Peerstate>,
|
||||
) -> VerifiedStatus {
|
||||
) -> Result<VerifiedStatus> {
|
||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||
// on this device
|
||||
if self.id == DC_CONTACT_ID_SELF {
|
||||
return VerifiedStatus::BidirectVerified;
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return VerifiedStatus::BidirectVerified;
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
}
|
||||
|
||||
let peerstate = match Peerstate::from_addr(context, &self.addr).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to load peerstate for address {}: {}", self.addr, err
|
||||
);
|
||||
return VerifiedStatus::Unverified;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ps) = peerstate {
|
||||
if ps.verified_key.is_some() {
|
||||
return VerifiedStatus::BidirectVerified;
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
}
|
||||
|
||||
VerifiedStatus::Unverified
|
||||
Ok(VerifiedStatus::Unverified)
|
||||
}
|
||||
|
||||
pub async fn addr_equals_contact(context: &Context, addr: &str, contact_id: u32) -> bool {
|
||||
pub async fn addr_equals_contact(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
contact_id: u32,
|
||||
) -> Result<bool> {
|
||||
if addr.is_empty() {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if !contact.addr.is_empty() {
|
||||
let normalized_addr = addr_normalize(addr);
|
||||
if contact.addr == normalized_addr {
|
||||
return true;
|
||||
}
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
if !contact.addr.is_empty() {
|
||||
let normalized_addr = addr_normalize(addr);
|
||||
if contact.addr == normalized_addr {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
@@ -1110,30 +1097,34 @@ impl Contact {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
|
||||
if !context.sql.is_open().await || contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return false;
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> Result<bool> {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
context
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub async fn scaleup_origin_by_id(context: &Context, contact_id: u32, origin: Origin) -> bool {
|
||||
pub async fn scaleup_origin_by_id(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id as i32, origin],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,56 +1165,60 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return;
|
||||
}
|
||||
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
);
|
||||
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if contact.blocked != new_blocking
|
||||
&& context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
||||
// non-destructive blocking->unblocking.
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
|
||||
if contact.blocked != new_blocking {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
||||
// non-destructive blocking->unblocking.
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET blocked=?
|
||||
WHERE type=? AND id IN (
|
||||
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
||||
);
|
||||
"#,
|
||||
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await?;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await
|
||||
{
|
||||
chat_id.set_blocked(context, Blocked::Not).await;
|
||||
}
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock(context).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set profile image for a contact.
|
||||
@@ -1305,14 +1300,35 @@ pub(crate) async fn set_status(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
|
||||
pub(crate) async fn update_last_seen(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can not update special contact last seen timestamp"
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
||||
let full_name = full_name.as_ref().trim();
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
@@ -1355,44 +1371,45 @@ fn cat_fingerprint(
|
||||
impl Context {
|
||||
/// determine whether the specified addr maps to the/a self addr
|
||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
let self_addr = self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
if let Some(self_addr) = self.get_config(Config::ConfiguredAddr).await? {
|
||||
Ok(addr_cmp(&self_addr, addr))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2).to_lowercase();
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
book.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|mut chunk| {
|
||||
let name = chunk.next().unwrap();
|
||||
let addr = match chunk.next() {
|
||||
Some(a) => a,
|
||||
None => return None,
|
||||
};
|
||||
Some((name, addr))
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.get(0)?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_std::fs::File;
|
||||
use async_std::io::WriteExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_may_be_valid_addr() {
|
||||
@@ -1400,12 +1417,12 @@ mod tests {
|
||||
assert_eq!(may_be_valid_addr("user@domain.tld"), true);
|
||||
assert_eq!(may_be_valid_addr("uuu"), false);
|
||||
assert_eq!(may_be_valid_addr("dd.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("tt.dd@uu"), false);
|
||||
assert_eq!(may_be_valid_addr("u@d"), false);
|
||||
assert_eq!(may_be_valid_addr("u@d."), false);
|
||||
assert_eq!(may_be_valid_addr("u@d.t"), false);
|
||||
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d."), true);
|
||||
assert_eq!(may_be_valid_addr("u@d.t"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d.tt"), true);
|
||||
assert_eq!(may_be_valid_addr("u@.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("u@.tt"), true);
|
||||
assert_eq!(may_be_valid_addr("@d.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("<da@d.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
|
||||
@@ -1507,11 +1524,11 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_is_self_addr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.is_self_addr("me@me.org").await.is_err());
|
||||
|
||||
let addr = t.configure_alice().await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
assert_eq!(t.is_self_addr(&addr).await?, true);
|
||||
|
||||
t.configure_addr("you@you.net").await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
assert_eq!(t.is_self_addr("you@you.net").await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1618,6 +1635,34 @@ mod tests {
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
assert!(Contact::delete(&alice, DC_CONTACT_ID_SELF).await.is_err());
|
||||
|
||||
// Create Bob contact
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
|
||||
// Can't delete a contact with ongoing chats.
|
||||
assert!(Contact::delete(&alice, contact_id).await.is_err());
|
||||
|
||||
// Delete chat.
|
||||
chat.get_id().delete(&alice).await?;
|
||||
|
||||
// Can delete contact now.
|
||||
Contact::delete(&alice, contact_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_remote_authnames() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -1821,9 +1866,6 @@ mod tests {
|
||||
assert!(Contact::create(&t, "", "dskjfdslk@sadklj.dk>")
|
||||
.await
|
||||
.is_err());
|
||||
assert!(Contact::create(&t, "", "dskjf@dslk@sadkljdk")
|
||||
.await
|
||||
.is_err());
|
||||
assert!(Contact::create(&t, "", "dskjf dslk@d.e").await.is_err());
|
||||
assert!(Contact::create(&t, "", "<dskjf dslk@sadklj.dk")
|
||||
.await
|
||||
@@ -1849,7 +1891,7 @@ mod tests {
|
||||
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.com", Origin::Unknown)
|
||||
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(id, Some(DC_CONTACT_ID_SELF));
|
||||
@@ -1874,7 +1916,7 @@ mod tests {
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?;
|
||||
let msg = bob.pop_sent_msg().await;
|
||||
@@ -1886,7 +1928,7 @@ mod tests {
|
||||
"End-to-end encryption preferred.
|
||||
Fingerprints:
|
||||
|
||||
alice@example.com:
|
||||
alice@example.org:
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
@@ -1937,7 +1979,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
// Bob replies.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
@@ -1963,4 +2005,94 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
// Alice has two devices.
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
|
||||
|
||||
let avatar_src = alice1.get_blobdir().join("avatar.png");
|
||||
File::create(&avatar_src)
|
||||
.await?
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.await?;
|
||||
|
||||
alice1
|
||||
.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await?;
|
||||
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.await;
|
||||
|
||||
// Bob sends a message so that Alice can encrypt to him.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice sends a message.
|
||||
let alice1_chat_id = alice1.get_last_msg().await.chat_id;
|
||||
alice1_chat_id.accept(&alice1).await?;
|
||||
send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// The message is encrypted.
|
||||
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||
assert!(message.get_showpadlock());
|
||||
|
||||
// Alice's second device receives a copy of the outgoing message.
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice's second device applies the selfavatar.
|
||||
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_last_seen() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
let mime = br#"Subject: Hello
|
||||
Message-ID: message@example.net
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Chat-Version: 1.0
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000
|
||||
|
||||
Hi."#;
|
||||
dc_receive_imf(&alice, mime, "Inbox", false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
assert!(timestamp > 0);
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Context module
|
||||
//! Context module.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
@@ -22,6 +22,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::securejoin::Bob;
|
||||
use crate::sql::Sql;
|
||||
@@ -46,7 +47,6 @@ pub struct InnerContext {
|
||||
/// Blob directory path
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) os_name: Option<String>,
|
||||
pub(crate) bob: Bob,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
@@ -62,6 +62,10 @@ pub struct InnerContext {
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
|
||||
|
||||
/// Recently loaded quota information, if any.
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
|
||||
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
|
||||
|
||||
/// ID for this `Context` in the current process.
|
||||
@@ -71,6 +75,11 @@ pub struct InnerContext {
|
||||
pub(crate) id: u32,
|
||||
|
||||
creation_time: SystemTime,
|
||||
|
||||
/// The text of the last error logged and emitted as an event.
|
||||
/// If the ui wants to display an error after a failure,
|
||||
/// `last_error` should be used to avoid races with the event thread.
|
||||
pub(crate) last_error: RwLock<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -98,7 +107,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(os_name: String, dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
@@ -108,11 +117,10 @@ impl Context {
|
||||
if !blobdir.exists().await {
|
||||
async_std::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
Context::with_blobdir(os_name, dbfile, blobdir, id).await
|
||||
Context::with_blobdir(dbfile, blobdir, id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn with_blobdir(
|
||||
os_name: String,
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
@@ -127,7 +135,6 @@ impl Context {
|
||||
id,
|
||||
blobdir,
|
||||
dbfile,
|
||||
os_name: Some(os_name),
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(),
|
||||
bob: Default::default(),
|
||||
@@ -139,8 +146,10 @@ impl Context {
|
||||
events: Events::default(),
|
||||
scheduler: RwLock::new(Scheduler::Stopped),
|
||||
ephemeral_task: RwLock::new(None),
|
||||
quota: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: RwLock::new("".to_string()),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -161,7 +170,9 @@ impl Context {
|
||||
|
||||
{
|
||||
let l = &mut *self.inner.scheduler.write().await;
|
||||
l.start(self.clone()).await;
|
||||
if let Err(err) = l.start(self.clone()).await {
|
||||
error!(self, "Failed to start IO: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,10 +288,11 @@ impl Context {
|
||||
let l2 = LoginParam::from_database(self, "configured_").await?;
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await? as usize;
|
||||
let real_msgs = message::get_real_msg_cnt(self).await as usize;
|
||||
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize;
|
||||
let request_msgs = message::get_request_msg_cnt(self).await as usize;
|
||||
let contacts = Contact::get_real_cnt(self).await? as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured).await?;
|
||||
let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?;
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int("dbversion")
|
||||
@@ -294,6 +306,7 @@ impl Context {
|
||||
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 send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||
|
||||
let prv_key_cnt = self
|
||||
.sql
|
||||
@@ -309,9 +322,7 @@ impl Context {
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
|
||||
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
|
||||
let folders_configured = self
|
||||
@@ -334,8 +345,8 @@ impl Context {
|
||||
// insert values
|
||||
res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
|
||||
res.insert("number_of_chats", chats.to_string());
|
||||
res.insert("number_of_chat_messages", real_msgs.to_string());
|
||||
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
|
||||
res.insert("number_of_chat_messages", unblocked_msgs.to_string());
|
||||
res.insert("messages_in_contact_requests", request_msgs.to_string());
|
||||
res.insert("number_of_contacts", contacts.to_string());
|
||||
res.insert("database_dir", self.get_dbfile().display().to_string());
|
||||
res.insert("database_version", dbversion.to_string());
|
||||
@@ -349,6 +360,7 @@ impl Context {
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
res.insert("socks5_enabled", socks5_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2.to_string());
|
||||
res.insert(
|
||||
@@ -361,9 +373,13 @@ impl Context {
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert("inbox_watch", inbox_watch.to_string());
|
||||
res.insert(
|
||||
"download_limit",
|
||||
self.get_config_int(Config::DownloadLimit)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_watch", mvbox_watch.to_string());
|
||||
res.insert("mvbox_move", mvbox_move.to_string());
|
||||
res.insert("sentbox_move", sentbox_move.to_string());
|
||||
res.insert("folders_configured", folders_configured.to_string());
|
||||
@@ -376,6 +392,7 @@ impl Context {
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("send_sync_msgs", send_sync_msgs.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
@@ -413,6 +430,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"quota_exceeding",
|
||||
self.get_config_int(Config::QuotaExceeding)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
@@ -420,7 +443,7 @@ impl Context {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
|
||||
/// Get a list of fresh, unmuted messages in unblocked chats.
|
||||
///
|
||||
/// The list starts with the most recent message
|
||||
/// and is typically used to show notifications.
|
||||
@@ -538,19 +561,16 @@ impl Context {
|
||||
|
||||
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
||||
|
||||
Ok(sentbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
|
||||
Ok(mvbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
|
||||
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
|
||||
|
||||
Ok(spam.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
@@ -560,6 +580,13 @@ impl Context {
|
||||
blob_fname.push("-blobs");
|
||||
dbfile.with_file_name(blob_fname)
|
||||
}
|
||||
|
||||
pub fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
|
||||
let mut wal_fname = OsString::new();
|
||||
wal_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
wal_fname.push("-wal");
|
||||
dbfile.with_file_name(wal_fname)
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
@@ -619,7 +646,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
std::fs::write(&dbfile, b"123").unwrap();
|
||||
let res = Context::new("FakeOs".into(), dbfile.into(), 1).await;
|
||||
let res = Context::new(dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -637,7 +664,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -647,7 +674,7 @@ mod tests {
|
||||
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
|
||||
);
|
||||
println!("{}", msg);
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false)
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -770,9 +797,7 @@ mod tests {
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
Context::new(dbfile.into(), 1).await.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
@@ -783,7 +808,7 @@ mod tests {
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
std::fs::write(&blobdir, b"123").unwrap();
|
||||
let res = Context::new("FakeOS".into(), dbfile.into(), 1).await;
|
||||
let res = Context::new(dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -793,9 +818,7 @@ mod tests {
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
Context::new(dbfile.into(), 1).await.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
@@ -805,7 +828,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
|
||||
let res = Context::with_blobdir(dbfile.into(), blobdir, 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -814,7 +837,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -866,6 +889,10 @@ mod tests {
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"smtp_certificate_checks",
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
"socks5_user",
|
||||
"socks5_password",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
215
src/dc_tools.rs
215
src/dc_tools.rs
@@ -5,6 +5,7 @@ use core::cmp::{max, min};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::str::from_utf8;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -14,10 +15,13 @@ use async_std::{fs, io};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use chrono::{Local, TimeZone};
|
||||
use mailparse::dateparse;
|
||||
use mailparse::headers::Headers;
|
||||
use mailparse::MailHeaderMap;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::chat::{add_device_msg, add_device_msg_with_importance};
|
||||
use crate::constants::{Viewtype, DC_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
|
||||
use crate::constants::{Viewtype, DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::Message;
|
||||
@@ -29,7 +33,7 @@ use crate::stock_str;
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let count = buf.chars().count();
|
||||
if approx_chars > 0 && count > approx_chars + DC_ELLIPSE.len() {
|
||||
if count > approx_chars + DC_ELLIPSIS.len() {
|
||||
let end_pos = buf
|
||||
.char_indices()
|
||||
.nth(approx_chars)
|
||||
@@ -37,9 +41,9 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
|
||||
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSE))
|
||||
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
|
||||
} else {
|
||||
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSE))
|
||||
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(buf)
|
||||
@@ -84,9 +88,9 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||
// but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
|
||||
const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
||||
|
||||
// returns the currently smeared timestamp,
|
||||
// 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!
|
||||
/// Returns the current smeared timestamp,
|
||||
///
|
||||
/// The returned timestamp MUST NOT be sent out.
|
||||
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||
let mut now = time();
|
||||
let ts = *context.last_smeared_timestamp.read().await;
|
||||
@@ -97,7 +101,7 @@ pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||
now
|
||||
}
|
||||
|
||||
// returns a timestamp that is guaranteed to be unique.
|
||||
/// Returns a timestamp that is guaranteed to be unique.
|
||||
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
||||
let now = time();
|
||||
let mut ret = now;
|
||||
@@ -608,19 +612,8 @@ impl FromStr for EmailAddress {
|
||||
if local.is_empty() {
|
||||
return err("empty string is not valid for local part");
|
||||
}
|
||||
if domain.len() <= 3 {
|
||||
return err("domain is too short");
|
||||
}
|
||||
let dot = domain.find('.');
|
||||
match dot {
|
||||
None => {
|
||||
return err("invalid domain");
|
||||
}
|
||||
Some(dot_idx) => {
|
||||
if dot_idx >= domain.len() - 2 {
|
||||
return err("invalid domain");
|
||||
}
|
||||
}
|
||||
if domain.is_empty() {
|
||||
return err("missing domain after '@'");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
@@ -666,7 +659,7 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
|
||||
0
|
||||
} else {
|
||||
// "Antw:" is the longest abbreviation in
|
||||
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
|
||||
// <https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages>,
|
||||
// so look at the first _5_ characters:
|
||||
match last_subject.chars().take(5).position(|c| c == ':') {
|
||||
Some(prefix_end) => prefix_end + 1,
|
||||
@@ -681,13 +674,144 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// Types and methods to create hop-info for message-info
|
||||
|
||||
fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Option<&'a str> {
|
||||
let header_len = header.len();
|
||||
header.find(start).and_then(|mut begin| {
|
||||
begin += start.len();
|
||||
let end = header
|
||||
.get(begin..)?
|
||||
.find(|c: char| c.is_whitespace())
|
||||
.unwrap_or(header_len);
|
||||
header.get(begin..begin + end)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_receive_header(header: &str) -> String {
|
||||
let header = header.replace(&['\r', '\n'][..], "");
|
||||
let mut hop_info = String::from("Hop: ");
|
||||
|
||||
if let Some(from) = extract_address_from_receive_header(&header, "from ") {
|
||||
hop_info += &format!("From: {}; ", from.trim());
|
||||
}
|
||||
|
||||
if let Some(by) = extract_address_from_receive_header(&header, "by ") {
|
||||
hop_info += &format!("By: {}; ", by.trim());
|
||||
}
|
||||
|
||||
if let Ok(date) = dateparse(&header) {
|
||||
// In tests, use the UTC timezone so that the test is reproducible
|
||||
#[cfg(test)]
|
||||
let date_obj = chrono::Utc.timestamp(date, 0);
|
||||
#[cfg(not(test))]
|
||||
let date_obj = Local.timestamp(date, 0);
|
||||
|
||||
hop_info += &format!("Date: {}", date_obj.to_rfc2822());
|
||||
};
|
||||
|
||||
hop_info
|
||||
}
|
||||
|
||||
/// parses "receive"-headers
|
||||
pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
|
||||
headers
|
||||
.get_all_headers("Received")
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|header_map_item| from_utf8(header_map_item.get_value_raw()).ok())
|
||||
.map(parse_receive_header)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::{
|
||||
config::Config, dc_receive_imf::dc_receive_imf, message::get_msg_info,
|
||||
test_utils::TestContext,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_receive_headers() {
|
||||
// Test `parse_receive_headers()` with some more-or-less random emails from the test-data
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let expected =
|
||||
"Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/wrong-html.eml");
|
||||
let expected =
|
||||
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 06 Aug 2020 16:40:31 +0000\n\
|
||||
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 06 Aug 2020 16:40:32 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/posteo_ndn.eml");
|
||||
let expected =
|
||||
"Hop: By: mout01.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
|
||||
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
|
||||
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 09 Jun 2020 18:44:24 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
}
|
||||
|
||||
fn check_parse_receive_headers(raw: &[u8], expected: &str) {
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
assert_eq!(hop_info, expected)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_receive_headers_integration() {
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let expected = r"State: Fresh
|
||||
|
||||
hi
|
||||
|
||||
Message-ID: 2dfdbde7@example.org
|
||||
|
||||
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml");
|
||||
let expected = "State: Fresh, Encrypted
|
||||
|
||||
Re: Message from alice@example.org
|
||||
|
||||
hi back\r\n\
|
||||
\r\n\
|
||||
-- \r\n\
|
||||
Sent with my Delta Chat Messenger: https://delta.chat
|
||||
|
||||
Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
|
||||
|
||||
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
|
||||
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
}
|
||||
|
||||
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
|
||||
dc_receive_imf(&t, raw, "INBOX", false).await.unwrap();
|
||||
let msg = t.get_last_msg().await;
|
||||
let msg_info = get_msg_info(&t, msg.id).await.unwrap();
|
||||
|
||||
// Ignore the first rows of the msg_info because they contain a
|
||||
// received time that depends on the test time which makes it impossible to
|
||||
// compare with a static string
|
||||
let capped_result = &msg_info[msg_info.find("State").unwrap()..];
|
||||
assert_eq!(expected, capped_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rust_ftoa() {
|
||||
@@ -722,10 +846,7 @@ mod tests {
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
|
||||
|
||||
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
|
||||
assert_eq!(
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
|
||||
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
|
||||
);
|
||||
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]");
|
||||
|
||||
// 9 characters, so no truncation
|
||||
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
|
||||
@@ -825,12 +946,19 @@ mod tests {
|
||||
domain: "domain.tld".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
"user@localhost".parse::<EmailAddress>().unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "localhost".into()
|
||||
}
|
||||
);
|
||||
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
|
||||
assert!("tt.dd@uu".parse::<EmailAddress>().is_ok());
|
||||
assert!("u@d".parse::<EmailAddress>().is_ok());
|
||||
assert!("u@d.".parse::<EmailAddress>().is_ok());
|
||||
assert!("u@d.t".parse::<EmailAddress>().is_ok());
|
||||
assert_eq!(
|
||||
"u@d.tt".parse::<EmailAddress>().unwrap(),
|
||||
EmailAddress {
|
||||
@@ -838,12 +966,12 @@ mod tests {
|
||||
domain: "d.tt".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
|
||||
assert!("u@tt".parse::<EmailAddress>().is_ok());
|
||||
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
|
||||
}
|
||||
|
||||
use crate::chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::{chat, test_utils};
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use proptest::prelude::*;
|
||||
|
||||
@@ -851,22 +979,18 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate(
|
||||
buf: String,
|
||||
approx_chars in 0..10000usize
|
||||
approx_chars in 0..100usize
|
||||
) {
|
||||
let res = dc_truncate(&buf, approx_chars);
|
||||
let el_len = 5;
|
||||
let l = res.chars().count();
|
||||
if approx_chars > 0 {
|
||||
assert!(
|
||||
l <= approx_chars + el_len,
|
||||
"buf: '{}' - res: '{}' - len {}, approx {}",
|
||||
&buf, &res, res.len(), approx_chars
|
||||
);
|
||||
} else {
|
||||
assert_eq!(&res, &buf);
|
||||
}
|
||||
assert!(
|
||||
l <= approx_chars + el_len,
|
||||
"buf: '{}' - res: '{}' - len {}, approx {}",
|
||||
&buf, &res, res.len(), approx_chars
|
||||
);
|
||||
|
||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||
if buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
@@ -993,8 +1117,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_filemeta() {
|
||||
let data = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
let (w, h) = dc_get_filemeta(data).unwrap();
|
||||
let (w, h) = dc_get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap();
|
||||
assert_eq!(w, 900);
|
||||
assert_eq!(h, 900);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! De-HTML
|
||||
//! De-HTML.
|
||||
//!
|
||||
//! A module to remove HTML tags from the email text
|
||||
|
||||
@@ -36,9 +36,9 @@ impl Dehtml {
|
||||
""
|
||||
}
|
||||
}
|
||||
fn append_prefix(&self, line_end: impl AsRef<str>) -> String {
|
||||
fn append_prefix(&self, line_end: &str) -> String {
|
||||
// line_end is e.g. "\n\n". We add "> " if necessary.
|
||||
line_end.as_ref().to_owned() + self.line_prefix()
|
||||
line_end.to_string() + self.line_prefix()
|
||||
}
|
||||
fn get_add_text(&self) -> AddText {
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
|
||||
396
src/download.rs
Normal file
396
src/download.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
//! # Download large messages manually.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::job::{self, Action, Job, Status};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::param::Params;
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
use std::cmp::max;
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// need to be downloaded completely to handle them correctly,
|
||||
/// eg. to assign them to the correct chat.
|
||||
/// As these messages are typically small,
|
||||
/// they're catched by `MIN_DOWNLOAD_LIMIT`.
|
||||
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
/// the user might have no chance to actually download that message.
|
||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum DownloadState {
|
||||
Done = 0,
|
||||
Available = 10,
|
||||
Failure = 20,
|
||||
InProgress = 1000,
|
||||
}
|
||||
|
||||
impl Default for DownloadState {
|
||||
fn default() -> Self {
|
||||
DownloadState::Done
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
// Returns validated download limit or `None` for "no limit".
|
||||
pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {
|
||||
let download_limit = self.get_config_int(Config::DownloadLimit).await?;
|
||||
if download_limit <= 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Schedules full message download for partially downloaded message.
|
||||
pub async fn download_full(self, context: &Context) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
match msg.download_state() {
|
||||
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
|
||||
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
|
||||
DownloadState::Available | DownloadState::Failure => {
|
||||
self.update_download_state(context, DownloadState::InProgress)
|
||||
.await?;
|
||||
job::add(
|
||||
context,
|
||||
Job::new(Action::DownloadMsg, self.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_download_state(
|
||||
self,
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET download_state=? WHERE id=?;",
|
||||
paramsv![download_state, self],
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: self,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Returns the download state of the message.
|
||||
pub fn download_state(&self) -> DownloadState {
|
||||
self.download_state
|
||||
}
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Actually download a message.
|
||||
/// Called in response to `Action::DownloadMsg`.
|
||||
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "download: could not connect: {:?}", err);
|
||||
return Status::RetryNow;
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
let row = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
paramsv![msg.rfc724_mid],
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
Ok((server_uid, server_folder))
|
||||
}
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, server_uid)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
/// Download a single message and pipe it to receive_imf().
|
||||
///
|
||||
/// receive_imf() is not directly aware that this is a result of a call to download_msg(),
|
||||
/// however, implicitly knows that as the existing message is flagged as being partly.
|
||||
async fn fetch_single_msg(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
) -> ImapActionResult {
|
||||
if let Some(imapresult) = self
|
||||
.prepare_imap_operation_on_msg(context, folder, uid)
|
||||
.await
|
||||
{
|
||||
return imapresult;
|
||||
}
|
||||
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
let (last_uid, _received) = self
|
||||
.fetch_many_msgs(context, folder, vec![uid], false, false)
|
||||
.await;
|
||||
if last_uid.is_none() {
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
ImapActionResult::Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeMessage {
|
||||
/// Creates a placeholder part and add that to `parts`.
|
||||
///
|
||||
/// To create the placeholder, only the outermost header can be used,
|
||||
/// the mime-structure itself is not available.
|
||||
///
|
||||
/// The placeholder part currently contains a text with size and availability of the message;
|
||||
/// in the future, we may do more advanced things as previews here.
|
||||
pub(crate) async fn create_stub_from_partial_download(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
org_bytes: u32,
|
||||
) -> Result<()> {
|
||||
let mut text = format!(
|
||||
"[{}]",
|
||||
stock_str::partial_download_msg_body(context, org_bytes).await
|
||||
);
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
|
||||
let until = stock_str::download_availability(
|
||||
context,
|
||||
time() + max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
)
|
||||
.await;
|
||||
text += format!(" [{}]", until).as_str();
|
||||
};
|
||||
|
||||
info!(context, "Partial download: {}", text);
|
||||
|
||||
self.parts.push(Part {
|
||||
typ: Viewtype::Text,
|
||||
msg: text,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::send_msg;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf_inner;
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::test_utils::TestContext;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(DownloadState::Done, DownloadState::default());
|
||||
assert_eq!(DownloadState::Done, DownloadState::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
DownloadState::Available,
|
||||
DownloadState::from_i32(10).unwrap()
|
||||
);
|
||||
assert_eq!(DownloadState::Failure, DownloadState::from_i32(20).unwrap());
|
||||
assert_eq!(
|
||||
DownloadState::InProgress,
|
||||
DownloadState::from_i32(1000).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_download_limit() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
|
||||
t.set_config(Config::DownloadLimit, Some("200000")).await?;
|
||||
assert_eq!(t.download_limit().await?, Some(200000));
|
||||
|
||||
t.set_config(Config::DownloadLimit, Some("20000")).await?;
|
||||
assert_eq!(t.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
|
||||
t.set_config(Config::DownloadLimit, None).await?;
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
|
||||
for val in &["0", "-1", "-100", "", "foo"] {
|
||||
t.set_config(Config::DownloadLimit, Some(val)).await?;
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_update_download_state() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("Hi Bob".to_owned()));
|
||||
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
|
||||
for s in &[
|
||||
DownloadState::Available,
|
||||
DownloadState::InProgress,
|
||||
DownloadState::Failure,
|
||||
DownloadState::Done,
|
||||
] {
|
||||
msg_id.update_download_state(&t, *s).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), *s);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_partial_receive_imf() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let header =
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <Mr.12345678901@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
dc_receive_imf_inner(&t, header.as_bytes(), "INBOX", false, Some(100000), false).await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert!(msg
|
||||
.get_text()
|
||||
.unwrap()
|
||||
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
|
||||
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
format!("{}\n\n100k text...", header).as_bytes(),
|
||||
"INBOX",
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert_eq!(msg.get_text(), Some("100k text...".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_partial_download_and_ephemeral() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = t
|
||||
.create_chat_with_contact("bob", "bob@example.org")
|
||||
.await
|
||||
.id;
|
||||
chat_id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
// download message from bob partially, this must not change the ephemeral timer
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain",
|
||||
"INBOX",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&t).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
117
src/e2ee.rs
117
src/e2ee.rs
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use anyhow::{bail, format_err, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -161,24 +161,29 @@ pub async fn try_decrypt(
|
||||
let mut peerstate = Peerstate::from_addr(context, &from).await?;
|
||||
|
||||
// Apply Autocrypt header
|
||||
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
match Aheader::from_headers(&from, &mail.headers) {
|
||||
Ok(Some(ref header)) => {
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => warn!(context, "Failed to parse Autocrypt header: {}", err),
|
||||
}
|
||||
|
||||
// Possibly perform decryption
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.handle_fingerprint_change(context).await?;
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, message_time)
|
||||
.await?;
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
@@ -186,14 +191,17 @@ pub async fn try_decrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let out_mail = decrypt_if_autocrypt_message(
|
||||
let (out_mail, signatures) = match decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
&mut signatures,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
@@ -269,8 +277,7 @@ async fn decrypt_if_autocrypt_message(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
|
||||
None => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
@@ -284,36 +291,60 @@ async fn decrypt_if_autocrypt_message(
|
||||
encrypted_data_part,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
ret_valid_signatures,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
|
||||
///
|
||||
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
|
||||
/// fingerprints for which there is a valid signature.
|
||||
async fn validate_detached_signature(
|
||||
mail: &ParsedMail<'_>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
// First part is the content, second part is the signature.
|
||||
let content = first_part.raw_bytes;
|
||||
let signature = second_part.get_body_raw()?;
|
||||
let ret_valid_signatures =
|
||||
pgp::pk_validate(content, &signature, public_keyring_for_validate).await?;
|
||||
|
||||
Ok(Some((content.to_vec(), ret_valid_signatures)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ok(None) if nothing encrypted was found.
|
||||
async fn decrypt_part(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
if has_decrypted_pgp_armor(&data) {
|
||||
// we should only have one decryption happening
|
||||
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
|
||||
let (plain, ret_valid_signatures) =
|
||||
pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?;
|
||||
|
||||
let plain = pgp::pk_decrypt(
|
||||
data,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
Some(ret_valid_signatures),
|
||||
)
|
||||
.await?;
|
||||
// Check for detached signatures.
|
||||
// If decrypted part is a multipart/signed, then there is a detached signature.
|
||||
let decrypted_part = mailparse::parse_mail(&plain)?;
|
||||
if let Some((content, valid_detached_signatures)) =
|
||||
validate_detached_signature(&decrypted_part, &public_keyring_for_validate).await?
|
||||
{
|
||||
return Ok(Some((content, valid_detached_signatures)));
|
||||
} else {
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check the signatures then.
|
||||
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check the signatures then.
|
||||
|
||||
return Ok(Some(plain));
|
||||
return Ok(Some((plain, ret_valid_signatures)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -333,7 +364,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a MIME structure contains a multipart/report part.
|
||||
/// Checks if a MIME structure contains a multipart/report part.
|
||||
///
|
||||
/// As reports are often unencrypted, we do not reset the Autocrypt header in
|
||||
/// this case.
|
||||
@@ -385,9 +416,11 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_prexisting() {
|
||||
let t = TestContext::new().await;
|
||||
let test_addr = t.configure_alice().await;
|
||||
assert_eq!(ensure_secret_key_exists(&t).await.unwrap(), test_addr);
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
ensure_secret_key_exists(&t).await.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -458,7 +491,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
assert!(!msg.was_encrypted());
|
||||
|
||||
// Parsing a message is enough to update peerstate
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
@@ -489,28 +522,28 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message with Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
@@ -518,7 +551,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
410
src/ephemeral.rs
410
src/ephemeral.rs
@@ -1,4 +1,4 @@
|
||||
//! # Ephemeral messages
|
||||
//! # Ephemeral messages.
|
||||
//!
|
||||
//! Ephemeral messages are messages that have an Ephemeral-Timer
|
||||
//! header attached to them, which specifies time in seconds after
|
||||
@@ -52,7 +52,7 @@
|
||||
//! `MsgsChanged` event is emitted when a message deletion is due, to
|
||||
//! make UI reload displayed messages and cause actual deletion.
|
||||
//!
|
||||
//! Server deletion happens by generating IMAP deletion jobs based on
|
||||
//! Server deletion happens by updating the `imap` table based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
@@ -71,11 +71,12 @@ use crate::constants::{
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
@@ -261,7 +262,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
@@ -277,9 +278,9 @@ impl MsgId {
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time() + i64::from(duration);
|
||||
let ephemeral_timestamp = time().saturating_add(duration.into());
|
||||
|
||||
context
|
||||
.sql
|
||||
@@ -416,58 +417,52 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
let context1 = context.clone();
|
||||
let ephemeral_task = task::spawn(async move {
|
||||
async_std::task::sleep(duration).await;
|
||||
emit_event!(
|
||||
context1,
|
||||
EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
context1.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
});
|
||||
*context.ephemeral_task.write().await = Some(ephemeral_task);
|
||||
} else {
|
||||
// Emit event immediately
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns ID of any expired message that should be deleted from the server.
|
||||
///
|
||||
/// It looks up the trash chat too, to find messages that are already
|
||||
/// deleted locally, but not deleted on the server.
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
|
||||
/// Schedules expired IMAP messages for deletion.
|
||||
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
let threshold_timestamp = match context.get_config_delete_server_after().await? {
|
||||
None => 0,
|
||||
Some(delete_server_after) => now - delete_server_after,
|
||||
};
|
||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
||||
match context.get_config_delete_server_after().await? {
|
||||
None => (0, 0),
|
||||
Some(delete_server_after) => (
|
||||
now - delete_server_after,
|
||||
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
),
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
timestamp < ? \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
||||
LIMIT 1",
|
||||
paramsv![threshold_timestamp, now, job::Action::DeleteMsgOnImap],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE EXISTS (
|
||||
SELECT * FROM msgs
|
||||
WHERE rfc724_mid=imap.rfc724_mid
|
||||
AND ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
@@ -502,10 +497,10 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::param::Params;
|
||||
use async_std::task::sleep;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::download::DownloadState;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::{
|
||||
chat::{self, Chat, ChatItem},
|
||||
@@ -641,8 +636,83 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_timer() -> anyhow::Result<()> {
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_enable_lost() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice enables the timer.
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
// The message enabling the timer is lost.
|
||||
let _sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled,
|
||||
);
|
||||
|
||||
// Alice sends a text message.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives text message and enables the timer, even though explicit timer update was
|
||||
// lost previously.
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
@@ -700,18 +770,30 @@ mod tests {
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob disables the chat timer.
|
||||
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
||||
// but the chat timer is enabled.
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_delete_msgs() {
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
|
||||
t.send_text(chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(chat.id).await;
|
||||
msg.id.delete_from_db(&t).await.unwrap();
|
||||
msg.id.delete_from_db(&t).await?;
|
||||
check_msg_was_deleted(&t, &chat, msg.id).await;
|
||||
|
||||
chat.id
|
||||
@@ -722,36 +804,12 @@ mod tests {
|
||||
.send_text(chat.id, "Saved message, disappearing after 1s")
|
||||
.await;
|
||||
|
||||
sleep(Duration::from_millis(1100)).await;
|
||||
async_std::task::sleep(Duration::from_millis(1100)).await;
|
||||
|
||||
// Check checks that the msg was deleted locally
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
|
||||
|
||||
// Check that the msg will be deleted on the server
|
||||
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_uid=1 WHERE id=?",
|
||||
paramsv![msg.sender_msg_id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let job = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(
|
||||
job,
|
||||
Some(job::Job::new(
|
||||
job::Action::DeleteMsgOnImap,
|
||||
msg.sender_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
))
|
||||
);
|
||||
// Let's assume that executing the job fails on first try and the job is saved to the db
|
||||
job.unwrap().save(&t).await.unwrap();
|
||||
|
||||
// Make sure that we don't get yet another job when loading from db
|
||||
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(job2, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
@@ -777,4 +835,202 @@ mod tests {
|
||||
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, message_id, timestamp, ephemeral_timestamp],
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
paramsv![message_id, id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=folder WHERE rfc724_mid='1000'",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=folder WHERE rfc724_mid='1010'",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Message with Message-ID <first@example.com> and no timer is received.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
// Message with Message-ID <second@example.com> is received.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||
Ephemeral-Timer: 60\n\
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted from the database when its timer expires.
|
||||
msg.id.delete_from_db(&alice).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@examle.com>. Because
|
||||
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
||||
// changed explicitly and the change should be ignored.
|
||||
//
|
||||
// The message also contains a quote of the first message to test that only References:
|
||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\n\
|
||||
\n\
|
||||
> hello\n",
|
||||
"INBOX",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! # Events specification
|
||||
//! # Events specification.
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
@@ -185,21 +185,6 @@ pub enum EventType {
|
||||
#[strum(props(id = "400"))]
|
||||
Error(String),
|
||||
|
||||
/// An action cannot be performed because there is no network available.
|
||||
///
|
||||
/// The library will typically try over after a some time
|
||||
/// and when dc_maybe_network() is called.
|
||||
///
|
||||
/// Network errors should be reported to users in a non-disturbing way,
|
||||
/// however, as network errors may come in a sequence,
|
||||
/// it is not useful to raise each an every error to the user.
|
||||
///
|
||||
/// Moreover, if the UI detects that the device is offline,
|
||||
/// it is probably more useful to report this to the user
|
||||
/// instead of the string from data2.
|
||||
#[strum(props(id = "401"))]
|
||||
ErrorNetwork(String),
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
@@ -214,7 +199,8 @@ pub enum EventType {
|
||||
/// - Chats created, deleted or archived
|
||||
/// - A draft has been set
|
||||
///
|
||||
/// The `chat_id` and `msg_id` values will be 0 if more than one message is changed.
|
||||
/// `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
/// `msg_id` is set if only a single message is affected by the changes, otherwise 0.
|
||||
#[strum(props(id = "2000"))]
|
||||
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
|
||||
|
||||
@@ -330,4 +316,14 @@ pub enum EventType {
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
|
||||
/// dc_get_connectivity_html() for details.
|
||||
#[strum(props(id = "2100"))]
|
||||
ConnectivityChanged,
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
}
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
//! Export chats module
|
||||
//!
|
||||
//! ## Export Format
|
||||
//! The format of an exported chat is a zip file with the following structure:
|
||||
//! ```text
|
||||
//! ├── blobs/ # all files that are referenced by the chat
|
||||
//! ├── msg_info/
|
||||
//! │ └── [msg_id].txt # message info
|
||||
//! ├── msg_source/
|
||||
//! │ └── [msg_id].eml # email sourcecode of messages if availible¹
|
||||
//! └── chat.json # chat info, messages and message authors
|
||||
//! ```
|
||||
//! ##### ¹ Saving Mime header
|
||||
//! To save the mime header you need to have the config option [`SaveMimeHeaders`] enabled.
|
||||
//! This option saves the mime headers on future messages. Normaly the original email source code is discarded to save space.
|
||||
//! You can use the repl tool to do this job:
|
||||
//! ```sh
|
||||
//! $ cargo run --example repl --features=repl /path/to/account/db.sqlite
|
||||
//! > set save_mime_headers 1
|
||||
//! ```
|
||||
//! [`SaveMimeHeaders`]: ../config/enum.Config.html#variant.SaveMimeHeaders
|
||||
|
||||
use crate::chat::*;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::constants::DC_GCM_ADDDAYMARKER;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
// use crate::error::Error;
|
||||
use crate::dc_tools::time;
|
||||
use crate::message::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
use zip::write::FileOptions;
|
||||
|
||||
use crate::location::Location;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ExportChatResult {
|
||||
chat_json: String,
|
||||
// locations_geo_json: String,
|
||||
message_ids: Vec<MsgId>,
|
||||
referenced_blobs: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn export_chat_to_zip(context: &Context, chat_id: ChatId, filename: &str) {
|
||||
let res = export_chat_data(&context, chat_id).await;
|
||||
let destination = std::path::Path::new(filename);
|
||||
let pack_res = pack_exported_chat(&context, res, destination).await;
|
||||
match &pack_res {
|
||||
Ok(()) => println!("Exported chat successfully to {}", filename),
|
||||
Err(err) => println!("Error {:?}", err),
|
||||
};
|
||||
}
|
||||
|
||||
async fn pack_exported_chat(
|
||||
context: &Context,
|
||||
artifact: ExportChatResult,
|
||||
destination: &Path,
|
||||
) -> zip::result::ZipResult<()> {
|
||||
let file = std::fs::File::create(&destination).unwrap();
|
||||
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
zip.start_file("chat.json", Default::default())?;
|
||||
zip.write_all(artifact.chat_json.as_bytes())?;
|
||||
|
||||
zip.add_directory("blobs/", Default::default())?;
|
||||
|
||||
let options = FileOptions::default();
|
||||
for blob_name in artifact.referenced_blobs {
|
||||
let path = context.get_blobdir().join(&blob_name);
|
||||
|
||||
// println!("adding file {:?} as {:?} ...", path, &blob_name);
|
||||
zip.start_file(format!("blobs/{}", &blob_name), options)?;
|
||||
let mut f = File::open(path)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&*buffer)?;
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
zip.add_directory("msg_info/", Default::default())?;
|
||||
zip.add_directory("msg_source/", Default::default())?;
|
||||
for id in artifact.message_ids {
|
||||
zip.start_file(format!("msg_info/{}.txt", id.to_u32()), options)?;
|
||||
zip.write_all((get_msg_info(&context, id).await).as_bytes())?;
|
||||
if let Some(mime_headers) = get_mime_headers(&context, id).await {
|
||||
zip.start_file(format!("msg_source/{}.eml", id.to_u32()), options)?;
|
||||
zip.write_all((mime_headers).as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
zip.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChatJSON {
|
||||
chat_json_version: u8,
|
||||
export_timestamp: i64,
|
||||
name: String,
|
||||
color: String,
|
||||
profile_img: Option<String>,
|
||||
contacts: HashMap<u32, ContactJSON>,
|
||||
referenced_external_messages:Vec<ChatItemJSON>,
|
||||
messages: Vec<ChatItemJSON>,
|
||||
locations: Vec<Location>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ContactJSON {
|
||||
name: String,
|
||||
email: String,
|
||||
color: String,
|
||||
profile_img: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FileReference {
|
||||
name: String,
|
||||
filesize: u64,
|
||||
mime: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Qoute {
|
||||
quoted_text: String,
|
||||
message_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ChatItemJSON {
|
||||
Message {
|
||||
id: u32,
|
||||
author_id: u32, // from_id
|
||||
view_type: Viewtype,
|
||||
timestamp_sort: i64,
|
||||
timestamp_sent: i64,
|
||||
timestamp_rcvd: i64,
|
||||
text: Option<String>,
|
||||
attachment: Option<FileReference>,
|
||||
location_id: Option<u32>,
|
||||
is_info_message: bool,
|
||||
show_padlock: bool,
|
||||
state: MessageState,
|
||||
is_forwarded: bool,
|
||||
quote: Option<Qoute>
|
||||
},
|
||||
MessageError {
|
||||
id: u32,
|
||||
error: String,
|
||||
},
|
||||
DayMarker {
|
||||
timestamp: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ChatItemJSON {
|
||||
pub async fn from_message(message: &Message, context: &Context) -> ChatItemJSON {
|
||||
let msg_id = message.get_id();
|
||||
ChatItemJSON::Message {
|
||||
id: msg_id.to_u32(),
|
||||
author_id: message.get_from_id(), // from_id
|
||||
view_type: message.get_viewtype(),
|
||||
timestamp_sort: message.timestamp_sort,
|
||||
timestamp_sent: message.timestamp_sent,
|
||||
timestamp_rcvd: message.timestamp_rcvd,
|
||||
text: message.get_text(),
|
||||
attachment: match message.get_file(context) {
|
||||
Some(file) => Some(FileReference {
|
||||
name: message.get_filename().unwrap_or_else(|| "".to_owned()),
|
||||
filesize: message.get_filebytes(context).await,
|
||||
mime: message.get_filemime().unwrap_or_else(|| "".to_owned()),
|
||||
path: format!(
|
||||
"blobs/{}",
|
||||
file.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
),
|
||||
}),
|
||||
None => None,
|
||||
},
|
||||
location_id: match message.has_location() {
|
||||
true => Some(message.location_id),
|
||||
false => None,
|
||||
},
|
||||
is_info_message: message.is_info(),
|
||||
show_padlock: message.get_showpadlock(),
|
||||
state: message.get_state(),
|
||||
is_forwarded: message.is_forwarded(),
|
||||
quote: match message.quoted_text() {
|
||||
Some(text) => match message.quoted_message(&context).await {
|
||||
Ok(Some(msg)) => Some(Qoute {
|
||||
quoted_text: text,
|
||||
message_id: Some(msg.get_id().to_u32())
|
||||
}),
|
||||
Err(_) | Ok(None) => Some(Qoute {
|
||||
quoted_text: text,
|
||||
message_id: None
|
||||
})
|
||||
}
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn export_chat_data(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
let mut blobs = Vec::new();
|
||||
let mut chat_author_ids = Vec::new();
|
||||
// message_ids var is used for writing message info to files
|
||||
let mut message_ids: Vec<MsgId> = Vec::new();
|
||||
let mut message_json: Vec<ChatItemJSON> = Vec::new();
|
||||
let mut referenced_external_messages: Vec<ChatItemJSON> = Vec::new();
|
||||
|
||||
for item in get_chat_msgs(context, chat_id, DC_GCM_ADDDAYMARKER, None).await {
|
||||
if let Some(json_item) = match item {
|
||||
ChatItem::Message { msg_id } => match Message::load_from_db(context, msg_id).await {
|
||||
Ok(message) => {
|
||||
let filename = message.get_filename();
|
||||
if let Some(file) = filename {
|
||||
// push referenced blobs (attachments)
|
||||
blobs.push(file);
|
||||
}
|
||||
message_ids.push(message.id);
|
||||
// populate contactid list
|
||||
chat_author_ids.push(message.from_id);
|
||||
|
||||
if let Ok(Some(ex_msg)) = message.quoted_message(&context).await {
|
||||
if ex_msg.get_chat_id() != chat_id {
|
||||
// if external add it to the file
|
||||
referenced_external_messages.push(ChatItemJSON::from_message(&ex_msg, &context).await)
|
||||
// contacts don't need to be referenced, because these should only be private replies
|
||||
}
|
||||
}
|
||||
|
||||
Some(ChatItemJSON::from_message(&message, &context).await)
|
||||
}
|
||||
Err(error_message) => Some(ChatItemJSON::MessageError {
|
||||
id: msg_id.to_u32(),
|
||||
error: error_message.to_string(),
|
||||
}),
|
||||
},
|
||||
ChatItem::DayMarker { timestamp } => Some(ChatItemJSON::DayMarker { timestamp }),
|
||||
ChatItem::Marker1 => None,
|
||||
} {
|
||||
message_json.push(json_item)
|
||||
}
|
||||
}
|
||||
|
||||
// deduplicate contact list and load the contacts
|
||||
chat_author_ids.sort();
|
||||
chat_author_ids.dedup();
|
||||
// load information about the authors
|
||||
let mut chat_authors: HashMap<u32, ContactJSON> = HashMap::new();
|
||||
chat_authors.insert(
|
||||
0,
|
||||
ContactJSON {
|
||||
name: "Err: Contact not found".to_owned(),
|
||||
email: "error@localhost".to_owned(),
|
||||
profile_img: None,
|
||||
color: "grey".to_owned(),
|
||||
},
|
||||
);
|
||||
for author_id in chat_author_ids {
|
||||
let contact = Contact::get_by_id(context, author_id).await;
|
||||
if let Ok(c) = contact {
|
||||
let profile_img_path: String;
|
||||
if let Some(path) = c.get_profile_image(context).await {
|
||||
profile_img_path = path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
// push referenced blobs (avatars)
|
||||
blobs.push(profile_img_path.clone());
|
||||
} else {
|
||||
profile_img_path = "".to_owned();
|
||||
}
|
||||
chat_authors.insert(
|
||||
author_id,
|
||||
ContactJSON {
|
||||
name: c.get_display_name().to_owned(),
|
||||
email: c.get_addr().to_owned(),
|
||||
profile_img: match profile_img_path != "" {
|
||||
true => Some(profile_img_path),
|
||||
false => None,
|
||||
},
|
||||
color: format!("{:#}", c.get_color()), // TODO
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load information about the chat
|
||||
let chat: Chat = Chat::load_from_db(context, chat_id).await.unwrap();
|
||||
let chat_avatar = match chat.get_profile_image(context).await {
|
||||
Some(img) => {
|
||||
let path = img
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
blobs.push(path.clone());
|
||||
Some(format!("blobs/{}", path))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let chat_json = ChatJSON {
|
||||
chat_json_version: 1,
|
||||
export_timestamp: time(),
|
||||
name: chat.get_name().to_owned(),
|
||||
color: format!("{:#}", chat.get_color(&context).await),
|
||||
profile_img: chat_avatar,
|
||||
contacts: chat_authors,
|
||||
referenced_external_messages,
|
||||
messages: message_json,
|
||||
locations: crate::location::get_range(&context, chat_id, 0, 0, crate::dc_tools::time())
|
||||
.await,
|
||||
};
|
||||
|
||||
blobs.sort();
|
||||
blobs.dedup();
|
||||
ExportChatResult {
|
||||
chat_json: serde_json::to_string(&chat_json).unwrap(),
|
||||
message_ids,
|
||||
referenced_blobs: blobs,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
///! # format=flowed support
|
||||
///!
|
||||
///! Format=flowed is defined in
|
||||
///! [RFC 3676](https://tools.ietf.org/html/rfc3676).
|
||||
///!
|
||||
///! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
|
||||
///! during formatting, i.e., DelSp parameter introduced in RFC 3676
|
||||
///! is assumed to be set to "no".
|
||||
///!
|
||||
///! For received messages, DelSp parameter is honoured.
|
||||
//! # format=flowed support.
|
||||
//!
|
||||
//! Format=flowed is defined in
|
||||
//! [RFC 3676](https://tools.ietf.org/html/rfc3676).
|
||||
//!
|
||||
//! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
|
||||
//! during formatting, i.e., DelSp parameter introduced in RFC 3676
|
||||
//! is assumed to be set to "no".
|
||||
//!
|
||||
//! For received messages, DelSp parameter is honoured.
|
||||
|
||||
/// Wraps line to 72 characters using format=flowed soft breaks.
|
||||
///
|
||||
@@ -104,13 +104,13 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
|
||||
|
||||
for line in text.split('\n') {
|
||||
// Revert space-stuffing
|
||||
let line = line.strip_prefix(" ").unwrap_or(line);
|
||||
let line = line.strip_prefix(' ').unwrap_or(line);
|
||||
|
||||
if !skip_newline {
|
||||
result.push('\n');
|
||||
}
|
||||
|
||||
if let Some(line) = line.strip_suffix(" ") {
|
||||
if let Some(line) = line.strip_suffix(' ') {
|
||||
// Flowed line
|
||||
result += line;
|
||||
if !delsp {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::strum::AsStaticRef;
|
||||
//! # List of email headers.
|
||||
|
||||
use mailparse::{MailHeader, MailHeaderMap};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
pub enum HeaderDef {
|
||||
MessageId,
|
||||
@@ -25,7 +26,14 @@ pub enum HeaderDef {
|
||||
/// we need to check that header as well.
|
||||
XMicrosoftOriginalMessageId,
|
||||
|
||||
/// Thunderbird header used to store Draft information.
|
||||
///
|
||||
/// Thunderbird 78.11.0 does not set \Draft flag on messages saved as "Template", but sets this
|
||||
/// header, so it can be used to ignore such messages.
|
||||
XMozillaDraftInfo,
|
||||
|
||||
ListId,
|
||||
ListPost,
|
||||
References,
|
||||
InReplyTo,
|
||||
Precedence,
|
||||
@@ -59,9 +67,9 @@ pub enum HeaderDef {
|
||||
}
|
||||
|
||||
impl HeaderDef {
|
||||
/// Returns the corresponding Event id.
|
||||
/// Returns the corresponding header string.
|
||||
pub fn get_headername(&self) -> &'static str {
|
||||
self.as_static()
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
src/html.rs
54
src/html.rs
@@ -1,11 +1,12 @@
|
||||
///! # Get message as HTML.
|
||||
///!
|
||||
///! Use `Message.has_html()` to check if the UI shall render a
|
||||
///! corresponding button and `MsgId.get_html()` to get the full message.
|
||||
///!
|
||||
///! Even when the original mime-message is not HTML,
|
||||
///! `MsgId.get_html()` will return HTML -
|
||||
///! this allows nice quoting, handling linebreaks properly etc.
|
||||
//! # Get message as HTML.
|
||||
//!
|
||||
//! Use `Message.has_html()` to check if the UI shall render a
|
||||
//! corresponding button and `MsgId.get_html()` to get the full message.
|
||||
//!
|
||||
//! Even when the original mime-message is not HTML,
|
||||
//! `MsgId.get_html()` will return HTML -
|
||||
//! this allows nice quoting, handling linebreaks properly etc.
|
||||
|
||||
use futures::future::FutureExt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
@@ -248,7 +249,7 @@ impl MsgId {
|
||||
let rawmime = message::get_mime_headers(context, self).await?;
|
||||
|
||||
if !rawmime.is_empty() {
|
||||
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
|
||||
match HtmlMsgParser::from_bytes(context, &rawmime).await {
|
||||
Err(err) => {
|
||||
warn!(context, "get_html: parser error: {}", err);
|
||||
Ok(None)
|
||||
@@ -424,10 +425,10 @@ test some special html-characters as < > and & but also " and &#x
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_html_empty() {
|
||||
async fn test_get_html_invalid_msgid() {
|
||||
let t = TestContext::new().await;
|
||||
let msg_id = MsgId::new(100);
|
||||
assert!(msg_id.get_html(&t).await.unwrap().is_none())
|
||||
assert!(msg_id.get_html(&t).await.is_err())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -439,9 +440,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
dc_receive_imf(&alice, raw, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::No);
|
||||
@@ -467,7 +466,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.com").await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
|
||||
@@ -490,9 +489,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
dc_receive_imf(&alice, raw, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// forward the message to saved-messages,
|
||||
@@ -550,4 +547,25 @@ test some special html-characters as < > and & but also " and &#x
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_cp1252_html() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
include_bytes!("../test-data/message/cp1252-html.eml"),
|
||||
"INBOX",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
assert!(msg.text.as_ref().unwrap().contains("foo bar ä ö ü ß"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&t).await?.unwrap();
|
||||
println!("{}", html);
|
||||
assert!(html.contains("foo bar ä ö ü ß"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
1873
src/imap.rs
1873
src/imap.rs
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,23 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use async_imap::{
|
||||
error::{Error as ImapError, Result as ImapResult},
|
||||
Client as ImapClient,
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use async_imap::Client as ImapClient;
|
||||
|
||||
use async_smtp::ServerAddress;
|
||||
use async_std::net::{self, TcpStream};
|
||||
|
||||
use super::session::Session;
|
||||
use crate::login_param::dc_build_tls;
|
||||
use crate::login_param::{dc_build_tls, Socks5Config};
|
||||
|
||||
use super::session::SessionStream;
|
||||
|
||||
/// IMAP write and read timeout in seconds.
|
||||
const IMAP_TIMEOUT: u64 = 30;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Client {
|
||||
is_secure: bool,
|
||||
@@ -32,24 +39,12 @@ impl DerefMut for Client {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn login(
|
||||
self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||
let Client { inner, is_secure } = self;
|
||||
pub async fn login(self, username: &str, password: &str) -> Result<Session> {
|
||||
let Client { inner, .. } = self;
|
||||
let session = inner
|
||||
.login(username, password)
|
||||
.await
|
||||
.map_err(|(err, client)| {
|
||||
(
|
||||
err,
|
||||
Client {
|
||||
is_secure,
|
||||
inner: client,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
.map_err(|(err, _client)| err)?;
|
||||
Ok(Session { inner: session })
|
||||
}
|
||||
|
||||
@@ -57,21 +52,12 @@ impl Client {
|
||||
self,
|
||||
auth_type: &str,
|
||||
authenticator: impl async_imap::Authenticator,
|
||||
) -> 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,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
) -> Result<Session> {
|
||||
let Client { inner, .. } = self;
|
||||
let session = inner
|
||||
.authenticate(auth_type, authenticator)
|
||||
.await
|
||||
.map_err(|(err, _client)| err)?;
|
||||
Ok(Session { inner: session })
|
||||
}
|
||||
|
||||
@@ -79,7 +65,7 @@ impl Client {
|
||||
addr: impl net::ToSocketAddrs,
|
||||
domain: &str,
|
||||
strict_tls: bool,
|
||||
) -> ImapResult<Self> {
|
||||
) -> Result<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, stream).await?);
|
||||
@@ -88,7 +74,7 @@ impl Client {
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
.context("failed to read greeting")?;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
@@ -96,14 +82,14 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> ImapResult<Self> {
|
||||
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result<Self> {
|
||||
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
||||
|
||||
let mut client = ImapClient::new(stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
.context("failed to read greeting")?;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: false,
|
||||
@@ -111,7 +97,56 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn secure(self, domain: &str, strict_tls: bool) -> ImapResult<Client> {
|
||||
pub async fn connect_secure_socks5(
|
||||
target_addr: &ServerAddress,
|
||||
strict_tls: bool,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream: Box<dyn SessionStream> = Box::new(
|
||||
socks5_config
|
||||
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
|
||||
.await?,
|
||||
);
|
||||
|
||||
let tls = dc_build_tls(strict_tls);
|
||||
let tls_stream: Box<dyn SessionStream> =
|
||||
Box::new(tls.connect(target_addr.host.clone(), socks5_stream).await?);
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")?;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
inner: client,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn connect_insecure_socks5(
|
||||
target_addr: &ServerAddress,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream: Box<dyn SessionStream> = Box::new(
|
||||
socks5_config
|
||||
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
|
||||
.await?,
|
||||
);
|
||||
|
||||
let mut client = ImapClient::new(socks5_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")?;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: false,
|
||||
inner: client,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Client> {
|
||||
if self.is_secure {
|
||||
Ok(self)
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::Imap;
|
||||
|
||||
use anyhow::{bail, format_err, Result};
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_imap::types::UnsolicitedResponse;
|
||||
use async_std::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -25,31 +24,18 @@ impl Imap {
|
||||
if !self.can_idle() {
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
self.setup_handle(context).await?;
|
||||
self.prepare(context).await?;
|
||||
|
||||
self.select_folder(context, watch_folder.as_deref()).await?;
|
||||
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
let mut info = Default::default();
|
||||
|
||||
if self.server_sent_unsolicited_exists(context) {
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
if let Some(session) = self.session.take() {
|
||||
// if we have unsolicited responses we directly return
|
||||
let mut unsolicited_exists = false;
|
||||
while let Ok(response) = session.unsolicited_responses.try_recv() {
|
||||
match response {
|
||||
UnsolicitedResponse::Exists(_) => {
|
||||
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
|
||||
unsolicited_exists = true;
|
||||
}
|
||||
_ => info!(context, "ignoring unsolicited response {:?}", response),
|
||||
}
|
||||
}
|
||||
|
||||
if unsolicited_exists {
|
||||
self.session = Some(session);
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
if let Ok(info) = self.idle_interrupt.try_recv() {
|
||||
info!(context, "skip idle, got interrupt {:?}", info);
|
||||
self.session = Some(session);
|
||||
@@ -158,13 +144,13 @@ impl Imap {
|
||||
// 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 {
|
||||
if let Err(err) = self.prepare(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);
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
@@ -176,12 +162,12 @@ impl Imap {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {:#}", err);
|
||||
self.trigger_reconnect()
|
||||
self.trigger_reconnect(context).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::time::Instant;
|
||||
use std::{collections::BTreeMap, time::Instant};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::{config::Config, log::LogExt};
|
||||
use crate::{context::Context, imap::FolderMeaning};
|
||||
use async_std::prelude::*;
|
||||
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
|
||||
impl Imap {
|
||||
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
|
||||
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
|
||||
// First of all, debounce to once per minute:
|
||||
let mut last_scan = context.last_full_folder_scan.lock().await;
|
||||
if let Some(last_scan) = *last_scan {
|
||||
@@ -25,14 +25,13 @@ impl Imap {
|
||||
}
|
||||
info!(context, "Starting full folder scan");
|
||||
|
||||
self.connect_configured(context).await?;
|
||||
self.prepare(context).await?;
|
||||
let session = self.session.as_mut();
|
||||
let session = session.context("scan_folders(): IMAP No Connection established")?;
|
||||
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
|
||||
let watched_folders = get_watched_folders(context).await;
|
||||
let watched_folders = get_watched_folders(context).await?;
|
||||
|
||||
let mut sentbox_folder = None;
|
||||
let mut spam_folder = None;
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
for folder in folders {
|
||||
let folder = match folder {
|
||||
@@ -43,57 +42,81 @@ impl Imap {
|
||||
}
|
||||
};
|
||||
|
||||
let foldername = folder.name();
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(foldername);
|
||||
|
||||
if folder_meaning == FolderMeaning::SentObjects {
|
||||
// Always takes precedent
|
||||
sentbox_folder = Some(folder.name().to_string());
|
||||
} else if folder_meaning == FolderMeaning::Spam {
|
||||
spam_folder = Some(folder.name().to_string());
|
||||
} else if folder_name_meaning == FolderMeaning::SentObjects {
|
||||
// only set iff none has been already set
|
||||
if sentbox_folder.is_none() {
|
||||
sentbox_folder = Some(folder.name().to_string());
|
||||
}
|
||||
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
|
||||
spam_folder = Some(folder.name().to_string());
|
||||
// Gmail labels are not folders and should be skipped. For example,
|
||||
// emails appear in the inbox and under "All Mail" as soon as it is
|
||||
// received. The code used to wrongly conclude that the email had
|
||||
// already been moved and left it in the inbox.
|
||||
let folder_name = folder.name();
|
||||
if folder_name.starts_with("[Gmail]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
|
||||
|
||||
if let Some(config) = folder_meaning.to_config() {
|
||||
// Always takes precedence
|
||||
folder_configs.insert(config, folder.name().to_string());
|
||||
} else if let Some(config) = folder_name_meaning.to_config() {
|
||||
// only set if none has been already set
|
||||
folder_configs
|
||||
.entry(config)
|
||||
.or_insert_with(|| folder.name().to_string());
|
||||
}
|
||||
|
||||
let is_drafts = folder_meaning == FolderMeaning::Drafts
|
||||
|| (folder_meaning == FolderMeaning::Unknown
|
||||
&& folder_name_meaning == FolderMeaning::Drafts);
|
||||
|
||||
// Don't scan folders that are watched anyway
|
||||
if !watched_folders.contains(&foldername.to_string()) {
|
||||
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
|
||||
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
|
||||
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
|
||||
// Drain leftover unsolicited EXISTS messages
|
||||
self.server_sent_unsolicited_exists(context);
|
||||
|
||||
loop {
|
||||
self.fetch_move_delete(context, folder.name())
|
||||
.await
|
||||
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
|
||||
|
||||
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
|
||||
if !self.server_sent_unsolicited_exists(context) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
|
||||
.await?;
|
||||
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
|
||||
// `ConfiguredSentboxFolder` is set to `None`:
|
||||
for config in &[
|
||||
Config::ConfiguredSentboxFolder,
|
||||
Config::ConfiguredSpamFolder,
|
||||
] {
|
||||
context
|
||||
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
last_scan.replace(Instant::now());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_watched_folders(context: &Context) -> Vec<String> {
|
||||
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
|
||||
let mut res = Vec::new();
|
||||
if let Some(inbox_folder) = context.get_config(Config::ConfiguredInboxFolder).await? {
|
||||
res.push(inbox_folder);
|
||||
}
|
||||
let folder_watched_configured = &[
|
||||
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
|
||||
(Config::MvboxWatch, Config::ConfiguredMvboxFolder),
|
||||
(Config::InboxWatch, Config::ConfiguredInboxFolder),
|
||||
(Config::MvboxMove, Config::ConfiguredMvboxFolder),
|
||||
];
|
||||
for (watched, configured) in folder_watched_configured {
|
||||
if context.get_config_bool(*watched).await.unwrap_or_default() {
|
||||
if let Ok(Some(folder)) = context.get_config(*configured).await {
|
||||
if context.get_config_bool(*watched).await? {
|
||||
if let Some(folder) = context.get_config(*configured).await? {
|
||||
res.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub enum Error {
|
||||
#[error("IMAP Folder name invalid: {0}")]
|
||||
BadFolderName(String),
|
||||
|
||||
#[error("IMAP folder does not exist: {0}")]
|
||||
NoFolder(String),
|
||||
|
||||
#[error("IMAP close/expunge failed")]
|
||||
CloseExpungeFailed(#[from] async_imap::error::Error),
|
||||
|
||||
@@ -26,7 +29,7 @@ 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
|
||||
/// <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);
|
||||
@@ -37,7 +40,7 @@ impl Imap {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
self.trigger_reconnect();
|
||||
self.trigger_reconnect(context).await;
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
}
|
||||
}
|
||||
@@ -70,7 +73,7 @@ impl Imap {
|
||||
if self.session.is_none() {
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_folder_needs_expunge = false;
|
||||
self.trigger_reconnect();
|
||||
self.trigger_reconnect(context).await;
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
|
||||
@@ -92,7 +95,7 @@ impl Imap {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
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>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
@@ -103,16 +106,19 @@ impl Imap {
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => {
|
||||
self.trigger_reconnect();
|
||||
self.trigger_reconnect(context).await;
|
||||
self.config.selected_folder = None;
|
||||
Err(Error::ConnectionLost)
|
||||
}
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(_)) => {
|
||||
Err(Error::NoFolder(folder.to_string()))
|
||||
}
|
||||
Err(err) => {
|
||||
self.config.selected_folder = None;
|
||||
self.trigger_reconnect();
|
||||
self.trigger_reconnect(context).await;
|
||||
Err(Error::Other(err.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -123,6 +129,28 @@ impl Imap {
|
||||
Ok(NewlySelected::No)
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a folder. Tries to create it once and select again if the folder does not exist.
|
||||
pub(super) async fn select_or_create_folder(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<NewlySelected> {
|
||||
match self.select_folder(context, Some(folder)).await {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(_) => {
|
||||
if let Some(ref mut session) = self.session {
|
||||
session.create(folder).await?;
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
self.select_folder(context, Some(folder)).await
|
||||
}
|
||||
_ => Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
|
||||
pub(super) enum NewlySelected {
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::ops::{Deref, DerefMut};
|
||||
use async_imap::Session as ImapSession;
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::TcpStream;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Session {
|
||||
@@ -17,6 +18,7 @@ pub(crate) trait SessionStream:
|
||||
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
|
||||
impl SessionStream for TlsStream<TcpStream> {}
|
||||
impl SessionStream for TcpStream {}
|
||||
impl SessionStream for Socks5Stream<TcpStream> {}
|
||||
|
||||
impl Deref for Session {
|
||||
type Target = ImapSession<Box<dyn SessionStream>>;
|
||||
|
||||
40
src/imex.rs
40
src/imex.rs
@@ -1,4 +1,4 @@
|
||||
//! # Import/export module
|
||||
//! # Import/export module.
|
||||
|
||||
use std::any::Any;
|
||||
use std::ffi::OsStr;
|
||||
@@ -66,15 +66,15 @@ pub enum ImexMode {
|
||||
|
||||
/// Import/export things.
|
||||
///
|
||||
/// What to do is defined by the *what* parameter.
|
||||
/// What to do is defined by the `what` parameter.
|
||||
///
|
||||
/// During execution of the job,
|
||||
/// some events are sent out:
|
||||
///
|
||||
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
|
||||
/// - A number of `DC_EVENT_IMEX_PROGRESS` events are sent and may be used to create
|
||||
/// a progress bar or stuff like that. Moreover, you'll be informed when the imex-job is done.
|
||||
///
|
||||
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
|
||||
/// - For each file written on export, the function sends `DC_EVENT_IMEX_FILE_WRITTEN`
|
||||
///
|
||||
/// Only one import-/export-progress can run at the same time.
|
||||
/// To cancel an import-/export-progress, drop the future returned by this function.
|
||||
@@ -184,7 +184,7 @@ pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String
|
||||
"Found backup file {} which could not be opened: {}", name, e
|
||||
);
|
||||
// On some Android devices we can't open sql files that are not in our private directory
|
||||
// (see https://github.com/deltachat/deltachat-android/issues/1768). So, compare names
|
||||
// (see <https://github.com/deltachat/deltachat-android/issues/1768>). So, compare names
|
||||
// to still find the newest backup.
|
||||
let name: String = name.into();
|
||||
if newest_backup_time == 0
|
||||
@@ -204,6 +204,7 @@ pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates key transfer via Autocrypt Setup Message.
|
||||
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
@@ -233,11 +234,11 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg = Message::default();
|
||||
msg.viewtype = Viewtype::File;
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
|
||||
msg.subject = stock_str::ac_setup_msg_subject(context).await;
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
@@ -435,7 +436,7 @@ async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
|
||||
Ok(plain_text)
|
||||
}
|
||||
|
||||
pub fn normalize_setup_code(s: &str) -> String {
|
||||
fn normalize_setup_code(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for c in s.chars() {
|
||||
if ('0'..='9').contains(&c) {
|
||||
@@ -506,14 +507,16 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
|
||||
let archive = Archive::new(backup_file);
|
||||
|
||||
let mut entries = archive.entries()?;
|
||||
let mut last_progress = 0;
|
||||
while let Some(file) = entries.next().await {
|
||||
let f = &mut file?;
|
||||
|
||||
let current_pos = f.raw_file_position();
|
||||
let progress = 1000 * current_pos / file_size;
|
||||
if progress > 10 && progress < 1000 {
|
||||
if progress != last_progress && progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
context.emit_event(EventType::ImexProgress(progress as usize));
|
||||
last_progress = progress;
|
||||
}
|
||||
|
||||
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
|
||||
@@ -736,6 +739,7 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
|
||||
let count = read_dir.len();
|
||||
let mut written_files = 0;
|
||||
|
||||
let mut last_progress = 0;
|
||||
for entry in read_dir.into_iter() {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
@@ -753,9 +757,10 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
|
||||
|
||||
written_files += 1;
|
||||
let progress = 1000 * written_files / count;
|
||||
if progress > 10 && progress < 1000 {
|
||||
if progress != last_progress && progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
emit_event!(context, EventType::ImexProgress(progress));
|
||||
context.emit_event(EventType::ImexProgress(progress));
|
||||
last_progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,9 +936,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
t.configure_alice().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let msg = render_setup_file(&t, "hello").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
@@ -950,11 +953,10 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
t.configure_alice().await;
|
||||
let msg = render_setup_file(&t, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>hello<br>there</p>"));
|
||||
@@ -1007,15 +1009,13 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_export_and_import_key() {
|
||||
let context = TestContext::new().await;
|
||||
context.configure_alice().await;
|
||||
let context = TestContext::new_alice().await;
|
||||
let blobdir = context.ctx.get_blobdir();
|
||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
|
||||
panic!("got error on export: {:?}", err);
|
||||
}
|
||||
|
||||
let context2 = TestContext::new().await;
|
||||
context2.configure_alice().await;
|
||||
let context2 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
|
||||
panic!("got error on import: {:?}", err);
|
||||
}
|
||||
|
||||
555
src/job.rs
555
src/job.rs
@@ -1,25 +1,21 @@
|
||||
//! # Job module
|
||||
//! # Job module.
|
||||
//!
|
||||
//! This module implements a job queue maintained in the SQLite database
|
||||
//! and job types.
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::{fmt, time::Duration};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use async_std::task::sleep;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatItem};
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_DEADDROP};
|
||||
use crate::contact::{normalize_name, Contact, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::location;
|
||||
@@ -96,10 +92,14 @@ pub enum Action {
|
||||
FetchExistingMsgs = 110,
|
||||
MarkseenMsgOnImap = 130,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
// bother moving message if it is already scheduled for deletion.
|
||||
MoveMsg = 200,
|
||||
DeleteMsgOnImap = 210,
|
||||
// this is user initiated so it should have a fairly high priority
|
||||
UpdateRecentQuota = 140,
|
||||
|
||||
// This job will download partially downloaded messages completely
|
||||
// and is added when download_full() is called.
|
||||
// Most messages are downloaded automatically on fetch
|
||||
// and do not go through this job.
|
||||
DownloadMsg = 250,
|
||||
|
||||
// UID synchronization is high-priority to make sure correct UIDs
|
||||
// are used by message moving/deletion.
|
||||
@@ -127,10 +127,10 @@ impl From<Action> for Thread {
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
FetchExistingMsgs => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
ResyncFolders => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
UpdateRecentQuota => Thread::Imap,
|
||||
DownloadMsg => Thread::Imap,
|
||||
|
||||
MaybeSendLocations => Thread::Smtp,
|
||||
MaybeSendLocationsEnded => Thread::Smtp,
|
||||
@@ -149,7 +149,6 @@ pub struct Job {
|
||||
pub added_timestamp: i64,
|
||||
pub tries: u32,
|
||||
pub param: Params,
|
||||
pub pending_error: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Job {
|
||||
@@ -170,7 +169,6 @@ impl Job {
|
||||
added_timestamp: timestamp,
|
||||
tries: 0,
|
||||
param,
|
||||
pending_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,11 +247,16 @@ impl Job {
|
||||
info!(context, "smtp-sending out mime message:");
|
||||
println!("{}", String::from_utf8_lossy(&message));
|
||||
}
|
||||
let status = match smtp.send(context, recipients, message, job_id).await {
|
||||
Err(crate::smtp::send::Error::SendError(err)) => {
|
||||
|
||||
smtp.connectivity.set_working(context).await;
|
||||
|
||||
let send_result = smtp.send(context, recipients, message, job_id).await;
|
||||
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
|
||||
|
||||
let status = match send_result {
|
||||
Err(crate::smtp::send::Error::SmtpSend(err)) => {
|
||||
// Remote error, retry later.
|
||||
warn!(context, "SMTP failed to send: {:?}", err);
|
||||
self.pending_error = Some(err.to_string());
|
||||
warn!(context, "SMTP failed to send: {:?}", &err);
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||
@@ -261,13 +264,13 @@ impl Job {
|
||||
// instead of temporary ones.
|
||||
let maybe_transient = match response.code {
|
||||
// Sometimes servers send a permanent error when actually it is a temporary error
|
||||
// For documentation see https://tools.ietf.org/html/rfc3463
|
||||
// For documentation see <https://tools.ietf.org/html/rfc3463>
|
||||
Code {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
..
|
||||
} => {
|
||||
// Ignore status code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||
// Ignore status code 5.5.0, see <https://support.delta.chat/t/every-other-message-gets-stuck/877/2>
|
||||
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
|
||||
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
|
||||
//
|
||||
@@ -301,7 +304,7 @@ impl Job {
|
||||
// Sometimes we receive transient errors that should be permanent.
|
||||
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
|
||||
// receive as a transient error are misconfigurations of the smtp server.
|
||||
// See https://tools.ietf.org/html/rfc3463#section-3.2
|
||||
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
|
||||
info!(context, "Smtp-job #{} Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", self.job_id, first_word);
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
} else {
|
||||
@@ -326,7 +329,7 @@ impl Job {
|
||||
|
||||
res
|
||||
}
|
||||
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
|
||||
Err(crate::smtp::send::Error::Envelope(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
@@ -362,6 +365,7 @@ impl Job {
|
||||
// SMTP server, if not yet done
|
||||
if let Err(err) = smtp.connect_configured(context).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
smtp.last_send_error = Some(format!("SMTP connection failure: {:#}", err));
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
@@ -404,6 +408,8 @@ impl Job {
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to check message existence: {:?}", err);
|
||||
smtp.last_send_error =
|
||||
Some(format!("failed to check message existence: {:#}", err));
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
@@ -418,6 +424,13 @@ impl Job {
|
||||
}
|
||||
// now also delete the generated file
|
||||
dc_delete_file(context, filename).await;
|
||||
|
||||
// finally, create another send-job if there are items to be synced.
|
||||
// triggering sync-job after msg-send-job guarantees, the recipient has grpid etc.
|
||||
// once the sync message arrives.
|
||||
// if there are no items to sync, this function returns fast.
|
||||
context.send_sync_msg().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
@@ -518,6 +531,7 @@ impl Job {
|
||||
// connect to SMTP server, if not yet done
|
||||
if let Err(err) = smtp.connect_configured(context).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
smtp.last_send_error = Some(err.to_string());
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
@@ -531,148 +545,6 @@ impl Job {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
let server_folder = &job_try!(msg
|
||||
.server_folder
|
||||
.context("Can't move message out of folder if we don't know the current folder"));
|
||||
|
||||
let move_res = msg.id.needs_move(context, server_folder).await;
|
||||
let dest_folder = match move_res {
|
||||
Err(e) => {
|
||||
warn!(context, "could not load dest folder: {}", e);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
context,
|
||||
"msg {} does not need to be moved from {}", msg.id, server_folder
|
||||
);
|
||||
return Status::Finished(Ok(()));
|
||||
}
|
||||
Ok(Some(config)) => match context.get_config(config).await {
|
||||
Ok(folder) => folder,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load config: {}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
match imap
|
||||
.mv(context, server_folder, msg.server_uid, &dest_folder)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::Success => {
|
||||
// Rust-Imap provides no target uid on mv, so just set it to 0, update again when precheck_imf() is called for the moved message
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0).await;
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
ImapActionResult::Failed => {
|
||||
Status::Finished(Err(format_err!("IMAP action failed")))
|
||||
}
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Err(format_err!("No mvbox folder configured")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a message on the server.
|
||||
///
|
||||
/// foreign_id is a MsgId pointing to a message in the trash chat
|
||||
/// or a hidden message.
|
||||
///
|
||||
/// This job removes the database record. If there are no more
|
||||
/// records pointing to the same message on the server, the job
|
||||
/// also removes the message on the server.
|
||||
async fn delete_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid).await;
|
||||
info!(
|
||||
context,
|
||||
"Running delete job for message {} which has {} entries in the database",
|
||||
&msg.rfc724_mid,
|
||||
cnt
|
||||
);
|
||||
if cnt > 1 {
|
||||
info!(
|
||||
context,
|
||||
"The message is deleted from the server when all parts are deleted.",
|
||||
);
|
||||
} else if cnt == 0 {
|
||||
warn!(
|
||||
context,
|
||||
"The message {} has no UID on the server to delete", &msg.rfc724_mid
|
||||
);
|
||||
} else {
|
||||
/* if this is the last existing part of the message,
|
||||
we delete the message from the server */
|
||||
let mid = msg.rfc724_mid;
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let res = if msg.server_uid == 0 {
|
||||
// Message is already deleted on IMAP server.
|
||||
ImapActionResult::AlreadyDone
|
||||
} else {
|
||||
imap.delete_msg(context, &mid, server_folder, msg.server_uid)
|
||||
.await
|
||||
};
|
||||
match res {
|
||||
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
// If job has failed, for example due to some
|
||||
// IMAP bug, we postpone it instead of failing
|
||||
// immediately. This will prevent adding it
|
||||
// immediately again if user has enabled
|
||||
// automatic message deletion. Without this,
|
||||
// we might waste a lot of traffic constantly
|
||||
// retrying message deletion.
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg.chat_id.is_trash() || msg.hidden {
|
||||
// Messages are stored in trash chat only to keep
|
||||
// their server UID and Message-ID. Once message is
|
||||
// deleted from the server, database record can be
|
||||
// removed as well.
|
||||
//
|
||||
// Hidden messages are similar to trashed, but are
|
||||
// related to some chat. We also delete their
|
||||
// database records.
|
||||
job_try!(msg.id.delete_from_db(context).await)
|
||||
} else {
|
||||
// Remove server UID from the database record.
|
||||
//
|
||||
// We have either just removed the message from the
|
||||
// server, in which case UID is not valid anymore, or
|
||||
// we have more refernces to the same server UID, so
|
||||
// we remove UID to reduce the number of messages
|
||||
// pointing to the corresponding UID. Once the counter
|
||||
// reaches zero, we will remove the message.
|
||||
job_try!(msg.id.unlink(context).await);
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
} else {
|
||||
/* eg. device messages have no Message-ID */
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
/// This way, we can already offer them some email addresses they can write to.
|
||||
///
|
||||
@@ -682,7 +554,7 @@ impl Job {
|
||||
if job_try!(context.get_config_bool(Config::Bot).await) {
|
||||
return Status::Finished(Ok(())); // Bots don't want those messages
|
||||
}
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
@@ -698,7 +570,7 @@ impl Job {
|
||||
Config::ConfiguredSentboxFolder,
|
||||
] {
|
||||
if let Some(folder) = job_try!(context.get_config(*config).await) {
|
||||
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
|
||||
if let Err(e) = imap.fetch_new_messages(context, &folder, true).await {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
warn!(context, "Could not fetch messages, retrying: {:#}", e);
|
||||
return Status::RetryLater;
|
||||
@@ -707,40 +579,6 @@ impl Job {
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that if there now is a chat with a contact (created by an outgoing
|
||||
// message), then group contact requests from this contact should also be unblocked.
|
||||
// See https://github.com/deltachat/deltachat-core-rust/issues/2097.
|
||||
for item in job_try!(chat::get_chat_msgs(context, DC_CHAT_ID_DEADDROP, 0, None).await) {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = match Message::load_from_db(context, msg_id).await {
|
||||
Err(e) => {
|
||||
warn!(context, "can't get msg: {:#}", e);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
Ok(m) => m,
|
||||
};
|
||||
let chat = match Chat::load_from_db(context, msg.chat_id).await {
|
||||
Err(e) => {
|
||||
warn!(context, "can't get chat: {:#}", e);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
Ok(c) => c,
|
||||
};
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
if let Ok(Some(one_to_one_chat)) =
|
||||
ChatIdBlocked::lookup_by_contact(context, msg.from_id).await
|
||||
{
|
||||
if one_to_one_chat.blocked == Blocked::Not {
|
||||
chat.id.unblock(context).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Done fetching existing messages.");
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
@@ -755,7 +593,7 @@ impl Job {
|
||||
/// Chat in contrast to the Sent folder, which is normally managed
|
||||
/// by the user via webmail or another email client.
|
||||
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
@@ -779,78 +617,81 @@ impl Job {
|
||||
}
|
||||
|
||||
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
|
||||
let result = if msg.server_uid == 0 {
|
||||
// The message is moved or deleted by us.
|
||||
//
|
||||
// Do not call set_seen with zero UID, as it will return
|
||||
// ImapActionResult::RetryLater, but we do not want to
|
||||
// retry. If the message was moved, we will create another
|
||||
// job to mark the message as seen later. If it was
|
||||
// deleted, there is nothing to do.
|
||||
info!(context, "Can't mark message as seen: No UID");
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
imap.set_seen(context, folder, msg.server_uid).await
|
||||
};
|
||||
|
||||
match result {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {
|
||||
// XXX the message might just have been moved
|
||||
// we want to send out an MDN anyway
|
||||
// The job will not be retried so locally
|
||||
// there is no risk of double-sending MDNs.
|
||||
//
|
||||
// Read receipts for system messages are never
|
||||
// sent. These messages have no place to display
|
||||
// received read receipt anyway. And since their text
|
||||
// is locally generated, quoting them is dangerous as
|
||||
// it may contain contact names. E.g., for original
|
||||
// message "Group left by me", a read receipt will
|
||||
// quote "Group left by <name>", and the name can be a
|
||||
// display name stored in address book rather than
|
||||
// the name sent in the From field by the user.
|
||||
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& !msg.is_system_message()
|
||||
{
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if mdns_enabled {
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
|
||||
return Status::Finished(Err(err));
|
||||
}
|
||||
let row = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap
|
||||
WHERE rfc724_mid=? AND folder=target
|
||||
ORDER BY uid ASC
|
||||
LIMIT 1",
|
||||
paramsv![msg.rfc724_mid],
|
||||
|row| {
|
||||
let uid: u32 = row.get(0)?;
|
||||
let folder: String = row.get(1)?;
|
||||
Ok((uid, folder))
|
||||
}
|
||||
)
|
||||
.await
|
||||
);
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
let result = imap.set_seen(context, &server_folder, server_uid).await;
|
||||
match result {
|
||||
ImapActionResult::RetryLater => return Status::RetryLater,
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {}
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't mark the message {} as seen on IMAP because there is no known UID",
|
||||
msg.rfc724_mid
|
||||
);
|
||||
}
|
||||
|
||||
// XXX we send MDN even in case of failure to mark the messages as seen, e.g. if it was
|
||||
// already deleted on the server by another device. The job will not be retried so locally
|
||||
// there is no risk of double-sending MDNs.
|
||||
//
|
||||
// Read receipts for system messages are never sent. These messages have no place to
|
||||
// display received read receipt anyway. And since their text is locally generated,
|
||||
// quoting them is dangerous as it may contain contact names. E.g., for original message
|
||||
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
|
||||
// be a display name stored in address book rather than the name sent in the From field by
|
||||
// the user.
|
||||
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() && !msg.is_system_message() {
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if mdns_enabled {
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
|
||||
return Status::Finished(Err(err));
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all pending jobs with the given action.
|
||||
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
||||
pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
|
||||
.await
|
||||
.is_ok()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
);
|
||||
context
|
||||
.sql
|
||||
@@ -859,15 +700,15 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn action_exists(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM jobs WHERE action=?;",
|
||||
paramsv![action],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
@@ -899,7 +740,7 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(normalize_name)
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
@@ -1001,7 +842,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
}
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?;
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
@@ -1018,6 +859,12 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||
@@ -1051,20 +898,6 @@ pub(crate) enum Connection<'a> {
|
||||
Smtp(&'a mut Smtp),
|
||||
}
|
||||
|
||||
pub(crate) async fn load_imap_deletion_job(context: &Context) -> Result<Option<Job>> {
|
||||
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
||||
Some(Job::new(
|
||||
Action::DeleteMsgOnImap,
|
||||
msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Connection<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -1108,7 +941,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
"{} thread increases job {} tries to {}", &connection, job, tries
|
||||
);
|
||||
job.tries = tries;
|
||||
let time_offset = get_backoff_time_offset(tries);
|
||||
let time_offset = get_backoff_time_offset(tries, job.action);
|
||||
job.desired_timestamp = time() + time_offset;
|
||||
info!(
|
||||
context,
|
||||
@@ -1173,15 +1006,18 @@ async fn perform_job_action(
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
|
||||
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
|
||||
Action::Housekeeping => {
|
||||
sql::housekeeping(context).await.ok_or_log(context);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
|
||||
Ok(status) => status,
|
||||
Err(err) => Status::Finished(Err(err)),
|
||||
},
|
||||
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
|
||||
};
|
||||
|
||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||
@@ -1189,33 +1025,43 @@ async fn perform_job_action(
|
||||
try_res
|
||||
}
|
||||
|
||||
fn get_backoff_time_offset(tries: u32) -> i64 {
|
||||
let n = 2_i32.pow(tries - 1) * 60;
|
||||
let mut rng = thread_rng();
|
||||
let r: i32 = rng.gen();
|
||||
let mut seconds = r % (n + 1);
|
||||
if seconds < 1 {
|
||||
seconds = 1;
|
||||
fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
|
||||
match action {
|
||||
// Just try every 10s to update the quota
|
||||
// If all retries are exhausted, a new job will be created when the quota information is needed
|
||||
Action::UpdateRecentQuota => 10,
|
||||
|
||||
_ => {
|
||||
// Exponential backoff
|
||||
let n = 2_i32.pow(tries - 1) * 60;
|
||||
let mut rng = thread_rng();
|
||||
let r: i32 = rng.gen();
|
||||
let mut seconds = r % (n + 1);
|
||||
if seconds < 1 {
|
||||
seconds = 1;
|
||||
}
|
||||
seconds as i64
|
||||
}
|
||||
}
|
||||
seconds as i64
|
||||
}
|
||||
|
||||
async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
|
||||
let mut param = Params::new();
|
||||
param.set(Param::MsgId, msg.id.to_u32().to_string());
|
||||
|
||||
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await;
|
||||
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(context: &Context) {
|
||||
kill_action(context, Action::ResyncFolders).await;
|
||||
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
|
||||
kill_action(context, Action::ResyncFolders).await?;
|
||||
add(
|
||||
context,
|
||||
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a job.
|
||||
@@ -1229,55 +1075,44 @@ pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64
|
||||
}
|
||||
|
||||
/// Adds a job to the database, scheduling it.
|
||||
pub async fn add(context: &Context, job: Job) {
|
||||
pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
let action = job.action;
|
||||
let delay_seconds = job.delay_seconds();
|
||||
job.save(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to save job: {}", err);
|
||||
});
|
||||
job.save(context).await.context("failed to save job")?;
|
||||
|
||||
if delay_seconds == 0 {
|
||||
match action {
|
||||
Action::Unknown => unreachable!(),
|
||||
Action::Housekeeping
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::ResyncFolders
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::FetchExistingMsgs
|
||||
| Action::MoveMsg => {
|
||||
| Action::UpdateRecentQuota
|
||||
| Action::DownloadMsg => {
|
||||
info!(context, "interrupt: imap");
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
}
|
||||
Action::MaybeSendLocations
|
||||
| Action::MaybeSendLocationsEnded
|
||||
| Action::SendMdn
|
||||
| Action::SendMsgToSmtp => {
|
||||
info!(context, "interrupt: smtp");
|
||||
context
|
||||
.interrupt_smtp(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_housekeeping_job(context: &Context) -> Option<Job> {
|
||||
let last_time = match context.get_config_i64(Config::LastHousekeeping).await {
|
||||
Ok(last_time) => last_time,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load housekeeping config: {:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
async fn load_housekeeping_job(context: &Context) -> Result<Option<Job>> {
|
||||
let last_time = context.get_config_i64(Config::LastHousekeeping).await?;
|
||||
|
||||
let next_time = last_time + (60 * 60 * 24);
|
||||
if next_time <= time() {
|
||||
kill_action(context, Action::Housekeeping).await;
|
||||
Some(Job::new(Action::Housekeeping, 0, Params::new(), 0))
|
||||
kill_action(context, Action::Housekeeping).await?;
|
||||
Ok(Some(Job::new(Action::Housekeeping, 0, Params::new(), 0)))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1291,37 +1126,15 @@ pub(crate) async fn load_next(
|
||||
context: &Context,
|
||||
thread: Thread,
|
||||
info: &InterruptInfo,
|
||||
) -> Option<Job> {
|
||||
) -> Result<Option<Job>> {
|
||||
info!(context, "loading job for {}-thread", thread);
|
||||
|
||||
while !context.sql.is_open().await {
|
||||
// The db is closed, which means that this thread should not be running.
|
||||
// Wait until the db is re-opened (if we returned None, this thread might do further damage)
|
||||
warn!(
|
||||
context,
|
||||
"{}: load_next() was called but the db was not opened, THIS SHOULD NOT HAPPEN. Waiting...",
|
||||
thread
|
||||
);
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
let query;
|
||||
let params;
|
||||
let t = time();
|
||||
let m;
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
query = r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND foreign_id=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
m = msg_id;
|
||||
params = paramsv![thread_i, m];
|
||||
} else if !info.probe_network {
|
||||
if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
query = r#"
|
||||
@@ -1358,7 +1171,6 @@ LIMIT 1;
|
||||
added_timestamp: row.get("added_timestamp")?,
|
||||
tries: row.get("tries")?,
|
||||
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
|
||||
pending_error: None,
|
||||
};
|
||||
|
||||
Ok(job)
|
||||
@@ -1372,51 +1184,32 @@ LIMIT 1;
|
||||
info!(context, "cleaning up job, because of {}", err);
|
||||
|
||||
// TODO: improve by only doing a single query
|
||||
match context
|
||||
let id = context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.await
|
||||
{
|
||||
Ok(id) => {
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.await
|
||||
{
|
||||
warn!(context, "failed to delete job {}: {:?}", id, err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "failed to retrieve invalid job from DB: {}", err);
|
||||
break None;
|
||||
}
|
||||
}
|
||||
.context("Failed to retrieve invalid job ID from the database")?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete invalid job {}", id))?;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match thread {
|
||||
Thread::Unknown => {
|
||||
error!(context, "unknown thread for job");
|
||||
None
|
||||
bail!("unknown thread for job")
|
||||
}
|
||||
Thread::Imap => {
|
||||
if let Some(job) = job {
|
||||
if job.action < Action::DeleteMsgOnImap {
|
||||
load_imap_deletion_job(context)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.or(Some(job))
|
||||
} else {
|
||||
Some(job)
|
||||
}
|
||||
} else if let Some(job) = load_imap_deletion_job(context).await.unwrap_or_default() {
|
||||
Some(job)
|
||||
Ok(Some(job))
|
||||
} else {
|
||||
load_housekeeping_job(context).await
|
||||
Ok(load_housekeeping_job(context).await?)
|
||||
}
|
||||
}
|
||||
Thread::Smtp => job,
|
||||
Thread::Smtp => Ok(job),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1436,8 +1229,12 @@ mod tests {
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
now,
|
||||
Thread::from(Action::MoveMsg),
|
||||
if valid { Action::MoveMsg as i32 } else { -1 },
|
||||
Thread::from(Action::DownloadMsg),
|
||||
if valid {
|
||||
Action::DownloadMsg as i32
|
||||
} else {
|
||||
-1
|
||||
},
|
||||
foreign_id,
|
||||
Params::new().to_string(),
|
||||
now
|
||||
@@ -1448,7 +1245,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_next_job_two() {
|
||||
async fn test_load_next_job_two() -> Result<()> {
|
||||
// We want to ensure that loading jobs skips over jobs which
|
||||
// fails to load from the database instead of failing to load
|
||||
// all jobs.
|
||||
@@ -1456,35 +1253,37 @@ mod tests {
|
||||
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
||||
assert_eq!(jobs.unwrap().action, Action::Housekeeping);
|
||||
|
||||
insert_job(&t, 1, true).await;
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_next_job_one() {
|
||||
async fn test_load_next_job_one() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
insert_job(&t, 1, true).await;
|
||||
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
115
src/key.rs
115
src/key.rs
@@ -1,49 +1,25 @@
|
||||
//! Cryptographic key module
|
||||
//! Cryptographic key module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{format_err, Result};
|
||||
use async_trait::async_trait;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
|
||||
use crate::dc_tools::{time, EmailAddress};
|
||||
|
||||
// Re-export key types
|
||||
pub use crate::pgp::KeyPair;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
|
||||
/// Error type for deltachat key handling.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("Could not decode base64")]
|
||||
Base64Decode(#[from] base64::DecodeError),
|
||||
#[error("rPGP error: {}", _0)]
|
||||
Pgp(#[from] pgp::errors::Error),
|
||||
#[error("Failed to generate PGP key: {}", _0)]
|
||||
Keygen(#[from] crate::pgp::PgpKeygenError),
|
||||
#[error("Failed to save generated key: {}", _0)]
|
||||
StoreKey(#[from] SaveKeyError),
|
||||
#[error("No address configured")]
|
||||
NoConfiguredAddr,
|
||||
#[error("Configured address is invalid: {}", _0)]
|
||||
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
||||
#[error("no data provided")]
|
||||
Empty,
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Convenience trait for working with keys.
|
||||
///
|
||||
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
||||
@@ -74,7 +50,8 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
/// the ASCII-armored representation.
|
||||
fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap<String, String>)> {
|
||||
let bytes = data.as_bytes();
|
||||
Self::KeyType::from_armor_single(Cursor::new(bytes)).map_err(Error::Pgp)
|
||||
Self::KeyType::from_armor_single(Cursor::new(bytes))
|
||||
.map_err(|err| format_err!("rPGP error: {}", err))
|
||||
}
|
||||
|
||||
/// Load the users' default key from the database.
|
||||
@@ -225,7 +202,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.ok_or(Error::NoConfiguredAddr)?;
|
||||
.ok_or_else(|| format_err!("No address configured"))?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
@@ -284,24 +261,6 @@ pub enum KeyPairUse {
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
/// Error saving a keypair to the database.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("SaveKeyError: {message}")]
|
||||
pub struct SaveKeyError {
|
||||
message: String,
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
}
|
||||
|
||||
impl SaveKeyError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the keypair as an owned keypair for addr in the database.
|
||||
///
|
||||
/// This will save the keypair as keys for the given address. The
|
||||
@@ -318,7 +277,7 @@ pub async fn store_self_keypair(
|
||||
context: &Context,
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> std::result::Result<(), SaveKeyError> {
|
||||
) -> Result<()> {
|
||||
// Everything should really be one transaction, more refactoring
|
||||
// is needed for that.
|
||||
let public_key = DcKey::to_bytes(&keypair.public);
|
||||
@@ -330,13 +289,13 @@ pub async fn store_self_keypair(
|
||||
paramsv![public_key, secret_key],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
||||
.map_err(|err| err.context("failed to remove old use of key"))?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||
.map_err(|err| err.context("failed to clear default"))?;
|
||||
}
|
||||
let is_default = match default {
|
||||
KeyPairUse::Default => true as i32,
|
||||
@@ -354,7 +313,7 @@ pub async fn store_self_keypair(
|
||||
paramsv![addr, is_default, public_key, secret_key, t],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
|
||||
.map_err(|err| err.context("failed to insert keypair"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -364,10 +323,10 @@ pub async fn store_self_keypair(
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
pub fn new(v: Vec<u8>) -> std::result::Result<Fingerprint, FingerprintError> {
|
||||
pub fn new(v: Vec<u8>) -> Result<Fingerprint> {
|
||||
match v.len() {
|
||||
20 => Ok(Fingerprint(v)),
|
||||
_ => Err(FingerprintError::WrongLength),
|
||||
_ => Err(format_err!("Wrong fingerprint length")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +365,7 @@ impl fmt::Display for Fingerprint {
|
||||
|
||||
/// Parse a human-readable or otherwise formatted fingerprint.
|
||||
impl std::str::FromStr for Fingerprint {
|
||||
type Err = FingerprintError;
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let hex_repr: String = input
|
||||
@@ -420,21 +379,11 @@ impl std::str::FromStr for Fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FingerprintError {
|
||||
#[error("Invalid hex characters")]
|
||||
NotHex(#[from] hex::FromHexError),
|
||||
#[error("Incorrect fingerprint lengths")]
|
||||
WrongLength,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use async_std::sync::Arc;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
@@ -561,8 +510,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
|
||||
@@ -572,7 +520,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedPublicKey::load_self(&t).await;
|
||||
@@ -582,7 +530,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedSecretKey::load_self(&t).await;
|
||||
@@ -594,7 +542,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
use std::thread;
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let thr0 = {
|
||||
@@ -640,27 +588,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
assert_eq!(nrows().await, 1);
|
||||
}
|
||||
|
||||
// Convenient way to create a new key if you need one, run with
|
||||
// `cargo test key::tests::gen_key`.
|
||||
// #[test]
|
||||
// fn gen_key() {
|
||||
// let name = "fiona";
|
||||
// let keypair = crate::pgp::create_keypair(
|
||||
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-public.asc", name),
|
||||
// keypair.public.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-secret.asc", name),
|
||||
// keypair.secret.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint_from_str() {
|
||||
let res = Fingerprint::new(vec![
|
||||
@@ -676,13 +603,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
.unwrap();
|
||||
assert_eq!(fp, res);
|
||||
|
||||
let err = "1".parse::<Fingerprint>().err().unwrap();
|
||||
match err {
|
||||
FingerprintError::NotHex(_) => (),
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
let src_err = err.source().unwrap().downcast_ref::<hex::FromHexError>();
|
||||
assert_eq!(src_err, Some(&hex::FromHexError::OddLength));
|
||||
assert!("1".parse::<Fingerprint>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user