mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 13:32:11 +03:00
Compare commits
334 Commits
test-ecc-b
...
disable-wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8089b0443d | ||
|
|
6483b8c138 | ||
|
|
9702647044 | ||
|
|
3b2192a046 | ||
|
|
b864911e18 | ||
|
|
9e0506cb48 | ||
|
|
a0000b9489 | ||
|
|
df3fac4e5e | ||
|
|
8a36f55439 | ||
|
|
ab253744f8 | ||
|
|
fff1eaba45 | ||
|
|
eafb7b979d | ||
|
|
9e22bf39cc | ||
|
|
fc6691ce5b | ||
|
|
598dc86ca5 | ||
|
|
3e2bfc35e3 | ||
|
|
6658ad8618 | ||
|
|
759ccdbee2 | ||
|
|
635060a02d | ||
|
|
a128e7e7ab | ||
|
|
3cb69b8035 | ||
|
|
7dc58bb330 | ||
|
|
8bd0a62cb3 | ||
|
|
3ee81cbee0 | ||
|
|
4b744337fe | ||
|
|
8d7d2f7a44 | ||
|
|
24bf1dbffb | ||
|
|
c1890bb126 | ||
|
|
a4a570896a | ||
|
|
0594034ee6 | ||
|
|
396e376f49 | ||
|
|
fca9eae0fd | ||
|
|
f0d9bdd901 | ||
|
|
c185d5b0b5 | ||
|
|
6ece2a3449 | ||
|
|
682d52441d | ||
|
|
c2c0c81f1c | ||
|
|
fe23907eb3 | ||
|
|
e2bf8a8a11 | ||
|
|
3f0136ae7c | ||
|
|
9f992409c7 | ||
|
|
cd53ed16e9 | ||
|
|
cc56edc91d | ||
|
|
25eb4b3547 | ||
|
|
c5eb112f5a | ||
|
|
8d904f415a | ||
|
|
c34173ca6e | ||
|
|
aa292ac6b8 | ||
|
|
c36227e2fc | ||
|
|
a406e0416f | ||
|
|
215cc5e71d | ||
|
|
0e72acee10 | ||
|
|
000ed3175d | ||
|
|
2f6bae4e2a | ||
|
|
0fefe11bfd | ||
|
|
2dbb1bbbea | ||
|
|
a586a1d525 | ||
|
|
4724101e75 | ||
|
|
1b921cd533 | ||
|
|
fcf3786fc5 | ||
|
|
d78f75aa60 | ||
|
|
56056cf10e | ||
|
|
5fd9b20213 | ||
|
|
076cdae3fd | ||
|
|
6543c7c26f | ||
|
|
3035c8af30 | ||
|
|
3cbd647dad | ||
|
|
4efcbee772 | ||
|
|
bb59cf94e9 | ||
|
|
96436814f5 | ||
|
|
e8763e936d | ||
|
|
c41a6b87b8 | ||
|
|
54395a7252 | ||
|
|
4322b8b932 | ||
|
|
f444af825f | ||
|
|
0d0e7f774e | ||
|
|
30ed27ae5c | ||
|
|
b7283487b2 | ||
|
|
551f7dc05a | ||
|
|
1b485770b6 | ||
|
|
50e18f84c2 | ||
|
|
9eab96090d | ||
|
|
737a741a54 | ||
|
|
46253039df | ||
|
|
2f5b6a115d | ||
|
|
05d63fcbe6 | ||
|
|
ae5bc76123 | ||
|
|
b70e92effb | ||
|
|
2a9b967d2d | ||
|
|
459fec56db | ||
|
|
bfdd6f36e2 | ||
|
|
432e4b7f0a | ||
|
|
39cb9c425c | ||
|
|
dff1ae0fb4 | ||
|
|
2943624439 | ||
|
|
94064e526a | ||
|
|
95cac4dfb9 | ||
|
|
46fb6a21ee | ||
|
|
3d6ca973c4 | ||
|
|
fc03f4c87a | ||
|
|
502ec2a56f | ||
|
|
66d5c3f620 | ||
|
|
220500efbb | ||
|
|
d29c5eabbb | ||
|
|
979d7c5625 | ||
|
|
4e828199c8 | ||
|
|
61c84c8e01 | ||
|
|
30be5e33d7 | ||
|
|
b1c524d4a3 | ||
|
|
7c33c7f7da | ||
|
|
7846c23edd | ||
|
|
6c0dd8543d | ||
|
|
70c082a1b1 | ||
|
|
02cda1e611 | ||
|
|
a1c82eaea6 | ||
|
|
9eda710538 | ||
|
|
a87a2d0b71 | ||
|
|
6e2f4d85a3 | ||
|
|
511727fdfa | ||
|
|
a86c7c767a | ||
|
|
da88d8f17f | ||
|
|
dbd3705441 | ||
|
|
8d2f526ee7 | ||
|
|
b075a73222 | ||
|
|
eb5387ca38 | ||
|
|
1eaef94c71 | ||
|
|
e1903edd04 | ||
|
|
0b6b8ced92 | ||
|
|
cc6ce72f6e | ||
|
|
857a384d8b | ||
|
|
13dd88b7ad | ||
|
|
e85cdc8c9f | ||
|
|
1760740a4c | ||
|
|
daf40fde82 | ||
|
|
e909b8199b | ||
|
|
ec089faf3a | ||
|
|
9fcb30ac33 | ||
|
|
cb92579461 | ||
|
|
0327000f8d | ||
|
|
3a91c87c73 | ||
|
|
db5b5d321b | ||
|
|
76b7e7408a | ||
|
|
324e5d0258 | ||
|
|
d997bbc081 | ||
|
|
1c21d4f356 | ||
|
|
5f574cf283 | ||
|
|
92f1e6da1e | ||
|
|
24aa3c781b | ||
|
|
32bd6109e3 | ||
|
|
52442017e2 | ||
|
|
278454287c | ||
|
|
5ded8fb400 | ||
|
|
24730e7ad6 | ||
|
|
016a780632 | ||
|
|
6d89638ca4 | ||
|
|
fdc091319b | ||
|
|
960c8745d9 | ||
|
|
134b09dba5 | ||
|
|
76b93274e8 | ||
|
|
ea455323d8 | ||
|
|
1855f84fe0 | ||
|
|
323d996d5f | ||
|
|
1b858393c5 | ||
|
|
ca88c5b41c | ||
|
|
7e1470ea46 | ||
|
|
f98d0bbc1f | ||
|
|
2a34022619 | ||
|
|
57f879a6ba | ||
|
|
d4ba09c753 | ||
|
|
6c3a8448cf | ||
|
|
d4e1c1b109 | ||
|
|
a1d5120e58 | ||
|
|
724e1ea97e | ||
|
|
6f8067ffd3 | ||
|
|
f38386d164 | ||
|
|
d66829702f | ||
|
|
36b50436d7 | ||
|
|
33dd747ec7 | ||
|
|
d8e14d9993 | ||
|
|
f61b9f7964 | ||
|
|
91cdc76414 | ||
|
|
a1379f61da | ||
|
|
a665d6de59 | ||
|
|
6a6a719ab6 | ||
|
|
84f17b7539 | ||
|
|
57141e478c | ||
|
|
6213917089 | ||
|
|
fb33c31378 | ||
|
|
79f5e736b0 | ||
|
|
0d4b6f5627 | ||
|
|
5c8f558f60 | ||
|
|
c851f9d5a3 | ||
|
|
2d74514dd0 | ||
|
|
84012e760e | ||
|
|
6baef49f9d | ||
|
|
f55d4fa73a | ||
|
|
ce00c627d4 | ||
|
|
d3c6f530e2 | ||
|
|
cf6391d51b | ||
|
|
57311d731e | ||
|
|
95d45b386f | ||
|
|
bbc8bed39c | ||
|
|
ec67b3975c | ||
|
|
e9967c32e6 | ||
|
|
493a213d41 | ||
|
|
f65dbee74b | ||
|
|
f51fd1267f | ||
|
|
711f3f69da | ||
|
|
24f4cbbb27 | ||
|
|
d31265895d | ||
|
|
6e35a879a3 | ||
|
|
9c2a3b8a82 | ||
|
|
916fab7d4b | ||
|
|
3163ef87c6 | ||
|
|
1934181b52 | ||
|
|
1b815a7d96 | ||
|
|
4e0a08106d | ||
|
|
e8cc739fbd | ||
|
|
fc57cbfb49 | ||
|
|
7522fec044 | ||
|
|
237dabb907 | ||
|
|
3686048ab6 | ||
|
|
2bf4c5d7e7 | ||
|
|
051d80b2f3 | ||
|
|
adaa1e856c | ||
|
|
4daa57c98e | ||
|
|
7e67b2cbb3 | ||
|
|
270d18a88a | ||
|
|
25f8a735a9 | ||
|
|
9eb672ea17 | ||
|
|
9febc762da | ||
|
|
4b742c220c | ||
|
|
9d03d441e1 | ||
|
|
ff8b249cc6 | ||
|
|
248e6ea5e7 | ||
|
|
be0afdebfd | ||
|
|
9f19d20344 | ||
|
|
aea8a32ba5 | ||
|
|
d1a4c82937 | ||
|
|
4f73812673 | ||
|
|
33150615a1 | ||
|
|
491f83c86d | ||
|
|
41f776763b | ||
|
|
65fdfac866 | ||
|
|
cb0c00bc6d | ||
|
|
ad53678c19 | ||
|
|
62097765a6 | ||
|
|
efb7280e99 | ||
|
|
bdb2a47743 | ||
|
|
c4677190be | ||
|
|
055aba189c | ||
|
|
314c3d5e78 | ||
|
|
6db03356b5 | ||
|
|
28af919b09 | ||
|
|
8f7a456a39 | ||
|
|
5b3bec1aac | ||
|
|
f2aa17c9d0 | ||
|
|
bc06b9e051 | ||
|
|
6d216af507 | ||
|
|
b2f1d9f376 | ||
|
|
a653e469f2 | ||
|
|
4f4241ba3a | ||
|
|
2cf9c68040 | ||
|
|
cc0f977d6f | ||
|
|
7879952fde | ||
|
|
4452cab987 | ||
|
|
98bd64621a | ||
|
|
c1c769ceb0 | ||
|
|
d64e55c66f | ||
|
|
76fc84be37 | ||
|
|
8cd5f5990e | ||
|
|
6ffe54d68f | ||
|
|
d78ea882c8 | ||
|
|
958802a233 | ||
|
|
00b02efdc2 | ||
|
|
50569f12f5 | ||
|
|
8aa4ceb570 | ||
|
|
a7afbf85ad | ||
|
|
8fdf3dcdb8 | ||
|
|
818c20e0cb | ||
|
|
c1d4996777 | ||
|
|
fd3e6e0ee4 | ||
|
|
edc5754c68 | ||
|
|
bd75dea000 | ||
|
|
e09a0a548f | ||
|
|
15ee8b4362 | ||
|
|
ab8d75b192 | ||
|
|
e135c969c9 | ||
|
|
36e7090466 | ||
|
|
f28f177c6b | ||
|
|
785973c624 | ||
|
|
9c06acff72 | ||
|
|
4fabddeb47 | ||
|
|
17ff1ab372 | ||
|
|
3c34096392 | ||
|
|
70e0d3b571 | ||
|
|
ae5a2396f3 | ||
|
|
8f82bf40e0 | ||
|
|
fe398de2fa | ||
|
|
a770d75e2e | ||
|
|
a330104e9b | ||
|
|
aae3cae4bb | ||
|
|
e7e4821804 | ||
|
|
9654802acc | ||
|
|
06a24fa4d0 | ||
|
|
62b1b0519a | ||
|
|
10afdfecdd | ||
|
|
c0e08fb927 | ||
|
|
6d6bc9b050 | ||
|
|
4714fb6887 | ||
|
|
5f47810964 | ||
|
|
0f6024e055 | ||
|
|
fafc15f80c | ||
|
|
9a85ea861d | ||
|
|
9541960307 | ||
|
|
95073deb96 | ||
|
|
82b4647b95 | ||
|
|
0c770a8b37 | ||
|
|
0e4031348f | ||
|
|
5cc26762c2 | ||
|
|
b8b4853d1f | ||
|
|
fbabe27fc1 | ||
|
|
4d1554c85b | ||
|
|
ab40495d5c | ||
|
|
42ebf49f92 | ||
|
|
c5ccd88f79 | ||
|
|
dbd1b227d9 | ||
|
|
63baac3c61 | ||
|
|
7c39bb6659 | ||
|
|
13e361aabc | ||
|
|
d1a26e66a7 | ||
|
|
ffe3c84e7c | ||
|
|
702c7382a7 | ||
|
|
b138d486e4 |
@@ -7,6 +7,10 @@ executors:
|
||||
doxygen:
|
||||
docker:
|
||||
- image: hrektts/doxygen
|
||||
python:
|
||||
docker:
|
||||
- image: 3.7.7-stretch
|
||||
|
||||
|
||||
restore-workspace: &restore-workspace
|
||||
attach_workspace:
|
||||
|
||||
8
.github/workflows/code-quality.yml
vendored
8
.github/workflows/code-quality.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2019-11-06
|
||||
toolchain: nightly-2020-03-12
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2019-11-06
|
||||
toolchain: nightly-2020-03-12
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly-2019-11-06
|
||||
toolchain: nightly-2020-03-12
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
args: --all-features
|
||||
|
||||
104
CHANGELOG.md
104
CHANGELOG.md
@@ -1,5 +1,109 @@
|
||||
# Changelog
|
||||
|
||||
## 1.33.0
|
||||
|
||||
- let `dc_set_muted()` also mute one-to-one chats #1470
|
||||
|
||||
- fix a bug that led to load and traffic if the server does not use sent-folder
|
||||
#1472
|
||||
|
||||
|
||||
## 1.32.0
|
||||
|
||||
- fix endless loop when trying to download messages with bad RFC Message-ID,
|
||||
also be more reliable on similar errors #1463 #1466 #1462
|
||||
|
||||
- fix bug with comma in contact request #1438
|
||||
|
||||
- do not refer to hidden messages on replies #1459
|
||||
|
||||
- improve error handling #1468 #1465 #1464
|
||||
|
||||
|
||||
## 1.31.0
|
||||
|
||||
- always describe the context of the displayed error #1451
|
||||
|
||||
- do not emit `DC_EVENT_ERROR` when message sending fails;
|
||||
`dc_msg_get_state()` and `dc_get_msg_info()` are sufficient #1451
|
||||
|
||||
- new config-option `media_quality` #1449
|
||||
|
||||
- try over if writing message to database fails #1447
|
||||
|
||||
|
||||
## 1.30.0
|
||||
|
||||
- expunge deleted messages #1440
|
||||
|
||||
- do not send `DC_EVENT_MSGS_CHANGED|INCOMING_MSG` on hidden messages #1439
|
||||
|
||||
|
||||
## 1.29.0
|
||||
|
||||
- new config options `delete_device_after` and `delete_server_after`,
|
||||
each taking an amount of seconds after which messages
|
||||
are deleted from the device and/or the server #1310 #1335 #1411 #1417 #1423
|
||||
|
||||
- new api `dc_estimate_deletion_cnt()` to estimate the effect
|
||||
of `delete_device_after` and `delete_server_after`
|
||||
|
||||
- use Ed25519 keys by default, these keys are much shorter
|
||||
than RSA keys, which results in saving traffic and speed improvements #1362
|
||||
|
||||
- improve message ellipsizing #1397 #1430
|
||||
|
||||
- emit `DC_EVENT_ERROR_NETWORK` also on smtp-errors #1378
|
||||
|
||||
- do not show badly formatted non-delta-messages as empty #1384
|
||||
|
||||
- try over SMTP on potentially recoverable error 5.5.0 #1379
|
||||
|
||||
- remove device-chat from forward-to-chat-list #1367
|
||||
|
||||
- improve group-handling #1368
|
||||
|
||||
- `dc_get_info()` returns uptime (how long the context is in use)
|
||||
|
||||
- python improvements and adaptions #1408 #1415
|
||||
|
||||
- log to the stdout and stderr in tests #1416
|
||||
|
||||
- refactoring, code improvements #1363 #1365 #1366 #1370 #1375 #1389 #1390 #1418 #1419
|
||||
|
||||
- removed api: `dc_chat_get_subtitle()`, `dc_get_version_str()`, `dc_array_add_id()`
|
||||
|
||||
- removed events: `DC_EVENT_MEMBER_ADDED`, `DC_EVENT_MEMBER_REMOVED`
|
||||
|
||||
|
||||
## 1.28.0
|
||||
|
||||
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
|
||||
that will sort the "saved messages" chat to the top of the chatlist #1336
|
||||
- mark mails as being deleted from server in dc_empty_server() #1333
|
||||
- fix interaction with servers that do not allow folder creation on root-level;
|
||||
use path separator as defined by the email server #1359
|
||||
- fix group creation if group was created by non-delta clients #1357
|
||||
- fix showing replies from non-delta clients #1353
|
||||
- fix member list on rejoining left groups #1343
|
||||
- fix crash when using empty groups #1354
|
||||
- fix potential crash on special names #1350
|
||||
|
||||
|
||||
## 1.27.0
|
||||
|
||||
- handle keys reliably on armv7 #1327
|
||||
|
||||
|
||||
## 1.26.0
|
||||
|
||||
- change generated key type back to RSA as shipped versions
|
||||
have problems to encrypt to Ed25519 keys
|
||||
|
||||
- update rPGP to encrypt reliably to Ed25519 keys;
|
||||
one of the next versions can finally use Ed25519 keys then
|
||||
|
||||
|
||||
## 1.25.0
|
||||
|
||||
- save traffic by downloading only messages that are really displayed #1236
|
||||
|
||||
591
Cargo.lock
generated
591
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.25.0"
|
||||
version = "1.33.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -12,7 +12,7 @@ lto = true
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
libc = "0.2.51"
|
||||
pgp = { version = "0.4.0", default-features = false }
|
||||
pgp = { version = "0.5.1", default-features = false }
|
||||
hex = "0.4.0"
|
||||
sha2 = "0.8.0"
|
||||
rand = "0.7.0"
|
||||
@@ -32,8 +32,6 @@ percent-encoding = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4.6"
|
||||
failure = "0.1.5"
|
||||
failure_derive = "0.1.5"
|
||||
indexmap = "1.3.0"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.1.6"
|
||||
@@ -53,13 +51,15 @@ bitflags = "1.1.0"
|
||||
debug_stub_derive = "0.3.0"
|
||||
sanitize-filename = "0.2.1"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = "0.10.2"
|
||||
mailparse = "0.12.0"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
native-tls = "0.2.3"
|
||||
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
||||
pretty_env_logger = "0.3.1"
|
||||
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
thiserror = "1.0.14"
|
||||
anyhow = "1.0.28"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
@@ -84,7 +84,7 @@ required-features = ["rustyline"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["nightly", "ringbuf"]
|
||||
default = ["nightly"]
|
||||
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
ringbuf = ["pgp/ringbuf"]
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ curl https://sh.rustup.rs -sSf | sh
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
|
||||
```
|
||||
cargo run --example repl -- /path/to/db
|
||||
cargo run --example repl -- ~/deltachat-db
|
||||
```
|
||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||
|
||||
Configure your account (if not already configured):
|
||||
|
||||
@@ -108,7 +109,6 @@ $ cargo test -- --ignored
|
||||
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
- `nightly`: Enable nightly only performance and security related features.
|
||||
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
|
||||
|
||||
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
|
||||
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
|
||||
|
||||
@@ -4,7 +4,7 @@ environment:
|
||||
|
||||
install:
|
||||
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
||||
- rustup-init -yv --default-toolchain nightly-2019-07-10
|
||||
- rustup-init -yv --default-toolchain nightly-2020-03-12
|
||||
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
|
||||
@@ -37,7 +37,7 @@ echo -----------------------
|
||||
# Bundle external shared libraries into the wheels
|
||||
pushd $WHEELHOUSEDIR
|
||||
|
||||
pip3 install -U pip
|
||||
pip3 install -U pip setuptools
|
||||
pip3 install devpi-client
|
||||
devpi use https://m.devpi.net
|
||||
devpi login dc --password $DEVPI_LOGIN
|
||||
|
||||
@@ -48,7 +48,7 @@ def run():
|
||||
projectnames = get_projectnames(baseurl, username, indexname)
|
||||
if indexname == "master" or not indexname:
|
||||
continue
|
||||
assert projectnames == ["deltachat"]
|
||||
clear_index = not projectnames
|
||||
for projectname in projectnames:
|
||||
dates = get_release_dates(baseurl, username, indexname, projectname)
|
||||
if not dates:
|
||||
@@ -60,8 +60,11 @@ def run():
|
||||
date = datetime.datetime(*max(dates))
|
||||
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
||||
assert username and indexname
|
||||
url = baseurl + username + "/" + indexname
|
||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||
clear_index = True
|
||||
break
|
||||
if clear_index:
|
||||
url = baseurl + username + "/" + indexname
|
||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-11-06 -y
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-03-12 -y
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
rustc --version
|
||||
|
||||
# remove some 300-400 MB that we don't need for automated builds
|
||||
rm -rf /root/.rustup/toolchains/nightly-2019-11-06-x86_64-unknown-linux-gnu/share/
|
||||
rm -rf /root/.rustup/toolchains/nightly-2020-03-12-x86_64-unknown-linux-gnu/share/
|
||||
|
||||
@@ -46,6 +46,7 @@ if [ -n "$TESTS" ]; then
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||
popd
|
||||
|
||||
@@ -32,11 +32,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
cd $BUILDDIR
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_PY_LIVECONFIG \
|
||||
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ ssh $SSHTARGET <<_HERE
|
||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||
export TARGET=release
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
#rm -rf virtualenv venv
|
||||
|
||||
@@ -37,7 +37,8 @@ mkdir -p $TOXWORKDIR
|
||||
# XXX we may switch on some live-tests on for better ensurances
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.25.0"
|
||||
version = "1.33.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -19,11 +19,11 @@ deltachat = { path = "../", default-features = false }
|
||||
libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
failure = "0.1.6"
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0.28"
|
||||
thiserror = "1.0.14"
|
||||
|
||||
[features]
|
||||
default = ["vendored", "nightly", "ringbuf"]
|
||||
default = ["vendored", "nightly"]
|
||||
vendored = ["deltachat/vendored"]
|
||||
nightly = ["deltachat/nightly"]
|
||||
ringbuf = ["deltachat/ringbuf"]
|
||||
|
||||
@@ -41,7 +41,7 @@ typedef struct _dc_provider dc_provider_t;
|
||||
* uintptr_t event_handler_func(dc_context_t* context, int event,
|
||||
* uintptr_t data1, uintptr_t data2)
|
||||
* {
|
||||
* return 0; // for unhandled events, it is always safe to return 0
|
||||
* return 0;
|
||||
* }
|
||||
*
|
||||
* dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL);
|
||||
@@ -208,7 +208,7 @@ typedef struct _dc_provider dc_provider_t;
|
||||
* @param event one of the @ref DC_EVENT constants
|
||||
* @param data1 depends on the event parameter
|
||||
* @param data2 depends on the event parameter
|
||||
* @return return 0 unless stated otherwise in the event parameter documentation
|
||||
* @return events do not expect a return value, just always return 0
|
||||
*/
|
||||
typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t data1, uintptr_t data2);
|
||||
|
||||
@@ -229,7 +229,7 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
|
||||
* otherwise!
|
||||
* - The callback SHOULD return _fast_, for GUI updates etc. you should
|
||||
* post yourself an asynchronous message to your GUI thread, if needed.
|
||||
* - If not mentioned otherweise, the callback should return 0.
|
||||
* - events do not expect a return value, just always return 0.
|
||||
* @param userdata can be used by the client for any purpuse. He finds it
|
||||
* later in dc_get_userdata().
|
||||
* @param os_name is only for decorative use
|
||||
@@ -342,7 +342,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way eg. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display eg. in email footers, defaults to a standard text
|
||||
* - `selfavatar` = File containing avatar. Will be copied to blob directory.
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* NULL to remove the avatar.
|
||||
* It is planned for future versions
|
||||
* to send this image together with the next messages.
|
||||
@@ -373,6 +374,25 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `save_mime_headers` = 1=save mime headers
|
||||
* and make dc_get_mime_headers() work for subsequent calls,
|
||||
* 0=do not save mime headers (default)
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||
* 1=delete messages directly after receiving from server, mvbox is skipped.
|
||||
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
|
||||
* "Saved messages" are deleted from the server as well as
|
||||
* emails matching the `show_emails` settings above, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||
* DC_MEDIA_QUALITY_WORSE (1)
|
||||
* allow worse images/videos/voice quality to gain smaller sizes,
|
||||
* suitable for providers or areas known to have a bad connection.
|
||||
* In contrast to other options, the implementation of this option is currently up to the UIs;
|
||||
* this may change in future, however,
|
||||
* having the option in the core allows provider-specific-defaults already today.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -901,6 +921,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
#define DC_GCL_ARCHIVED_ONLY 0x01
|
||||
#define DC_GCL_NO_SPECIALS 0x02
|
||||
#define DC_GCL_ADD_ALLDONE_HINT 0x04
|
||||
#define DC_GCL_FOR_FORWARDING 0x08
|
||||
|
||||
|
||||
/**
|
||||
@@ -939,6 +960,10 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
||||
* chats
|
||||
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
* and hides the "Device chat" and the deaddrop.
|
||||
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
* to also hide the archive link.
|
||||
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
* to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||
@@ -1292,6 +1317,22 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
||||
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* This includes emails downloaded due to the `show_emails` option.
|
||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
*/
|
||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all _fresh_ messages of any chat.
|
||||
* Typically used for implementing notification summaries.
|
||||
@@ -1586,8 +1627,10 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID to set the image for.
|
||||
* @param image Full path of the image to use as the group image. If you pass NULL here,
|
||||
* the group image is deleted (for promoted groups, all members are informed about this change anyway).
|
||||
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
|
||||
* this change anyway).
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
|
||||
@@ -1597,7 +1640,7 @@ int dc_set_chat_profile_image (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Set mute duration of a chat.
|
||||
*
|
||||
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
|
||||
* The ui can then call dc_chat_is_muted() when receiving a new message to decide whether it should trigger an notification.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED.
|
||||
*
|
||||
@@ -1655,8 +1698,9 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
|
||||
*/
|
||||
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
/**
|
||||
/*
|
||||
* Empty IMAP server folder: delete all messages.
|
||||
* Deprecated, use dc_set_config() with the key "delete_server_after" instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new()
|
||||
@@ -1759,7 +1803,7 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
|
||||
/**
|
||||
* Check if an e-mail address belongs to a known and unblocked contact.
|
||||
* Known and unblocked contacts will be returned by dc_get_contacts().
|
||||
* To get a list of all known and unblocked contacts, use dc_get_contacts().
|
||||
*
|
||||
* To validate an e-mail address independently of the contact database
|
||||
* use dc_may_be_valid_addr().
|
||||
@@ -1767,7 +1811,8 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param addr The e-mail-address to check.
|
||||
* @return 1=address is a contact in use, 0=address is not a contact in use.
|
||||
* @return Contact ID of the contact belonging to the e-mail-address
|
||||
* or 0 if there is no contact that is or was introduced by an accepted contact.
|
||||
*/
|
||||
uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char* addr);
|
||||
|
||||
@@ -2800,19 +2845,6 @@ int dc_chat_get_type (const dc_chat_t* chat);
|
||||
char* dc_chat_get_name (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/*
|
||||
* Get a subtitle for a chat. The subtitle is eg. the email-address or the
|
||||
* number of group members.
|
||||
*
|
||||
* Deprecated function. Subtitles should be created in the ui
|
||||
* where plural forms and other specials can be handled more gracefully.
|
||||
*
|
||||
* @param chat The chat object to calulate the subtitle for.
|
||||
* @return Subtitle as a string. Must be released using dc_str_unref() after usage. Never NULL.
|
||||
*/
|
||||
char* dc_chat_get_subtitle (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get the chat's profile image.
|
||||
* For groups, this is the image set by any group member
|
||||
@@ -2946,7 +2978,7 @@ int dc_chat_is_sending_locations (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the chat is currently muted
|
||||
* Check whether the chat is currently muted (can be changed by dc_set_chat_mute_duration()).
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -4124,28 +4156,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_EMPTY DC_EMPTY
|
||||
*
|
||||
* These constants configure emptying imap folders with dc_empty_server()
|
||||
*
|
||||
* @addtogroup DC_EMPTY
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clear all mvbox messages.
|
||||
*/
|
||||
#define DC_EMPTY_MVBOX 0x01
|
||||
|
||||
/**
|
||||
* Clear all INBOX messages.
|
||||
*/
|
||||
#define DC_EMPTY_INBOX 0x02
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
#define DC_EMPTY_MVBOX 0x01 // Deprecated, flag for dc_empty_server(): Clear all mvbox messages
|
||||
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
|
||||
|
||||
|
||||
/**
|
||||
@@ -4153,8 +4165,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* These constants are used as events
|
||||
* reported to the callback given to dc_context_new().
|
||||
* If you do not want to handle an event, it is always safe to return 0,
|
||||
* so there is no need to add a "case" for every event.
|
||||
*
|
||||
* @addtogroup DC_EVENT
|
||||
* @{
|
||||
@@ -4169,7 +4179,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_INFO 100
|
||||
|
||||
@@ -4180,7 +4189,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_SMTP_CONNECTED 101
|
||||
|
||||
@@ -4191,7 +4199,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_IMAP_CONNECTED 102
|
||||
|
||||
@@ -4201,7 +4208,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_SMTP_MESSAGE_SENT 103
|
||||
|
||||
@@ -4211,7 +4217,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
|
||||
|
||||
@@ -4221,7 +4226,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
||||
|
||||
@@ -4231,7 +4235,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) folder name.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
|
||||
|
||||
@@ -4241,7 +4244,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) path name
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_NEW_BLOB_FILE 150
|
||||
|
||||
@@ -4251,7 +4253,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) path name
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_DELETED_BLOB_FILE 151
|
||||
|
||||
@@ -4264,7 +4265,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 0
|
||||
* @param data2 (const char*) Warning string in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_WARNING 300
|
||||
|
||||
@@ -4287,7 +4287,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* Some error strings are taken from dc_set_stock_translation(),
|
||||
* however, most error strings will be in english language.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_ERROR 400
|
||||
|
||||
@@ -4311,7 +4310,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* 0=subsequent network error, should be logged only
|
||||
* @param data2 (const char*) Error string, always set, never NULL.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_ERROR_NETWORK 401
|
||||
|
||||
@@ -4327,7 +4325,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data2 (const char*) Info string in english language.
|
||||
* Must not be unref'd or modified
|
||||
* and is valid only until the callback returns.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
|
||||
|
||||
@@ -4341,7 +4338,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) chat_id for single added messages
|
||||
* @param data2 (int) msg_id for single added messages
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_MSGS_CHANGED 2000
|
||||
|
||||
@@ -4354,7 +4350,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_MSG 2005
|
||||
|
||||
@@ -4365,7 +4360,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_MSG_DELIVERED 2010
|
||||
|
||||
@@ -4376,7 +4370,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_MSG_FAILED 2012
|
||||
|
||||
@@ -4387,7 +4380,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_MSG_READ 2015
|
||||
|
||||
@@ -4400,7 +4392,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 0
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_CHAT_MODIFIED 2020
|
||||
|
||||
@@ -4410,7 +4401,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
|
||||
* @param data2 0
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_CONTACTS_CHANGED 2030
|
||||
|
||||
@@ -4423,7 +4413,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* If the locations of several contacts have been changed,
|
||||
* eg. after calling dc_delete_all_locations(), this parameter is set to 0.
|
||||
* @param data2 0
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_LOCATION_CHANGED 2035
|
||||
|
||||
@@ -4433,7 +4422,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
* @param data2 0
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_CONFIGURE_PROGRESS 2041
|
||||
|
||||
@@ -4443,7 +4431,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
* @param data2 0
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_IMEX_PROGRESS 2051
|
||||
|
||||
@@ -4458,7 +4445,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data1 (const char*) Path and file name.
|
||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||
* @param data2 0
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_IMEX_FILE_WRITTEN 2052
|
||||
|
||||
@@ -4476,7 +4462,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
* 1000=Protocol finished for this contact.
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
||||
|
||||
@@ -4492,21 +4477,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @param data2 (int) Progress as:
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
|
||||
|
||||
/**
|
||||
* This event is sent out to the inviter when a joiner successfully joined a group.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) contact_id
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_MEMBER_ADDED 2062
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -4522,8 +4495,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499)
|
||||
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
|
||||
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
|
||||
char* dc_get_version_str (void); // deprecated
|
||||
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
|
||||
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
|
||||
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
|
||||
|
||||
@@ -4535,6 +4506,14 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
||||
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
||||
#define DC_SHOW_EMAILS_ALL 2
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("media_quality")
|
||||
*/
|
||||
#define DC_MEDIA_QUALITY_BALANCED 0
|
||||
#define DC_MEDIA_QUALITY_WORSE 1
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("key_gen_type")
|
||||
*/
|
||||
@@ -4653,8 +4632,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
||||
#define DC_STR_NOMESSAGES 1
|
||||
#define DC_STR_SELF 2
|
||||
#define DC_STR_DRAFT 3
|
||||
#define DC_STR_MEMBER 4
|
||||
#define DC_STR_CONTACT 6
|
||||
#define DC_STR_VOICEMESSAGE 7
|
||||
#define DC_STR_DEADDROP 8
|
||||
#define DC_STR_IMAGE 9
|
||||
@@ -4686,7 +4663,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
||||
#define DC_STR_STARREDMSGS 41
|
||||
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
||||
#define DC_STR_AC_SETUP_MSG_BODY 43
|
||||
#define DC_STR_SELFTALK_SUBTITLE 50
|
||||
#define DC_STR_CANNOT_LOGIN 60
|
||||
#define DC_STR_SERVER_RESPONSE 61
|
||||
#define DC_STR_MSGACTIONBYUSER 62
|
||||
|
||||
@@ -13,7 +13,7 @@ extern crate human_panic;
|
||||
extern crate num_traits;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
use std::ffi::CString;
|
||||
use std::fmt::Write;
|
||||
@@ -27,7 +27,7 @@ use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
@@ -120,17 +120,17 @@ impl ContextWrapper {
|
||||
/// Unlock the context and execute a closure with it.
|
||||
///
|
||||
/// This is like [ContextWrapper::with_inner] but uses
|
||||
/// [failure::Error] as error type. This allows you to write a
|
||||
/// [anyhow::Error] as error type. This allows you to write a
|
||||
/// closure which could produce many errors, use the `?` operator
|
||||
/// to return them and handle them all as the return of this call.
|
||||
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, failure::Error>
|
||||
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, anyhow::Error>
|
||||
where
|
||||
F: FnOnce(&Context) -> Result<T, failure::Error>,
|
||||
F: FnOnce(&Context) -> Result<T, anyhow::Error>,
|
||||
{
|
||||
let guard = self.inner.read().unwrap();
|
||||
match guard.as_ref() {
|
||||
Some(ref ctx) => ctxfn(ctx),
|
||||
None => Err(failure::err_msg("context not open")),
|
||||
None => Err(anyhow::format_err!("context not open")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,17 +196,6 @@ impl ContextWrapper {
|
||||
progress as uintptr_t,
|
||||
);
|
||||
}
|
||||
Event::SecurejoinMemberAdded {
|
||||
chat_id,
|
||||
contact_id,
|
||||
} => {
|
||||
ffi_cb(
|
||||
self,
|
||||
event_id,
|
||||
chat_id.to_u32() as uintptr_t,
|
||||
contact_id as uintptr_t,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,7 +422,7 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
|
||||
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_info()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
let guard = ffi_context.inner.read().unwrap();
|
||||
@@ -445,7 +434,7 @@ pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c
|
||||
}
|
||||
|
||||
fn render_info(
|
||||
info: HashMap<&'static str, String>,
|
||||
info: BTreeMap<&'static str, String>,
|
||||
) -> std::result::Result<String, std::fmt::Error> {
|
||||
let mut res = String::new();
|
||||
for (key, value) in &info {
|
||||
@@ -476,11 +465,6 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
||||
.unwrap_or_else(|_| ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_version_str() -> *mut libc::c_char {
|
||||
context::get_version_str().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
@@ -1045,6 +1029,25 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
||||
context: *mut dc_context_t,
|
||||
from_server: libc::c_int,
|
||||
seconds: i64,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || seconds < 0 {
|
||||
eprintln!("ignoring careless call to dc_estimate_deletion_cnt()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
message::estimate_deletion_cnt(ctx, from_server != 0, seconds).unwrap_or(0)
|
||||
as libc::c_int
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1455,7 +1458,7 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_msg_info()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
@@ -1638,7 +1641,9 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| Contact::lookup_id_by_addr(ctx, to_string_lossy(addr)))
|
||||
.with_inner(|ctx| {
|
||||
Contact::lookup_id_by_addr(ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -2075,16 +2080,6 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
|
||||
Box::from_raw(a);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_add_id(array: *mut dc_array_t, item: libc::c_uint) {
|
||||
if array.is_null() {
|
||||
eprintln!("ignoring careless call to dc_array_add_id()");
|
||||
return;
|
||||
}
|
||||
|
||||
(*array).add_id(item);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_cnt(array: *const dc_array_t) -> libc::size_t {
|
||||
if array.is_null() {
|
||||
@@ -2406,23 +2401,10 @@ pub unsafe extern "C" fn dc_chat_get_type(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.get_name().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_subtitle(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_subtitle()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
let ffi_context: &ContextWrapper = &*ffi_chat.context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| ffi_chat.chat.get_subtitle(ctx).strdup())
|
||||
.unwrap_or_else(|_| "".strdup())
|
||||
ffi_chat.chat.get_name().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2728,7 +2710,7 @@ pub unsafe extern "C" fn dc_msg_get_sort_timestamp(msg: *mut dc_msg_t) -> i64 {
|
||||
pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_text()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_text().unwrap_or_default().strdup()
|
||||
@@ -2747,8 +2729,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
ffi_msg
|
||||
.message
|
||||
.get_file(ctx)
|
||||
.and_then(|p| p.to_c_string().ok())
|
||||
.map(|cs| dc_strdup(cs.as_ptr()))
|
||||
.map(|p| p.strdup())
|
||||
.unwrap_or_else(|| "".strdup())
|
||||
})
|
||||
.unwrap_or_else(|_| "".strdup())
|
||||
@@ -2758,7 +2739,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_filename()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_filename().unwrap_or_default().strdup()
|
||||
@@ -2768,13 +2749,13 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
|
||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_filemime()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
if let Some(x) = ffi_msg.message.get_filemime() {
|
||||
x.strdup()
|
||||
} else {
|
||||
dc_strdup(ptr::null())
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3098,7 +3079,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
|
||||
pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_addr()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_addr().strdup()
|
||||
@@ -3108,7 +3089,7 @@ pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut
|
||||
pub unsafe extern "C" fn dc_contact_get_name(contact: *mut dc_contact_t) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_name().strdup()
|
||||
@@ -3120,7 +3101,7 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_display_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_display_name().strdup()
|
||||
@@ -3132,7 +3113,7 @@ pub unsafe extern "C" fn dc_contact_get_name_n_addr(
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_name_n_addr().strdup()
|
||||
@@ -3144,7 +3125,7 @@ pub unsafe extern "C" fn dc_contact_get_first_name(
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_first_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_first_name().strdup()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use failure::Fail;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
|
||||
@@ -9,7 +8,7 @@ use std::ptr;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,norun
|
||||
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
||||
/// use crate::string::{dc_strdup, to_string_lossy};
|
||||
/// unsafe {
|
||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
||||
/// let str_a_copy = dc_strdup(str_a);
|
||||
@@ -17,7 +16,7 @@ use std::ptr;
|
||||
/// assert_ne!(str_a, str_a_copy);
|
||||
/// }
|
||||
/// ```
|
||||
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
let ret: *mut libc::c_char;
|
||||
if !s.is_null() {
|
||||
ret = libc::strdup(s);
|
||||
@@ -31,13 +30,13 @@ pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
}
|
||||
|
||||
/// Error type for the [OsStrExt] trait
|
||||
#[derive(Debug, Fail, PartialEq)]
|
||||
pub enum CStringError {
|
||||
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||
pub(crate) enum CStringError {
|
||||
/// The string contains an interior null byte
|
||||
#[fail(display = "String contains an interior null byte")]
|
||||
#[error("String contains an interior null byte")]
|
||||
InteriorNullByte,
|
||||
/// The string is not valid Unicode
|
||||
#[fail(display = "String is not valid unicode")]
|
||||
#[error("String is not valid unicode")]
|
||||
NotUnicode,
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ pub enum CStringError {
|
||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
||||
/// }
|
||||
/// ```
|
||||
pub trait OsStrExt {
|
||||
pub(crate) trait OsStrExt {
|
||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
||||
///
|
||||
/// This is useful to convert e.g. a [std::path::Path] to
|
||||
@@ -131,15 +130,16 @@ fn os_str_to_c_string_unicode(
|
||||
}
|
||||
|
||||
/// Convenience methods/associated functions for working with [CString]
|
||||
///
|
||||
/// This is helps transitioning from unsafe code.
|
||||
pub trait CStringExt {
|
||||
/// Create a new [CString], yolo style
|
||||
trait CStringExt {
|
||||
/// Create a new [CString], best effort
|
||||
///
|
||||
/// This unwrap the result, panicking when there are embedded NULL
|
||||
/// bytes.
|
||||
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||
CString::new(t).expect("String contains null byte, can not be CString")
|
||||
/// Like the [to_string_lossy] this doesn't give up in the face of
|
||||
/// bad input (embedded null bytes in this case) instead it does
|
||||
/// the best it can by stripping the embedded null bytes.
|
||||
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||
let mut s = t.into();
|
||||
s.retain(|&c| c != 0);
|
||||
CString::new(s).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ impl CStringExt for CString {}
|
||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
||||
/// and the compiler sometimes allows it in an unsafe way. These
|
||||
/// methods make it more succinct and help you get it right.
|
||||
pub trait StrExt {
|
||||
pub(crate) trait Strdup {
|
||||
/// Allocate a new raw C `*char` version of this string.
|
||||
///
|
||||
/// This allocates a new raw C string which must be freed using
|
||||
@@ -168,35 +168,44 @@ pub trait StrExt {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> StrExt for T {
|
||||
impl<T: AsRef<str>> Strdup for T {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::yolo(self.as_ref());
|
||||
let tmp = CString::new_lossy(self.as_ref());
|
||||
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 std::path::Path {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods to turn optional strings into C strings.
|
||||
///
|
||||
/// This is the same as the [StrExt] trait but a different trait name
|
||||
/// to work around the type system not allowing to implement [StrExt]
|
||||
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
|
||||
/// This is the same as the [Strdup] trait but a different trait name
|
||||
/// to work around the type system not allowing to implement [Strdup]
|
||||
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
|
||||
/// for `AsRef<&str>`.
|
||||
///
|
||||
/// When the [Option] is [Option::Some] this behaves just like
|
||||
/// [StrExt::strdup], when it is [Option::None] a null pointer is
|
||||
/// [Strdup::strdup], when it is [Option::None] a null pointer is
|
||||
/// returned.
|
||||
pub trait OptStrExt {
|
||||
pub(crate) trait OptStrdup {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [StrExt::strdup] for details.
|
||||
/// See [Strdup::strdup] for details.
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrExt for Option<T> {
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::yolo(s.as_ref());
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
@@ -204,7 +213,7 @@ impl<T: AsRef<str>> OptStrExt for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
if s.is_null() {
|
||||
return "".into();
|
||||
}
|
||||
@@ -214,7 +223,7 @@ pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
cstr.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
if s.is_null() {
|
||||
return None;
|
||||
}
|
||||
@@ -235,7 +244,7 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
unsafe {
|
||||
@@ -247,7 +256,7 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
|
||||
// as_path() implementation for windows, documented above.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
as_path_unicode(s)
|
||||
}
|
||||
|
||||
@@ -354,8 +363,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cstring_yolo() {
|
||||
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
||||
fn test_cstring_new_lossy() {
|
||||
assert!(CString::new("hel\x00lo").is_err());
|
||||
assert!(CString::new(String::from("hel\x00o")).is_err());
|
||||
let r = CString::new("hello").unwrap();
|
||||
assert_eq!(CString::new_lossy("hello"), r);
|
||||
assert_eq!(CString::new_lossy("hel\x00lo"), r);
|
||||
assert_eq!(CString::new_lossy(String::from("hello")), r);
|
||||
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,7 +3,6 @@ extern crate proc_macro;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn;
|
||||
|
||||
// For now, assume (not check) that these macroses are applied to enum without
|
||||
// data. If this assumption is violated, compiler error will point to
|
||||
|
||||
126
draft/group-sync.rst
Normal file
126
draft/group-sync.rst
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
Problem: missing eventual group consistency
|
||||
--------------------------------------------
|
||||
|
||||
If group members are concurrently adding new members,
|
||||
the new members will miss each other's additions, example:
|
||||
|
||||
- Alice and Bob are in a two-member group
|
||||
|
||||
- Alice adds Carol, concurrently Bob adds Doris
|
||||
|
||||
- Carol will see a three-member group (Alice, Bob, Carol),
|
||||
Doris will see a different three-member group (Alice, Bob, Doris),
|
||||
and only Alice and Bob will have all four members.
|
||||
|
||||
Note that for verified groups any mitigation mechanism likely
|
||||
needs to make all clients to know who originally added a member.
|
||||
|
||||
|
||||
solution: memorize+attach (possible encrypted) chat-meta mime messages
|
||||
----------------------------------------------------------------------
|
||||
|
||||
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
|
||||
|
||||
|
||||
- All Chat-Group-Member-Added/Removed messages are recorded in their
|
||||
full raw (signed and encrypted) mime-format in the DB
|
||||
|
||||
- If an incoming member-add/member-delete messages has a member list
|
||||
which is, apart from the added/removed member, not consistent
|
||||
with our own view, broadcast a "Chat-Group-Member-Correction" message to
|
||||
all members, attaching the original added/removed mime-message for all mismatching
|
||||
contacts. If we have no relevant add/del information, don't send a
|
||||
correction message out.
|
||||
|
||||
- Upong receiving added/removed attachments we don't do the
|
||||
check_consistency+correction message dance.
|
||||
This avoids recursion problems and hard-to-reason-about chatter.
|
||||
|
||||
Notes:
|
||||
|
||||
- mechanism works for both encrypted and unencrypted add/del messages
|
||||
|
||||
- we already have a "mime_headers" column in the DB for each incoming message.
|
||||
We could extend it to also include the payload and store mime unconditionally
|
||||
for member-added/removed messages.
|
||||
|
||||
- multiple member-added/removed messages can be attached in a single
|
||||
correction message
|
||||
|
||||
- it is minimal on the number of overall messages to reach group consistency
|
||||
(best-case: no extra messages, the ABCD case above: max two extra messages)
|
||||
|
||||
- somewhat backward compatible: older clients will probably ignore
|
||||
messages which are signed by someone who is not the outer From-address.
|
||||
|
||||
- the correction-protocol also helps with dropped messages. If a member
|
||||
did not see a member-added/removed message, the next member add/removed
|
||||
message in the group will likely heal group consistency for this member.
|
||||
|
||||
- we can quite easily extend the mechanism to also provide the group-avatar or
|
||||
other meta-information.
|
||||
|
||||
Discussions of variants
|
||||
++++++++++++++++++++++++
|
||||
|
||||
- instead of acting on MemberAdded/Removed message we could send
|
||||
corrections for any received message that addresses inconsistent group members but
|
||||
a) this would delay group-membership healing
|
||||
b) could lead to a lot of members sending corrections
|
||||
|
||||
- instead of broadcasting correction messages we could only send it to
|
||||
the sender of the inconsistent member-added/removed message.
|
||||
A receiver of such a correction message would then need to forward
|
||||
the message to the members it thinks also have an inconsistent view.
|
||||
This sounds complicated and error-prone. Concretely, if Alice
|
||||
receives Bob's "Member-added: Doris" message, then Alice
|
||||
broadcasting the correction message with "Member-added: Carol"
|
||||
would reach all four members, healing group consistency in one step.
|
||||
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
|
||||
Bob would broadcast a correction message to all four members as well.
|
||||
(Imagine a situation where Alice/Bob added Carol/Doris
|
||||
while both being in an offline or bad-connection situation).
|
||||
|
||||
|
||||
solution2: repeat member-added/removed messages
|
||||
---------------------------------------------------
|
||||
|
||||
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
|
||||
but keep sending out the old headers until the new protocol is sufficiently deployed.
|
||||
|
||||
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
|
||||
which controls repetition of the signed "add/del e-mail address" payload.
|
||||
|
||||
Example::
|
||||
|
||||
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
|
||||
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
|
||||
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
|
||||
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
|
||||
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
|
||||
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
|
||||
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
|
||||
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
|
||||
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
|
||||
=P6GG
|
||||
|
||||
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
|
||||
Receivers will apply the add/del change to the group-membership,
|
||||
decrease the TTL by 1, and if TTL>0 re-sent the header.
|
||||
|
||||
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
|
||||
This allows to propagate, in a cryptographically secured way,
|
||||
who added a member. This is particularly important for allowing
|
||||
to show in verified groups who added a member (planned).
|
||||
|
||||
Disadvantage to solution 1:
|
||||
|
||||
- requires to specify encoding and precise rules for what/how is signed.
|
||||
|
||||
- causes O(N^2) extra messages
|
||||
|
||||
- Not easily extendable for other things (without introducing a new
|
||||
header / encoding)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
@@ -91,7 +92,7 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
|
||||
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
|
||||
let data = dc_read_file(context, filename)?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
|
||||
@@ -297,7 +298,7 @@ fn chat_prefix(chat: &Chat) -> &'static str {
|
||||
chat.typ.into()
|
||||
}
|
||||
|
||||
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
|
||||
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
|
||||
let mut sel_chat = if !chat_id.is_unset() {
|
||||
Chat::load_from_db(context, chat_id).ok()
|
||||
@@ -397,6 +398,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
|
||||
clear -- clear screen\n\
|
||||
exit or quit\n\
|
||||
@@ -1028,6 +1030,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
bail!("Command failed.");
|
||||
}
|
||||
}
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
let seconds = arg1.parse()?;
|
||||
let device_cnt = message::estimate_deletion_cnt(context, false, seconds)?;
|
||||
let server_cnt = message::estimate_deletion_cnt(context, true, seconds)?;
|
||||
println!(
|
||||
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
|
||||
seconds, device_cnt, server_cnt
|
||||
);
|
||||
}
|
||||
"emptyserver" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <flags> missing");
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#[macro_use]
|
||||
extern crate deltachat;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate rusqlite;
|
||||
@@ -20,6 +18,7 @@ use std::process::Command;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
@@ -308,8 +307,17 @@ const CONTACT_COMMANDS: [&str; 6] = [
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 9] = [
|
||||
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
"getqr",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
"quit",
|
||||
"help",
|
||||
"estimatedeletion",
|
||||
];
|
||||
|
||||
impl Hinter for DcHelper {
|
||||
@@ -361,10 +369,10 @@ impl Highlighter for DcHelper {
|
||||
|
||||
impl Helper for DcHelper {}
|
||||
|
||||
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
||||
fn main_0(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
return Err(format_err!("No db-name specified"));
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new(
|
||||
Box::new(receive_event),
|
||||
@@ -434,7 +442,7 @@ enum ExitResult {
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
|
||||
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
let arg0 = args.next().unwrap_or_default();
|
||||
let arg1 = args.next().unwrap_or_default();
|
||||
@@ -517,7 +525,7 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
|
||||
Ok(ExitResult::Continue)
|
||||
}
|
||||
|
||||
pub fn main() -> Result<(), failure::Error> {
|
||||
pub fn main() -> Result<(), Error> {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
0.900.0 (DRAFT)
|
||||
---------------
|
||||
|
||||
- refactored internals to use plugin-approach
|
||||
|
||||
- introduced PerAccount and Global hooks that plugins can implement
|
||||
|
||||
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
|
||||
|
||||
- introduced two documented examples for an echo and a group-membership
|
||||
tracking plugin.
|
||||
|
||||
0.800.0
|
||||
-------
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
high level API reference
|
||||
========================
|
||||
|
||||
.. note::
|
||||
|
||||
This API is work in progress and may change in versions prior to 1.0.
|
||||
|
||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||
other classes)
|
||||
- :class:`deltachat.contact.Contact`
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
C deltachat interface
|
||||
=====================
|
||||
|
||||
See :doc:`lapi` for accessing many of the below functions
|
||||
through the ``deltachat.capi.lib`` namespace.
|
||||
|
||||
@@ -1,37 +1,60 @@
|
||||
|
||||
|
||||
examples
|
||||
========
|
||||
|
||||
|
||||
Playing around on the commandline
|
||||
----------------------------------
|
||||
|
||||
Once you have :doc:`installed deltachat bindings <install>`
|
||||
you can start playing from the python interpreter commandline.
|
||||
For example you can type ``python`` and then::
|
||||
you need email/password credentials for an IMAP/SMTP account.
|
||||
Delta Chat developers and the CI system use a special URL to create
|
||||
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
|
||||
|
||||
# instantiate and configure deltachat account
|
||||
import deltachat
|
||||
ac = deltachat.Account("/tmp/db")
|
||||
Receiving a Chat message from the command line
|
||||
----------------------------------------------
|
||||
|
||||
# start configuration activity and smtp/imap threads
|
||||
ac.start_threads()
|
||||
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
|
||||
Here is a simple bot that:
|
||||
|
||||
# create a contact and send a message
|
||||
contact = ac.create_contact("someother@email.address")
|
||||
chat = ac.create_chat_by_contact(contact)
|
||||
chat.send_text("hi from the python interpreter command line")
|
||||
- receives a message and sends back ("echoes") a message
|
||||
|
||||
Checkout our :doc:`api` for the various high-level things you can do
|
||||
to send/receive messages, create contacts and chats.
|
||||
- terminates the bot if the message `/quit` is sent
|
||||
|
||||
.. include:: ../examples/echo_and_quit.py
|
||||
:literal:
|
||||
|
||||
Looking at a real example
|
||||
With this file in your working directory you can run the bot
|
||||
by specifying a database path, an e-mail address and password of
|
||||
a SMTP-IMAP account::
|
||||
|
||||
$ cd examples
|
||||
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
|
||||
|
||||
While this process is running you can start sending chat messages
|
||||
to `ADDRESS`.
|
||||
|
||||
Track member additions and removals in a group
|
||||
----------------------------------------------
|
||||
|
||||
Here is a simple bot that:
|
||||
|
||||
- echoes messages sent to it
|
||||
|
||||
- tracks if configuration completed
|
||||
|
||||
- tracks member additions and removals for all chat groups
|
||||
|
||||
.. include:: ../examples/group_tracking.py
|
||||
:literal:
|
||||
|
||||
With this file in your working directory you can run the bot
|
||||
by specifying a database path, an e-mail address and password of
|
||||
a SMTP-IMAP account::
|
||||
|
||||
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
|
||||
|
||||
When this process is running you can start sending chat messages
|
||||
to `ADDRESS`.
|
||||
|
||||
Writing bots for real
|
||||
-------------------------
|
||||
|
||||
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
||||
contains a real-life example of Python bindings usage.
|
||||
|
||||
contains a little framework for writing deltachat bots in Python.
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ deltachat python bindings
|
||||
The ``deltachat`` Python package provides two layers of bindings for the
|
||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||
|
||||
- :doc:`api` is a high level interface to deltachat-core which aims
|
||||
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
||||
- :doc:`api` is a high level interface to deltachat-core.
|
||||
|
||||
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
|
||||
|
||||
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||
@@ -28,6 +29,7 @@ getting started
|
||||
changelog
|
||||
api
|
||||
lapi
|
||||
plugins
|
||||
|
||||
..
|
||||
Indices and tables
|
||||
|
||||
38
python/doc/plugins.rst
Normal file
38
python/doc/plugins.rst
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
Implementing Plugin Hooks
|
||||
==========================
|
||||
|
||||
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
|
||||
for managing global and per-account plugin registration, and performing
|
||||
hook calls. There are two kinds of plugins:
|
||||
|
||||
- Global plugins that are active for all accounts; they can implement
|
||||
hooks at account-creation and account-shutdown time.
|
||||
|
||||
- Account plugins that are only active during the lifetime of a
|
||||
single Account instance.
|
||||
|
||||
|
||||
Registering a plugin
|
||||
--------------------
|
||||
|
||||
.. autofunction:: deltachat.register_global_plugin
|
||||
:noindex:
|
||||
|
||||
.. automethod:: deltachat.account.Account.add_account_plugin
|
||||
:noindex:
|
||||
|
||||
|
||||
Per-Account Hook specifications
|
||||
-------------------------------
|
||||
|
||||
.. autoclass:: deltachat.hookspec.PerAccount
|
||||
:members:
|
||||
|
||||
|
||||
Global Hook specifications
|
||||
--------------------------
|
||||
|
||||
.. autoclass:: deltachat.hookspec.Global
|
||||
:members:
|
||||
|
||||
30
python/examples/echo_and_quit.py
Normal file
30
python/examples/echo_and_quit.py
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
# content of echo_and_quit.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
|
||||
class EchoPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
message.account.shutdown()
|
||||
else:
|
||||
# unconditionally accept the chat
|
||||
message.accept_sender_contact()
|
||||
addr = message.get_sender_contact().addr
|
||||
text = message.text
|
||||
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_message_delivered(self, message):
|
||||
print("ac_message_delivered", message)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
52
python/examples/group_tracking.py
Normal file
52
python/examples/group_tracking.py
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
# content of group_tracking.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
|
||||
class GroupTrackingPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
message.account.shutdown()
|
||||
else:
|
||||
# unconditionally accept the chat
|
||||
message.accept_sender_contact()
|
||||
addr = message.get_sender_contact().addr
|
||||
text = message.text
|
||||
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_outgoing_message(self, message):
|
||||
print("ac_outgoing_message:", message)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(self, success):
|
||||
print("ac_configure_completed:", success)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_chat_modified(self, chat):
|
||||
print("ac_chat_modified:", chat.id, chat.get_name())
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
print("ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
print("ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
python/examples/test_examples.py
Normal file
72
python/examples/test_examples.py
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
import pytest
|
||||
import py
|
||||
import echo_and_quit
|
||||
import group_tracking
|
||||
from deltachat.eventlogger import FFIEventLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def datadir():
|
||||
"""The py.path.local object of the test-data/ directory."""
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join('test-data')
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip('test-data directory not found')
|
||||
|
||||
|
||||
def test_echo_quit_plugin(acfactory):
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
ac1 = acfactory.get_one_online_account()
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch1 = ac1.create_chat_by_contact(bot_contact)
|
||||
ch1.send_text("hello")
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert "hello" in reply.text
|
||||
assert reply.chat == ch1
|
||||
ch1.send_text("/quit")
|
||||
botproc.wait()
|
||||
|
||||
|
||||
def test_group_tracking_plugin(acfactory, lp):
|
||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
|
||||
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
|
||||
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_configure_completed*
|
||||
""")
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1, "ac1"))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2, "ac2"))
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
ch.add_contact(bot_contact)
|
||||
ch.send_text("hello")
|
||||
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_chat_modified*bot test group*
|
||||
""")
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
contact3 = ac1.create_contact(ac2.get_config("addr"))
|
||||
ch.add_contact(contact3)
|
||||
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert "hello" in reply.text
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_added {}*
|
||||
""".format(contact3.addr))
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_removed {}*
|
||||
""".format(contact3.addr))
|
||||
@@ -22,6 +22,11 @@ def main():
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
entry_points = {
|
||||
'pytest11': [
|
||||
'deltachat.testplugin = deltachat.testplugin',
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from deltachat import capi, const
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.account import Account # noqa
|
||||
import sys
|
||||
|
||||
from . import capi, const, hookspec
|
||||
from .capi import ffi
|
||||
from .account import Account # noqa
|
||||
from .message import Message # noqa
|
||||
from .contact import Contact # noqa
|
||||
from .chat import Chat # noqa
|
||||
from .hookspec import account_hookimpl, global_hookimpl # noqa
|
||||
from . import eventlogger
|
||||
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
try:
|
||||
@@ -74,3 +81,62 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[val] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
def register_global_plugin(plugin):
|
||||
""" Register a global plugin which implements one or more
|
||||
of the :class:`deltachat.hookspec.Global` hooks.
|
||||
"""
|
||||
gm = hookspec.Global._get_plugin_manager()
|
||||
gm.register(plugin)
|
||||
gm.check_pending()
|
||||
|
||||
|
||||
def unregister_global_plugin(plugin):
|
||||
gm = hookspec.Global._get_plugin_manager()
|
||||
gm.unregister(plugin)
|
||||
|
||||
|
||||
register_global_plugin(eventlogger)
|
||||
|
||||
|
||||
def run_cmdline(argv=None, account_plugins=None):
|
||||
""" Run a simple default command line app, registering the specified
|
||||
account plugins. """
|
||||
import argparse
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
|
||||
parser.add_argument("db", action="store", help="database file")
|
||||
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
|
||||
parser.add_argument("--email", action="store", help="email address")
|
||||
parser.add_argument("--password", action="store", help="password")
|
||||
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
ac = Account(args.db)
|
||||
|
||||
if args.show_ffi:
|
||||
log = eventlogger.FFIEventLogger(ac, "bot")
|
||||
ac.add_account_plugin(log)
|
||||
|
||||
if not ac.is_configured():
|
||||
assert args.email and args.password, (
|
||||
"you must specify --email and --password once to configure this database/account"
|
||||
)
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
|
||||
for plugin in account_plugins or []:
|
||||
ac.add_account_plugin(plugin)
|
||||
|
||||
# start IO threads and configure if neccessary
|
||||
ac.start()
|
||||
|
||||
print("{}: waiting for message".format(ac.get_config("addr")))
|
||||
|
||||
ac.wait_shutdown()
|
||||
|
||||
@@ -2,21 +2,25 @@
|
||||
|
||||
from __future__ import print_function
|
||||
import atexit
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from email.utils import parseaddr
|
||||
import queue
|
||||
from threading import Event
|
||||
import os
|
||||
import time
|
||||
from array import array
|
||||
from queue import Queue
|
||||
|
||||
import deltachat
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .message import Message, map_system_message
|
||||
from .contact import Contact
|
||||
from .eventlogger import EventLogger
|
||||
from .hookspec import get_plugin_manager, hookimpl
|
||||
from .tracker import ImexTracker
|
||||
from . import hookspec, iothreads
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
""" Account is missing `addr` and `mail_pw` config values. """
|
||||
|
||||
|
||||
class Account(object):
|
||||
@@ -24,45 +28,63 @@ class Account(object):
|
||||
by the underlying deltachat core library. All public Account methods are
|
||||
meant to be memory-safe and return memory-safe objects.
|
||||
"""
|
||||
def __init__(self, db_path, logid=None, os_name=None, debug=True):
|
||||
MissingCredentials = MissingCredentials
|
||||
|
||||
def __init__(self, db_path, os_name=None, logging=True):
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
will be created if it doesn't exist.
|
||||
:param logid: an optional logging prefix that should be used with
|
||||
the default internal logging.
|
||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||
:param debug: turn on debug logging for events.
|
||||
"""
|
||||
# initialize per-account plugin system
|
||||
self._pm = hookspec.PerAccount._make_plugin_manager()
|
||||
self._logging = logging
|
||||
self.add_account_plugin(self)
|
||||
|
||||
self._dc_context = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
|
||||
_destroy_dc_context,
|
||||
)
|
||||
self._evlogger = EventLogger(self, logid, debug)
|
||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||
|
||||
# register event call back and initialize plugin system
|
||||
def _ll_event(ctx, evt_name, data1, data2):
|
||||
assert ctx == self._dc_context
|
||||
self.pluggy.hook.process_low_level_event(
|
||||
account=self, event_name=evt_name, data1=data1, data2=data2
|
||||
)
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
|
||||
self.pluggy = get_plugin_manager()
|
||||
self.pluggy.register(self._evlogger)
|
||||
deltachat.set_context_callback(self._dc_context, _ll_event)
|
||||
self._threads = iothreads.IOThreads(self)
|
||||
self._hook_event_queue = queue.Queue()
|
||||
self._in_use_iter_events = False
|
||||
self._shutdown_event = Event()
|
||||
|
||||
# open database
|
||||
self.db_path = db_path
|
||||
if hasattr(db_path, "encode"):
|
||||
db_path = db_path.encode("utf8")
|
||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||
self._configkeys = self.get_config("sys.config_keys").split()
|
||||
atexit.register(self.shutdown)
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def disable_logging(self):
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self):
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
@hookspec.account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||
ev = HookEvent(self, name=name, kwargs=kwargs)
|
||||
self._hook_event_queue.put(ev)
|
||||
|
||||
# def __del__(self):
|
||||
# self.shutdown()
|
||||
|
||||
def ac_log_line(self, msg):
|
||||
if self._logging:
|
||||
self._pm.hook.ac_log_line(message=msg)
|
||||
|
||||
def _check_config_key(self, name):
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
@@ -134,16 +156,15 @@ class Account(object):
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def configure(self, **kwargs):
|
||||
""" set config values and configure this account.
|
||||
def update_config(self, kwargs):
|
||||
""" update config values.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
values need to be unicode.
|
||||
:returns: None
|
||||
"""
|
||||
for name, value in kwargs.items():
|
||||
self.set_config(name, value)
|
||||
lib.dc_configure(self._dc_context)
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
|
||||
def is_configured(self):
|
||||
""" determine if the account is configured already; an initial connection
|
||||
@@ -151,7 +172,7 @@ class Account(object):
|
||||
|
||||
:returns: True if account is configured.
|
||||
"""
|
||||
return lib.dc_is_configured(self._dc_context)
|
||||
return bool(lib.dc_is_configured(self._dc_context))
|
||||
|
||||
def set_avatar(self, img_path):
|
||||
"""Set self avatar.
|
||||
@@ -202,8 +223,7 @@ class Account(object):
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
self.check_is_configured()
|
||||
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||
|
||||
def create_contact(self, email, name=None):
|
||||
""" create a (new) Contact. If there already is a Contact
|
||||
@@ -214,11 +234,14 @@ class Account(object):
|
||||
:param name: display name for this contact (optional)
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
email = as_dc_charpointer(email)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, email)
|
||||
realname, addr = parseaddr(email)
|
||||
if name:
|
||||
realname = name
|
||||
realname = as_dc_charpointer(realname)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, realname, addr)
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return Contact(self._dc_context, contact_id)
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
""" delete a Contact.
|
||||
@@ -231,6 +254,14 @@ 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):
|
||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
if contact_id:
|
||||
return self.get_contact_by_id(contact_id)
|
||||
|
||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||
""" get a (filtered) list of contacts.
|
||||
|
||||
@@ -250,7 +281,15 @@ class Account(object):
|
||||
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self):
|
||||
""" yield all fresh messages from all chats. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_fresh_msgs(self._dc_context),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||
|
||||
def create_chat_by_contact(self, contact):
|
||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
@@ -272,6 +311,9 @@ class Account(object):
|
||||
""" create or get an existing chat object for the
|
||||
the specified message.
|
||||
|
||||
If this message is in the deaddrop chat then
|
||||
the sender will become an accepted contact.
|
||||
|
||||
:param message: messsage id or message instance.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
@@ -324,6 +366,13 @@ class Account(object):
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_contact_by_id(self, contact_id):
|
||||
""" return Contact instance or None.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id):
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
@@ -368,13 +417,18 @@ class Account(object):
|
||||
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||
|
||||
def export_self_keys(self, path):
|
||||
""" export public and private keys to the specified directory. """
|
||||
""" export public and private keys to the specified directory.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
return self._export(path, imex_cmd=1)
|
||||
|
||||
def export_all(self, path):
|
||||
"""return new file containing a backup of all database state
|
||||
(chats, contacts, keys, media, ...). The file is created in the
|
||||
the `path` directory.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
export_files = self._export(path, 11)
|
||||
if len(export_files) != 1:
|
||||
@@ -382,7 +436,7 @@ class Account(object):
|
||||
return export_files[0]
|
||||
|
||||
def _export(self, path, imex_cmd):
|
||||
with ImexTracker(self) as imex_tracker:
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
@@ -392,6 +446,8 @@ class Account(object):
|
||||
""" Import private keys found in the `path` directory.
|
||||
The last imported key is made the default keys unless its name
|
||||
contains the string legacy. Public keys are not imported.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
self._import(path, imex_cmd=2)
|
||||
|
||||
@@ -404,7 +460,7 @@ class Account(object):
|
||||
self._import(path, imex_cmd=12)
|
||||
|
||||
def _import(self, path, imex_cmd):
|
||||
with ImexTracker(self) as imex_tracker:
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
@@ -473,46 +529,6 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def stop_ongoing(self):
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
#
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
""" wait for and return next incoming message. """
|
||||
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
return self.get_message_by_id(ev[2])
|
||||
|
||||
def start_threads(self, mvbox=False, sentbox=False):
|
||||
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
||||
|
||||
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
||||
:returns: None
|
||||
"""
|
||||
if not self.is_configured():
|
||||
self.configure()
|
||||
self._threads.start(mvbox=mvbox, sentbox=sentbox)
|
||||
|
||||
def stop_threads(self, wait=True):
|
||||
""" stop IMAP/SMTP threads. """
|
||||
if self._threads.is_started():
|
||||
self.stop_ongoing()
|
||||
self._threads.stop(wait=wait)
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
""" stop threads and close and remove underlying dc_context and callbacks. """
|
||||
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
||||
# print("SHUTDOWN", self)
|
||||
self.stop_threads(wait=False)
|
||||
lib.dc_close(self._dc_context)
|
||||
self.stop_threads(wait=wait) # to wait for threads
|
||||
deltachat.clear_context_callback(self._dc_context)
|
||||
del self._dc_context
|
||||
atexit.unregister(self.shutdown)
|
||||
self.pluggy.unregister(self._evlogger)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
@@ -527,120 +543,122 @@ class Account(object):
|
||||
if dc_res == 0:
|
||||
raise ValueError("no chat is streaming locations")
|
||||
|
||||
#
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
class ImexTracker:
|
||||
def __init__(self, account):
|
||||
self._imex_events = Queue()
|
||||
self.account = account
|
||||
def add_account_plugin(self, plugin, name=None):
|
||||
""" add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
"""
|
||||
self._pm.register(plugin, name=name)
|
||||
self._pm.check_pending()
|
||||
return plugin
|
||||
|
||||
def __enter__(self):
|
||||
self.account.pluggy.register(self)
|
||||
return self
|
||||
@contextmanager
|
||||
def temp_plugin(self, plugin):
|
||||
""" run a with-block with the given plugin temporarily registered. """
|
||||
self._pm.register(plugin)
|
||||
yield plugin
|
||||
self._pm.unregister(plugin)
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.account.pluggy.unregister(self)
|
||||
def stop_ongoing(self):
|
||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
@hookimpl
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
# there could be multiple accounts instantiated
|
||||
if self.account is not account:
|
||||
def start(self, callback_thread=True):
|
||||
""" start this account (activate imap/smtp threads etc.)
|
||||
and return immediately.
|
||||
|
||||
If this account is not configured, an internal configuration
|
||||
job will be scheduled if config values are sufficiently specified.
|
||||
|
||||
You may call `wait_shutdown` or `shutdown` after the
|
||||
account is in started mode.
|
||||
|
||||
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
if not self.is_configured():
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
lib.dc_configure(self._dc_context)
|
||||
self._threads.start(callback_thread=callback_thread)
|
||||
|
||||
def wait_shutdown(self):
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
""" shutdown account, stop threads and close and remove
|
||||
underlying dc_context and callbacks. """
|
||||
dc_context = self._dc_context
|
||||
if dc_context is None:
|
||||
return
|
||||
if event_name == "DC_EVENT_IMEX_PROGRESS":
|
||||
self._imex_events.put(data1)
|
||||
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(data1)
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, str):
|
||||
files_written.append(ev)
|
||||
elif ev == 0:
|
||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
||||
elif ev == 1000:
|
||||
return files_written
|
||||
if self._threads.is_started():
|
||||
self.stop_ongoing()
|
||||
self._threads.stop(wait=False)
|
||||
lib.dc_close(dc_context)
|
||||
self._hook_event_queue.put(None)
|
||||
self._threads.stop(wait=wait) # to wait for threads
|
||||
self._dc_context = None
|
||||
atexit.unregister(self.shutdown)
|
||||
self._shutdown_event.set()
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_after_shutdown(account=self, dc_context=dc_context)
|
||||
|
||||
def _handle_current_events(self):
|
||||
""" handle all currently queued events and then return. """
|
||||
while 1:
|
||||
try:
|
||||
event = self._hook_event_queue.get(block=False)
|
||||
except queue.Empty:
|
||||
break
|
||||
else:
|
||||
event.call_hook()
|
||||
|
||||
class IOThreads:
|
||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
||||
self._dc_context = dc_context
|
||||
self._thread_quitflag = False
|
||||
self._name2thread = {}
|
||||
self._log_event = log_event
|
||||
def iter_events(self, timeout=None):
|
||||
""" yield hook events until shutdown.
|
||||
|
||||
def is_started(self):
|
||||
return len(self._name2thread) > 0
|
||||
It is not allowed to call iter_events() from multiple threads.
|
||||
"""
|
||||
if self._in_use_iter_events:
|
||||
raise RuntimeError("can only call iter_events() from one thread")
|
||||
self._in_use_iter_events = True
|
||||
while 1:
|
||||
event = self._hook_event_queue.get(timeout=timeout)
|
||||
if event is None:
|
||||
break
|
||||
yield event
|
||||
|
||||
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
|
||||
assert not self.is_started()
|
||||
if imap:
|
||||
self._start_one_thread("inbox", self.imap_thread_run)
|
||||
if mvbox:
|
||||
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
||||
if sentbox:
|
||||
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
||||
if smtp:
|
||||
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||
|
||||
def _start_one_thread(self, name, func):
|
||||
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
def stop(self, wait=False):
|
||||
self._thread_quitflag = True
|
||||
|
||||
# Workaround for a race condition. Make sure that thread is
|
||||
# not in between checking for quitflag and entering idle.
|
||||
time.sleep(0.5)
|
||||
|
||||
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||
if wait:
|
||||
for name, thread in self._name2thread.items():
|
||||
thread.join()
|
||||
|
||||
def imap_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
|
||||
|
||||
def mvbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
||||
|
||||
def sentbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
||||
|
||||
def smtp_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
name = ffi_event.name
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
data1 = ffi_event.data1
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
yield "ac_configure_completed", dict(success=success)
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
if msg.is_outgoing():
|
||||
res = map_system_message(msg)
|
||||
if res and res[0].startswith("ac_member"):
|
||||
yield res
|
||||
yield "ac_outgoing_message", dict(message=msg)
|
||||
elif msg.is_in_fresh():
|
||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_message_delivered", dict(message=msg)
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = self.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", dict(chat=chat)
|
||||
|
||||
|
||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||
@@ -667,3 +685,17 @@ class ScannedQRCode:
|
||||
@property
|
||||
def contact_id(self):
|
||||
return self._dc_lot.id()
|
||||
|
||||
|
||||
class HookEvent:
|
||||
def __init__(self, account, name, kwargs):
|
||||
assert hasattr(account._pm.hook, name), name
|
||||
self.account = account
|
||||
self.name = name
|
||||
self.kwargs = kwargs
|
||||
|
||||
def call_hook(self):
|
||||
hook = getattr(self.account._pm.hook, self.name, None)
|
||||
if hook is None:
|
||||
raise ValueError("event_name {} unknown".format(self.name))
|
||||
return hook(**self.kwargs)
|
||||
|
||||
@@ -30,7 +30,7 @@ class Chat(object):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
|
||||
@property
|
||||
def _dc_chat(self):
|
||||
@@ -51,6 +51,16 @@ class Chat(object):
|
||||
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self):
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_VERIFIED_GROUP
|
||||
)
|
||||
|
||||
def is_deaddrop(self):
|
||||
""" return true if this chat is a deaddrop chat.
|
||||
|
||||
@@ -129,7 +139,7 @@ class Chat(object):
|
||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||
|
||||
def get_type(self):
|
||||
""" return type of this chat.
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
@@ -353,7 +363,7 @@ class Chat(object):
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(
|
||||
dc_array, lambda id: Contact(self._dc_context, id))
|
||||
dc_array, lambda id: Contact(self.account, id))
|
||||
)
|
||||
|
||||
def set_profile_image(self, img_path):
|
||||
@@ -405,12 +415,6 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
def get_subtitle(self):
|
||||
"""return the subtitle of the chat
|
||||
:returns: the subtitle
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
||||
|
||||
# ------ location streaming API ------------------------------
|
||||
|
||||
def is_sending_locations(self):
|
||||
|
||||
@@ -11,6 +11,7 @@ from os.path import join as joinpath
|
||||
DC_GCL_ARCHIVED_ONLY = 0x01
|
||||
DC_GCL_NO_SPECIALS = 0x02
|
||||
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
||||
DC_GCL_FOR_FORWARDING = 0x08
|
||||
DC_GCL_VERIFIED_ONLY = 0x01
|
||||
DC_GCL_ADD_SELF = 0x02
|
||||
DC_QR_ASK_VERIFYCONTACT = 200
|
||||
@@ -98,7 +99,6 @@ DC_EVENT_IMEX_PROGRESS = 2051
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
||||
DC_EVENT_FILE_COPIED = 2055
|
||||
DC_EVENT_IS_OFFLINE = 2081
|
||||
DC_EVENT_GET_STRING = 2091
|
||||
@@ -115,8 +115,6 @@ DC_CHAT_VISIBILITY_PINNED = 2
|
||||
DC_STR_NOMESSAGES = 1
|
||||
DC_STR_SELF = 2
|
||||
DC_STR_DRAFT = 3
|
||||
DC_STR_MEMBER = 4
|
||||
DC_STR_CONTACT = 6
|
||||
DC_STR_VOICEMESSAGE = 7
|
||||
DC_STR_DEADDROP = 8
|
||||
DC_STR_IMAGE = 9
|
||||
@@ -148,7 +146,6 @@ DC_STR_ARCHIVEDCHATS = 40
|
||||
DC_STR_STARREDMSGS = 41
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43
|
||||
DC_STR_SELFTALK_SUBTITLE = 50
|
||||
DC_STR_CANNOT_LOGIN = 60
|
||||
DC_STR_SERVER_RESPONSE = 61
|
||||
DC_STR_MSGACTIONBYUSER = 62
|
||||
|
||||
@@ -10,8 +10,9 @@ class Contact(object):
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
def __init__(self, dc_context, id):
|
||||
self._dc_context = dc_context
|
||||
def __init__(self, account, id):
|
||||
self.account = account
|
||||
self._dc_context = account._dc_context
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -57,3 +58,7 @@ class Contact(object):
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
def get_chat(self):
|
||||
"""return 1:1 chat for this contact. """
|
||||
return self.account.create_chat_by_contact(self)
|
||||
|
||||
@@ -1,28 +1,88 @@
|
||||
import deltachat
|
||||
import threading
|
||||
import re
|
||||
import time
|
||||
import re
|
||||
from queue import Queue, Empty
|
||||
from .hookspec import hookimpl
|
||||
from .hookspec import account_hookimpl, global_hookimpl
|
||||
|
||||
|
||||
class EventLogger:
|
||||
@global_hookimpl
|
||||
def dc_account_init(account):
|
||||
# send all FFI events for this account to a plugin hook
|
||||
def _ll_event(ctx, evt_name, data1, data2):
|
||||
assert ctx == account._dc_context
|
||||
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
||||
account._pm.hook.ac_process_ffi_event(
|
||||
account=account, ffi_event=ffi_event
|
||||
)
|
||||
deltachat.set_context_callback(account._dc_context, _ll_event)
|
||||
|
||||
|
||||
@global_hookimpl
|
||||
def dc_account_after_shutdown(dc_context):
|
||||
deltachat.clear_context_callback(dc_context)
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name, data1, data2):
|
||||
self.name = name
|
||||
self.data1 = data1
|
||||
self.data2 = data2
|
||||
|
||||
def __str__(self):
|
||||
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||
|
||||
|
||||
class FFIEventLogger:
|
||||
""" If you register an instance of this logger with an Account
|
||||
you'll get all ffi-events printed.
|
||||
"""
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account, logid=None, debug=True):
|
||||
def __init__(self, account, logid):
|
||||
"""
|
||||
:param logid: an optional logging prefix that should be used with
|
||||
the default internal logging.
|
||||
"""
|
||||
self.account = account
|
||||
self._event_queue = Queue()
|
||||
self._debug = debug
|
||||
if logid is None:
|
||||
logid = str(self.account._dc_context).strip(">").split()[-1]
|
||||
self.logid = logid
|
||||
self._timeout = None
|
||||
self.init_time = time.time()
|
||||
|
||||
@hookimpl
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
if self.account == account:
|
||||
self._log_event(event_name, data1, data2)
|
||||
self._event_queue.put((event_name, data1, data2))
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self._log_event(ffi_event)
|
||||
|
||||
def _log_event(self, ffi_event):
|
||||
# don't show events that are anyway empty impls now
|
||||
if ffi_event.name == "DC_EVENT_GET_STRING":
|
||||
return
|
||||
self.account.ac_log_line(str(ffi_event))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_log_line(self, message):
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
elapsed = time.time() - self.init_time
|
||||
locname = tname
|
||||
if self.logid:
|
||||
locname += "-" + self.logid
|
||||
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
||||
with self._loglock:
|
||||
print(s, flush=True)
|
||||
|
||||
|
||||
class FFIEventTracker:
|
||||
def __init__(self, account, timeout=None):
|
||||
self.account = account
|
||||
self._timeout = timeout
|
||||
self._event_queue = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self._event_queue.put(ffi_event)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
@@ -32,10 +92,10 @@ class EventLogger:
|
||||
self.get(check_error=check_error)
|
||||
|
||||
def get(self, timeout=None, check_error=True):
|
||||
timeout = timeout or self._timeout
|
||||
timeout = timeout if timeout is not None else self._timeout
|
||||
ev = self._event_queue.get(timeout=timeout)
|
||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
||||
if check_error and ev.name == "DC_EVENT_ERROR":
|
||||
raise ValueError(str(ev))
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
@@ -47,35 +107,31 @@ class EventLogger:
|
||||
except Empty:
|
||||
break
|
||||
else:
|
||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||
assert not rex.match(ev.name), "event found {}".format(ev)
|
||||
|
||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||
self.account.ac_log_line("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
ev = self.get(timeout=timeout, check_error=check_error)
|
||||
if rex.match(ev[0]):
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.match(ev[2]):
|
||||
if rex.match(ev.data2):
|
||||
return ev
|
||||
|
||||
def _log_event(self, evt_name, data1, data2):
|
||||
# don't show events that are anyway empty impls now
|
||||
if evt_name == "DC_EVENT_GET_STRING":
|
||||
return
|
||||
if self._debug:
|
||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
||||
self._log(evpart)
|
||||
def wait_next_incoming_message(self):
|
||||
""" wait for and return next incoming message. """
|
||||
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
|
||||
def _log(self, msg):
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
with self._loglock:
|
||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
||||
def wait_next_messages_changed(self):
|
||||
""" wait for and return next message-changed message or None
|
||||
if the event contains no msgid"""
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
|
||||
@@ -1,25 +1,92 @@
|
||||
""" Hooks for python bindings """
|
||||
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
||||
|
||||
import pluggy
|
||||
|
||||
name = "deltachat"
|
||||
|
||||
hookspec = pluggy.HookspecMarker(name)
|
||||
hookimpl = pluggy.HookimplMarker(name)
|
||||
_plugin_manager = None
|
||||
account_spec_name = "deltachat-account"
|
||||
account_hookspec = pluggy.HookspecMarker(account_spec_name)
|
||||
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
|
||||
|
||||
global_spec_name = "deltachat-global"
|
||||
global_hookspec = pluggy.HookspecMarker(global_spec_name)
|
||||
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
|
||||
|
||||
|
||||
def get_plugin_manager():
|
||||
global _plugin_manager
|
||||
if _plugin_manager is None:
|
||||
_plugin_manager = pluggy.PluginManager(name)
|
||||
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
|
||||
return _plugin_manager
|
||||
class PerAccount:
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
Except for ac_process_ffi_event all hooks are executed
|
||||
in the thread which calls Account.wait_shutdown().
|
||||
"""
|
||||
@classmethod
|
||||
def _make_plugin_manager(cls):
|
||||
pm = pluggy.PluginManager(account_spec_name)
|
||||
pm.add_hookspecs(cls)
|
||||
return pm
|
||||
|
||||
@account_hookspec
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
""" process a CFFI low level events for a given account.
|
||||
|
||||
ffi_event has "name", "data1", "data2" values as specified
|
||||
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||
|
||||
DANGER: this hook is executed from the callback invoked by core.
|
||||
Hook implementations need to be short running and can typically
|
||||
not call back into core because this would easily cause recursion issues.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_log_line(self, message):
|
||||
""" log a message related to the account. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_configure_completed(self, success):
|
||||
""" Called when a configure process completed. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_incoming_message(self, message):
|
||||
""" Called on any incoming message (to deaddrop or chat). """
|
||||
|
||||
@account_hookspec
|
||||
def ac_outgoing_message(self, message):
|
||||
""" Called on each outgoing message (both system and "normal")."""
|
||||
|
||||
@account_hookspec
|
||||
def ac_message_delivered(self, message):
|
||||
""" Called when an outgoing message has been delivered to SMTP. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_chat_modified(self, chat):
|
||||
""" Chat was created or modified regarding membership, avatar, title. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
""" Called for each contact added to an accepted chat. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
""" Called for each contact removed from a chat. """
|
||||
|
||||
|
||||
class DeltaChatHookSpecs:
|
||||
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
|
||||
class Global:
|
||||
""" global hook specifications using a per-process singleton
|
||||
plugin manager instance.
|
||||
|
||||
@hookspec
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
""" process a CFFI low level events for a given account. """
|
||||
"""
|
||||
_plugin_manager = None
|
||||
|
||||
@classmethod
|
||||
def _get_plugin_manager(cls):
|
||||
if cls._plugin_manager is None:
|
||||
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
|
||||
pm.add_hookspecs(cls)
|
||||
return cls._plugin_manager
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_init(self, account):
|
||||
""" called when `Account::__init__()` function starts executing. """
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_after_shutdown(self, account, dc_context):
|
||||
""" Called after the account has been shutdown. """
|
||||
|
||||
106
python/src/deltachat/iothreads.py
Normal file
106
python/src/deltachat/iothreads.py
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .capi import lib
|
||||
|
||||
|
||||
class IOThreads:
|
||||
def __init__(self, account):
|
||||
self.account = account
|
||||
self._dc_context = account._dc_context
|
||||
self._thread_quitflag = False
|
||||
self._name2thread = {}
|
||||
|
||||
def is_started(self):
|
||||
return len(self._name2thread) > 0
|
||||
|
||||
def start(self, callback_thread):
|
||||
assert not self.is_started()
|
||||
self._start_one_thread("inbox", self.imap_thread_run)
|
||||
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||
|
||||
if callback_thread:
|
||||
self._start_one_thread("cb", self.cb_thread_run)
|
||||
|
||||
if int(self.account.get_config("mvbox_watch")):
|
||||
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
||||
|
||||
if int(self.account.get_config("sentbox_watch")):
|
||||
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
||||
|
||||
def _start_one_thread(self, name, func):
|
||||
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
@contextmanager
|
||||
def log_execution(self, message):
|
||||
self.account.ac_log_line(message + " START")
|
||||
yield
|
||||
self.account.ac_log_line(message + " FINISHED")
|
||||
|
||||
def stop(self, wait=False):
|
||||
self._thread_quitflag = True
|
||||
|
||||
# Workaround for a race condition. Make sure that thread is
|
||||
# not in between checking for quitflag and entering idle.
|
||||
time.sleep(0.5)
|
||||
|
||||
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||
if "mvbox" in self._name2thread:
|
||||
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
||||
if "sentbox" in self._name2thread:
|
||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||
if wait:
|
||||
for name, thread in self._name2thread.items():
|
||||
if thread != threading.currentThread():
|
||||
thread.join()
|
||||
|
||||
def cb_thread_run(self):
|
||||
with self.log_execution("CALLBACK THREAD START"):
|
||||
it = self.account.iter_events()
|
||||
while not self._thread_quitflag:
|
||||
try:
|
||||
ev = next(it)
|
||||
except StopIteration:
|
||||
break
|
||||
self.account.ac_log_line("calling hook name={} kwargs={}".format(ev.name, ev.kwargs))
|
||||
ev.call_hook()
|
||||
|
||||
def imap_thread_run(self):
|
||||
with self.log_execution("INBOX THREAD START"):
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_idle(self._dc_context)
|
||||
|
||||
def mvbox_thread_run(self):
|
||||
with self.log_execution("MVBOX THREAD"):
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
|
||||
def sentbox_thread_run(self):
|
||||
with self.log_execution("SENTBOX THREAD"):
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
|
||||
def smtp_thread_run(self):
|
||||
with self.log_execution("SMTP THREAD"):
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
@@ -28,7 +28,9 @@ class Message(object):
|
||||
return self.account == other.account and self.id == other.id
|
||||
|
||||
def __repr__(self):
|
||||
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
|
||||
c = self.get_sender_contact()
|
||||
return "<Message id={} sender={}/{} outgoing={} chat={}/{}>".format(
|
||||
self.id, c.id, c.addr, self.is_outgoing(), self.chat.id, self.chat.get_name())
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, account, id):
|
||||
@@ -50,6 +52,16 @@ class Message(object):
|
||||
lib.dc_msg_unref
|
||||
))
|
||||
|
||||
def accept_sender_contact(self):
|
||||
""" ensure that the sender is an accepted contact
|
||||
and that the message has a non-deaddrop chat object.
|
||||
"""
|
||||
self.account.create_chat_by_message(self)
|
||||
self._dc_msg = ffi.gc(
|
||||
lib.dc_get_msg(self._dc_context, self.id),
|
||||
lib.dc_msg_unref
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
@@ -81,6 +93,10 @@ class Message(object):
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
def is_system_message(self):
|
||||
""" return True if this message is a system/info message. """
|
||||
return lib.dc_msg_is_info(self._dc_msg)
|
||||
|
||||
def is_setup_message(self):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
@@ -159,6 +175,13 @@ class Message(object):
|
||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
def get_sender_chat(self):
|
||||
"""return the 1:1 chat with the sender of this message.
|
||||
|
||||
:returns: :class:`deltachat.chat.Chat` instance
|
||||
"""
|
||||
return self.get_sender_contact().get_chat()
|
||||
|
||||
def get_sender_contact(self):
|
||||
"""return the contact of who wrote the message.
|
||||
|
||||
@@ -166,7 +189,7 @@ class Message(object):
|
||||
"""
|
||||
from .contact import Contact
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return Contact(self._dc_context, contact_id)
|
||||
return Contact(self.account, contact_id)
|
||||
|
||||
#
|
||||
# Message State query methods
|
||||
@@ -207,6 +230,13 @@ class Message(object):
|
||||
"""
|
||||
return self._msgstate == const.DC_STATE_IN_SEEN
|
||||
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing. """
|
||||
return self._msgstate in (
|
||||
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD,
|
||||
const.DC_STATE_OUT_DELIVERED)
|
||||
|
||||
def is_out_preparing(self):
|
||||
"""Return True if Message is outgoing, but its file is being prepared.
|
||||
"""
|
||||
@@ -269,6 +299,10 @@ class Message(object):
|
||||
""" return True if it's a file message. """
|
||||
return self._view_type == const.DC_MSG_FILE
|
||||
|
||||
def mark_seen(self):
|
||||
""" mark this message as seen. """
|
||||
self.account.mark_seen_messages([self.id])
|
||||
|
||||
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
@@ -288,3 +322,29 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
return code
|
||||
raise ValueError("message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
|
||||
|
||||
|
||||
#
|
||||
# some helper code for turning system messages into hook events
|
||||
#
|
||||
|
||||
def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if res:
|
||||
contact = msg.account.get_contact_by_addr(res[1])
|
||||
if contact:
|
||||
d = dict(chat=msg.chat, contact=contact, message=msg)
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y removed by a@b
|
||||
text = text.lower()
|
||||
parts = text.split()
|
||||
if parts[0] == "member":
|
||||
if parts[2] in ("removed", "added"):
|
||||
return parts[2], parts[1]
|
||||
if parts[3] in ("removed", "added"):
|
||||
return parts[3], parts[2].strip("()")
|
||||
|
||||
439
python/src/deltachat/testplugin.py
Normal file
439
python/src/deltachat/testplugin.py
Normal file
@@ -0,0 +1,439 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import queue
|
||||
import threading
|
||||
import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from . import Account, const
|
||||
from .tracker import ConfigureTracker
|
||||
from .capi import lib
|
||||
from .eventlogger import FFIEventLogger, FFIEventTracker
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest._code import Source
|
||||
|
||||
import deltachat
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
parser.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
|
||||
)
|
||||
cfg = config.getoption('--liveconfig')
|
||||
if not cfg:
|
||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||
if cfg:
|
||||
config.option.liveconfig = cfg
|
||||
|
||||
# Make sure we don't get garbled output because threads keep running
|
||||
# collect all ever created accounts in a weakref-set (so we don't
|
||||
# keep objects unneccessarily alive) and enable/disable logging
|
||||
# for each pytest test phase # (setup/call/teardown).
|
||||
# Additionally make the acfactory use a logging/no-logging default.
|
||||
|
||||
class LoggingAspect:
|
||||
def __init__(self):
|
||||
self._accounts = weakref.WeakSet()
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_init(self, account):
|
||||
self._accounts.add(account)
|
||||
|
||||
def disable_logging(self, item):
|
||||
for acc in self._accounts:
|
||||
acc.disable_logging()
|
||||
acfactory = item.funcargs.get("acfactory")
|
||||
if acfactory:
|
||||
acfactory.set_logging_default(False)
|
||||
|
||||
def enable_logging(self, item):
|
||||
for acc in self._accounts:
|
||||
acc.enable_logging()
|
||||
acfactory = item.funcargs.get("acfactory")
|
||||
if acfactory:
|
||||
acfactory.set_logging_default(True)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.enable_logging(item)
|
||||
yield
|
||||
self.disable_logging(item)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem):
|
||||
self.enable_logging(pyfuncitem)
|
||||
yield
|
||||
self.disable_logging(pyfuncitem)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.enable_logging(item)
|
||||
yield
|
||||
self.disable_logging(item)
|
||||
|
||||
la = LoggingAspect()
|
||||
config.pluginmanager.register(la)
|
||||
deltachat.register_global_plugin(la)
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
m = MonkeyPatch()
|
||||
try:
|
||||
m.setattr(sys.stdout, "write", lambda x: len(x))
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
m.undo()
|
||||
os.remove(t)
|
||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
)])
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
if "?" in cfg:
|
||||
url, token = cfg.split("?", 1)
|
||||
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
|
||||
else:
|
||||
summary.append('Liveconfig file: {}'.format(cfg))
|
||||
return summary
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
else:
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self):
|
||||
# trying to find test data heuristically
|
||||
# because we are run from a dev-setup with pytest direct,
|
||||
# through tox, and then maybe also from deltachat-binding
|
||||
# users like "deltabot".
|
||||
self.paths = [os.path.normpath(x) for x in [
|
||||
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data")
|
||||
]]
|
||||
|
||||
def get_path(self, bn):
|
||||
""" return path of file or None if it doesn't exist. """
|
||||
for path in self.paths:
|
||||
fn = os.path.join(path, *bn.split("/"))
|
||||
if os.path.exists(fn):
|
||||
return fn
|
||||
print("WARNING: path does not exist: {!r}".format(fn))
|
||||
|
||||
def read_path(self, bn, mode="r"):
|
||||
fn = self.get_path(bn)
|
||||
if fn is not None:
|
||||
with open(fn, mode) as f:
|
||||
return f.read()
|
||||
|
||||
return Data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self.init_time = time.time()
|
||||
self._generated_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
self.set_logging_default(False)
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
|
||||
while self._accounts:
|
||||
acc = self._accounts.pop()
|
||||
acc.shutdown()
|
||||
acc.disable_logging()
|
||||
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac._configtracker = ac.add_account_plugin(ConfigureTracker())
|
||||
if not quiet:
|
||||
ac.add_account_plugin(FFIEventLogger(ac, logid=logid))
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
def set_logging_default(self, logging):
|
||||
self._logging = bool(logging)
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
if self._generated_keys:
|
||||
keyname = self._generated_keys.pop(0)
|
||||
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
|
||||
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
|
||||
# do a pseudo-configured account
|
||||
addr = "addr{}@offline.org".format(self.offline_count)
|
||||
ac.set_config("addr", addr)
|
||||
self._preconfigure_key(ac, addr)
|
||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||
ac.set_config("mail_pw", "123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||
return ac
|
||||
|
||||
def get_online_config(self, pre_generated_key=True, quiet=False):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||
configdict = session_liveconfig.get(self.live_count)
|
||||
self.live_count += 1
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_watch"] = str(int(mvbox))
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac.start()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
ac1._configtracker.wait_imap_connected()
|
||||
ac1._configtracker.wait_smtp_connected()
|
||||
ac1._configtracker.wait_finish()
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self, move=False, quiet=False):
|
||||
ac1 = self.get_online_configuring_account(move=True, quiet=quiet)
|
||||
ac2 = self.get_online_configuring_account(quiet=quiet)
|
||||
ac1._configtracker.wait_finish()
|
||||
ac2._configtracker.wait_finish()
|
||||
return ac1, ac2
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_watch=account.get_config("mvbox_watch"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
ac.start()
|
||||
return ac
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
|
||||
bot_ac, bot_cfg = self.get_online_config()
|
||||
|
||||
args = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
fn,
|
||||
"--email", bot_cfg["addr"],
|
||||
"--password", bot_cfg["mail_pw"],
|
||||
bot_ac.db_path,
|
||||
]
|
||||
if ffi:
|
||||
args.insert(-1, "--show-ffi")
|
||||
print("$", " ".join(args))
|
||||
popen = subprocess.Popen(
|
||||
args=args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
|
||||
bufsize=0, # line buffering
|
||||
close_fds=True, # close all FDs other than 0/1/2
|
||||
universal_newlines=True # give back text
|
||||
)
|
||||
bot = BotProcess(popen, bot_cfg)
|
||||
self._finalizers.append(bot.kill)
|
||||
return bot
|
||||
|
||||
am = AccountMaker()
|
||||
request.addfinalizer(am.finalize)
|
||||
return am
|
||||
|
||||
|
||||
class BotProcess:
|
||||
def __init__(self, popen, bot_cfg):
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
|
||||
# we read stdout as quickly as we can in a thread and make
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self):
|
||||
try:
|
||||
while 1:
|
||||
line = self.popen.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
self.stdout_queue.put(line)
|
||||
finally:
|
||||
self.stdout_queue.put(None)
|
||||
|
||||
def kill(self):
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30):
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()]
|
||||
for next_pattern in patterns:
|
||||
print("+++FNMATCH:", next_pattern)
|
||||
ignored = []
|
||||
while 1:
|
||||
line = self.stdout_queue.get(timeout=15)
|
||||
if line is None:
|
||||
if ignored:
|
||||
print("BOT stdout terminated after these lines")
|
||||
for line in ignored:
|
||||
print(line)
|
||||
raise IOError("BOT stdout-thread terminated")
|
||||
if fnmatch.fnmatch(line, next_pattern):
|
||||
print("+++MATCHED:", line)
|
||||
break
|
||||
else:
|
||||
print("+++IGN:", line)
|
||||
ignored.append(line)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_db_path(tmpdir):
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
return Printer()
|
||||
76
python/src/deltachat/tracker.py
Normal file
76
python/src/deltachat/tracker.py
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
|
||||
from .hookspec import account_hookimpl
|
||||
|
||||
|
||||
class ImexFailed(RuntimeError):
|
||||
""" Exception for signalling that import/export operations failed."""
|
||||
|
||||
|
||||
class ImexTracker:
|
||||
def __init__(self):
|
||||
self._imex_events = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
|
||||
self._imex_events.put(ffi_event.data1)
|
||||
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(ffi_event.data1)
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, str):
|
||||
files_written.append(ev)
|
||||
elif ev == 0:
|
||||
raise ImexFailed("export failed, exp-files: {}".format(files_written))
|
||||
elif ev == 1000:
|
||||
return files_written
|
||||
|
||||
|
||||
class ConfigureFailed(RuntimeError):
|
||||
""" Exception for signalling that configuration failed."""
|
||||
|
||||
|
||||
class ConfigureTracker:
|
||||
ConfigureFailed = ConfigureFailed
|
||||
|
||||
def __init__(self):
|
||||
self._configure_events = Queue()
|
||||
self._smtp_finished = Event()
|
||||
self._imap_finished = Event()
|
||||
self._ffi_events = []
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self._ffi_events.append(ffi_event)
|
||||
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
|
||||
self._smtp_finished.set()
|
||||
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
|
||||
self._imap_finished.set()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(self, success):
|
||||
self._configure_events.put(success)
|
||||
|
||||
def wait_smtp_connected(self):
|
||||
""" wait until smtp is configured. """
|
||||
self._smtp_finished.wait()
|
||||
|
||||
def wait_imap_connected(self):
|
||||
""" wait until smtp is configured. """
|
||||
self._imap_finished.wait()
|
||||
|
||||
def wait_finish(self):
|
||||
""" wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
if not self._configure_events.get():
|
||||
content = "\n".join(map(str, self._ffi_events))
|
||||
raise ConfigureFailed(content)
|
||||
@@ -1,317 +1,18 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import py
|
||||
import pytest
|
||||
import requests
|
||||
import time
|
||||
from deltachat import Account
|
||||
from deltachat import const
|
||||
from deltachat.capi import lib
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import tempfile
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
parser.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
|
||||
)
|
||||
cfg = config.getoption('--liveconfig')
|
||||
if not cfg:
|
||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||
if cfg:
|
||||
config.option.liveconfig = cfg
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if (list(item.iter_markers(name="ignored"))
|
||||
and not item.config.getoption("ignored")):
|
||||
pytest.skip("Ignored tests not requested, use --ignored")
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
m = MonkeyPatch()
|
||||
try:
|
||||
m.setattr(sys.stdout, "write", lambda x: len(x))
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
m.undo()
|
||||
os.remove(t)
|
||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
)])
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
if "#" in cfg:
|
||||
url, token = cfg.split("#", 1)
|
||||
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
|
||||
else:
|
||||
summary.append('Liveconfig file: {}'.format(cfg))
|
||||
return summary
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def data():
|
||||
class Data:
|
||||
def __init__(self):
|
||||
self.path = os.path.join(os.path.dirname(__file__), "data")
|
||||
|
||||
def get_path(self, bn):
|
||||
fn = os.path.join(self.path, bn)
|
||||
assert os.path.exists(fn)
|
||||
return fn
|
||||
return Data()
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
else:
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def datadir():
|
||||
"""The py.path.local object of the test-data/ directory."""
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join('test-data')
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip('test-data directory not found')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
self.init_time = time.time()
|
||||
self._generated_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
|
||||
def make_account(self, path, logid):
|
||||
ac = Account(path, logid=logid)
|
||||
self._finalizers.append(ac.shutdown)
|
||||
return ac
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
if self._generated_keys:
|
||||
keyname = self._generated_keys.pop(0)
|
||||
fname_pub = "key/{name}-public.asc".format(name=keyname)
|
||||
fname_sec = "key/{name}-secret.asc".format(name=keyname)
|
||||
account._preconfigure_keypair(addr,
|
||||
datadir.join(fname_pub).read(),
|
||||
datadir.join(fname_sec).read())
|
||||
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
|
||||
# do a pseudo-configured account
|
||||
addr = "addr{}@offline.org".format(self.offline_count)
|
||||
ac.set_config("addr", addr)
|
||||
self._preconfigure_key(ac, addr)
|
||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||
ac.set_config("mail_pw", "123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||
return ac
|
||||
|
||||
def get_online_config(self, pre_generated_key=True):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||
configdict = session_liveconfig.get(self.live_count)
|
||||
self.live_count += 1
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False,
|
||||
pre_generated_key=True, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key)
|
||||
configdict.update(config)
|
||||
ac.configure(**configdict)
|
||||
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key)
|
||||
wait_successful_IMAP_SMTP_connection(ac1)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self):
|
||||
ac1 = self.get_online_configuring_account()
|
||||
ac2 = self.get_online_configuring_account()
|
||||
wait_successful_IMAP_SMTP_connection(ac1)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_successful_IMAP_SMTP_connection(ac2)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
return ac1, ac2
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(30)
|
||||
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
||||
ac.start_threads()
|
||||
return ac
|
||||
|
||||
am = AccountMaker()
|
||||
request.addfinalizer(am.finalize)
|
||||
return am
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_db_path(tmpdir):
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
return Printer()
|
||||
|
||||
|
||||
def wait_configuration_progress(account, min_target, max_target=1001):
|
||||
min_target = min(min_target, max_target)
|
||||
while 1:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
if data1 >= min_target and data1 <= max_target:
|
||||
event = account._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
if event.data1 >= min_target and event.data1 <= max_target:
|
||||
print("** CONFIG PROGRESS {}".format(min_target), account)
|
||||
break
|
||||
|
||||
|
||||
def wait_securejoin_inviter_progress(account, target):
|
||||
while 1:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||
if data2 >= target:
|
||||
event = account._evtracker.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||
if event.data2 >= target:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
||||
break
|
||||
|
||||
|
||||
def wait_successful_IMAP_SMTP_connection(account):
|
||||
imap_ok = smtp_ok = False
|
||||
while not imap_ok or not smtp_ok:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
|
||||
if evt_name == "DC_EVENT_IMAP_CONNECTED":
|
||||
imap_ok = True
|
||||
print("** IMAP OK", account)
|
||||
if evt_name == "DC_EVENT_SMTP_CONNECTED":
|
||||
smtp_ok = True
|
||||
print("** SMTP OK", account)
|
||||
print("** IMAP and SMTP logins successful", account)
|
||||
|
||||
|
||||
def wait_msgs_changed(account, chat_id, msg_id=None):
|
||||
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev[1] == chat_id
|
||||
if msg_id is not None:
|
||||
assert ev[2] == msg_id
|
||||
return ev[2]
|
||||
|
||||
1
python/tests/data/key
Symbolic link
1
python/tests/data/key
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../test-data/key
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,18 @@ import shutil
|
||||
import pytest
|
||||
from filecmp import cmp
|
||||
|
||||
from conftest import wait_configuration_progress, wait_msgs_changed
|
||||
from conftest import wait_configuration_progress
|
||||
from deltachat import const
|
||||
|
||||
|
||||
def wait_msgs_changed(account, chat_id, msg_id=None):
|
||||
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data1 == chat_id
|
||||
if msg_id is not None:
|
||||
assert ev.data2 == msg_id
|
||||
return ev.data2
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
@@ -91,22 +99,22 @@ class TestOnlineInCreation:
|
||||
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
||||
|
||||
lp.sec("wait for the messages to be delivered to SMTP")
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev[1] == chat.id
|
||||
assert ev[2] == prepared_original.id
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev[1] == chat2.id
|
||||
assert ev[2] == forwarded_id
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev.data1 == chat.id
|
||||
assert ev.data2 == prepared_original.id
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev.data1 == chat2.id
|
||||
assert ev.data2 == forwarded_id
|
||||
|
||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
received_original = ac2.get_message_by_id(ev1[2])
|
||||
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)
|
||||
assert cmp(received_original.filename, orig, shallow=False)
|
||||
|
||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev2[1] != ev1[1]
|
||||
received_copy = ac2.get_message_by_id(ev2[2])
|
||||
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)
|
||||
assert cmp(received_copy.filename, orig, shallow=False)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import print_function
|
||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
||||
from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
|
||||
@@ -16,21 +18,31 @@ def test_callback_None2int():
|
||||
clear_context_callback(ctx)
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir):
|
||||
from deltachat.account import Account
|
||||
p = tmpdir.join("hello.db")
|
||||
ac1 = Account(p.strpath)
|
||||
def test_dc_close_events(tmpdir, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
# register after_shutdown function
|
||||
shutdowns = []
|
||||
|
||||
class ShutdownPlugin:
|
||||
@global_hookimpl
|
||||
def dc_account_after_shutdown(self, account):
|
||||
assert account._dc_context is None
|
||||
shutdowns.append(account)
|
||||
register_global_plugin(ShutdownPlugin())
|
||||
assert hasattr(ac1, "_dc_context")
|
||||
ac1.shutdown()
|
||||
assert shutdowns == [ac1]
|
||||
|
||||
def find(info_string):
|
||||
evlog = ac1._evlogger
|
||||
evlog = ac1._evtracker
|
||||
while 1:
|
||||
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
||||
data2 = ev[2]
|
||||
data2 = ev.data2
|
||||
if info_string in data2:
|
||||
return
|
||||
else:
|
||||
print("skipping event", *ev)
|
||||
print("skipping event", ev)
|
||||
|
||||
find("disconnecting inbox-thread")
|
||||
find("disconnecting sentbox-thread")
|
||||
@@ -80,10 +92,10 @@ def test_markseen_invalid_message_ids(acfactory):
|
||||
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
|
||||
chat = ac1.create_chat_by_contact(contact1)
|
||||
chat.send_text("one messae")
|
||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg_ids = [9]
|
||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||
|
||||
|
||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
|
||||
@@ -7,13 +7,14 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
deps =
|
||||
@@ -40,13 +41,13 @@ deps =
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
flake8 src/deltachat
|
||||
flake8 tests/
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
sphinx==2.2.0
|
||||
sphinx
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
@@ -66,7 +67,6 @@ commands =
|
||||
|
||||
[pytest]
|
||||
addopts = -v -ra --strict-markers
|
||||
python_files = tests/test_*.py
|
||||
norecursedirs = .tox
|
||||
xfail_strict=true
|
||||
timeout = 90
|
||||
|
||||
@@ -1 +1 @@
|
||||
nightly-2019-11-06
|
||||
nightly-2020-03-12
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Aheader {
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Option<Self> {
|
||||
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Self::from_str(&value) {
|
||||
Ok(header) => {
|
||||
if addr_cmp(&header.addr, wanted_from) {
|
||||
@@ -127,14 +127,10 @@ impl str::FromStr for Aheader {
|
||||
.split(';')
|
||||
.filter_map(|a| {
|
||||
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
||||
if attribute.len() < 2 {
|
||||
return None;
|
||||
match &attribute[..] {
|
||||
[key, value] => Some((key.trim().to_string(), value.trim().to_string())),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
Some((
|
||||
attribute[0].trim().to_string(),
|
||||
attribute[1].trim().to_string(),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
100
src/blob.rs
100
src/blob.rs
@@ -6,13 +6,13 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use self::image::GenericImageView;
|
||||
use image::GenericImageView;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::constants::AVATAR_SIZE;
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
|
||||
extern crate image;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
///
|
||||
/// The object has a name, which will always be valid UTF-8. Having a
|
||||
@@ -56,7 +56,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: blobdir.to_path_buf(),
|
||||
blobname: name.clone(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
let blob = BlobObject {
|
||||
blobdir,
|
||||
@@ -84,7 +83,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
@@ -97,7 +95,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,7 +119,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobname: String::from(""),
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
|
||||
@@ -138,7 +134,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobname: name_for_err,
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
})?;
|
||||
let blob = BlobObject {
|
||||
@@ -198,17 +193,14 @@ impl<'a> BlobObject<'a> {
|
||||
.map_err(|_| BlobError::WrongBlobdir {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
src: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
}
|
||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
}
|
||||
@@ -236,7 +228,6 @@ impl<'a> BlobObject<'a> {
|
||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: PathBuf::from(name),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
}
|
||||
Ok(BlobObject {
|
||||
@@ -359,7 +350,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
|
||||
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
|
||||
@@ -372,7 +362,6 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -386,98 +375,41 @@ impl<'a> fmt::Display for BlobObject<'a> {
|
||||
}
|
||||
|
||||
/// Errors for the [BlobObject].
|
||||
#[derive(Fail, Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BlobError {
|
||||
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
|
||||
CreateFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[cause]
|
||||
#[source]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
|
||||
WriteFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[cause]
|
||||
#[source]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
|
||||
CopyFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
src: PathBuf,
|
||||
#[cause]
|
||||
#[source]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
|
||||
RecodeFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[cause]
|
||||
#[source]
|
||||
cause: image::ImageError,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
WrongBlobdir {
|
||||
blobdir: PathBuf,
|
||||
src: PathBuf,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
WrongName {
|
||||
blobname: PathBuf,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
}
|
||||
|
||||
// Implementing Display is done by hand because the failure
|
||||
// #[fail(display = "...")] syntax does not allow using
|
||||
// `blobdir.display()`.
|
||||
impl fmt::Display for BlobError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Match on the data rather than kind, they are equivalent for
|
||||
// identifying purposes but contain the actual data we need.
|
||||
match &self {
|
||||
BlobError::CreateFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Failed to create blob {} in {}",
|
||||
blobname,
|
||||
blobdir.display()
|
||||
),
|
||||
BlobError::WriteFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Failed to write data to blob {} in {}",
|
||||
blobname,
|
||||
blobdir.display()
|
||||
),
|
||||
BlobError::CopyFailure {
|
||||
blobdir,
|
||||
blobname,
|
||||
src,
|
||||
..
|
||||
} => write!(
|
||||
f,
|
||||
"Failed to copy data from {} to blob {} in {}",
|
||||
src.display(),
|
||||
blobname,
|
||||
blobdir.display(),
|
||||
),
|
||||
BlobError::RecodeFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
|
||||
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
|
||||
f,
|
||||
"File path {} is not in blobdir {}",
|
||||
src.display(),
|
||||
blobdir.display(),
|
||||
),
|
||||
BlobError::WrongName { blobname, .. } => {
|
||||
write!(f, "Blob has a bad name: {}", blobname.display(),)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
|
||||
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||
#[error("Blob has a badname {}", .blobname.display())]
|
||||
WrongName { blobname: PathBuf },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
312
src/chat.rs
312
src/chat.rs
@@ -15,7 +15,7 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::events::Event;
|
||||
use crate::job::*;
|
||||
use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId};
|
||||
@@ -286,10 +286,7 @@ impl ChatId {
|
||||
/// Returns `true`, if message was deleted, `false` otherwise.
|
||||
fn maybe_delete_draft(self, context: &Context) -> bool {
|
||||
match self.get_draft_msg_id(context) {
|
||||
Some(msg_id) => {
|
||||
Message::delete_from_db(context, msg_id);
|
||||
true
|
||||
}
|
||||
Some(msg_id) => msg_id.delete_from_db(context).is_ok(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -327,7 +324,7 @@ impl ChatId {
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
msg.text.as_ref().map(String::as_str).unwrap_or(""),
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
],
|
||||
@@ -362,6 +359,74 @@ impl ChatId {
|
||||
.unwrap_or_default() as usize
|
||||
}
|
||||
|
||||
pub(crate) fn get_param(self, context: &Context) -> Result<Params, Error> {
|
||||
let res: Option<String> = context
|
||||
.sql
|
||||
.query_get_value_result("SELECT param FROM chats WHERE id=?", params![self])?;
|
||||
Ok(res
|
||||
.map(|s| s.parse().unwrap_or_default())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
// Returns true if chat is a saved messages chat.
|
||||
pub fn is_self_talk(self, context: &Context) -> Result<bool, Error> {
|
||||
Ok(self.get_param(context)?.exists(Param::Selftalk))
|
||||
}
|
||||
|
||||
/// Returns true if chat is a device chat.
|
||||
pub fn is_device_talk(self, context: &Context) -> Result<bool, Error> {
|
||||
Ok(self.get_param(context)?.exists(Param::Devicetalk))
|
||||
}
|
||||
|
||||
fn parent_query<T, F>(self, context: &Context, fields: &str, f: F) -> sql::Result<Option<T>>
|
||||
where
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
{
|
||||
let sql = &context.sql;
|
||||
let query = format!(
|
||||
"SELECT {} \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;",
|
||||
fields
|
||||
);
|
||||
sql.query_row_optional(
|
||||
query,
|
||||
params![
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed
|
||||
],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_parent_mime_headers(self, context: &Context) -> Option<(String, String, String)> {
|
||||
let collect = |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?, row.get(2)?));
|
||||
self.parent_query(
|
||||
context,
|
||||
"rfc724_mid, mime_in_reply_to, mime_references",
|
||||
collect,
|
||||
)
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn parent_is_encrypted(self, context: &Context) -> Result<bool, Error> {
|
||||
let collect = |row: &rusqlite::Row| Ok(row.get(0)?);
|
||||
let packed: Option<String> = self.parent_query(context, "param", collect)?;
|
||||
|
||||
if let Some(ref packed) = packed {
|
||||
let param = packed.parse::<Params>()?;
|
||||
Ok(param.exists(Param::GuaranteeE2ee))
|
||||
} else {
|
||||
// No messages
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -539,76 +604,6 @@ impl Chat {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn get_subtitle(&self, context: &Context) -> String {
|
||||
// returns either the address or the number of chat members
|
||||
|
||||
if self.typ == Chattype::Single && self.param.exists(Param::Selftalk) {
|
||||
return context.stock_str(StockMessage::SelfTalkSubTitle).into();
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
return context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT c.addr
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?;",
|
||||
params![self.id],
|
||||
)
|
||||
.unwrap_or_else(|| "Err".into());
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup {
|
||||
if self.id.is_deaddrop() {
|
||||
return context.stock_str(StockMessage::DeadDrop).into();
|
||||
}
|
||||
let cnt = get_chat_contact_cnt(context, self.id);
|
||||
return context.stock_string_repl_int(StockMessage::Member, cnt as i32);
|
||||
}
|
||||
|
||||
"Err".to_string()
|
||||
}
|
||||
|
||||
fn parent_query(fields: &str) -> String {
|
||||
// Check for server_uid guarantees that we don't
|
||||
// select a draft or undelivered message.
|
||||
format!(
|
||||
"SELECT {} \
|
||||
FROM msgs WHERE chat_id=?1 AND server_uid!=0 \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;",
|
||||
fields
|
||||
)
|
||||
}
|
||||
|
||||
fn get_parent_mime_headers(&self, context: &Context) -> Option<(String, String, String)> {
|
||||
let collect = |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?, row.get(2)?));
|
||||
let params = params![self.id];
|
||||
let sql = &context.sql;
|
||||
|
||||
let query = Self::parent_query("rfc724_mid, mime_in_reply_to, mime_references");
|
||||
|
||||
sql.query_row(&query, params, collect).ok()
|
||||
}
|
||||
|
||||
fn parent_is_encrypted(&self, context: &Context) -> Result<bool, Error> {
|
||||
let sql = &context.sql;
|
||||
let params = params![self.id];
|
||||
let query = Self::parent_query("param");
|
||||
|
||||
let packed: Option<String> = sql.query_get_value_result(&query, params)?;
|
||||
|
||||
if let Some(ref packed) = packed {
|
||||
let param = packed.parse::<Params>()?;
|
||||
Ok(param.exists(Param::GuaranteeE2ee))
|
||||
} else {
|
||||
// No messages
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
@@ -666,7 +661,6 @@ impl Chat {
|
||||
is_sending_locations: self.is_sending_locations,
|
||||
color: self.get_color(context),
|
||||
profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new),
|
||||
subtitle: self.get_subtitle(context),
|
||||
draft,
|
||||
is_muted: self.is_muted(),
|
||||
})
|
||||
@@ -815,7 +809,7 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
|
||||
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)?) {
|
||||
if can_encrypt && (all_mutual || self.id.parent_is_encrypted(context)?) {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
}
|
||||
@@ -830,7 +824,7 @@ impl Chat {
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
if !self.is_self_talk() {
|
||||
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
self.get_parent_mime_headers(context)
|
||||
self.id.get_parent_mime_headers(context)
|
||||
{
|
||||
if !parent_rfc724_mid.is_empty() {
|
||||
new_in_reply_to = parent_rfc724_mid.clone();
|
||||
@@ -1009,9 +1003,6 @@ pub struct ChatInfo {
|
||||
/// currently.
|
||||
pub profile_image: PathBuf,
|
||||
|
||||
/// Subtitle for the chat.
|
||||
pub subtitle: String,
|
||||
|
||||
/// The draft message text.
|
||||
///
|
||||
/// If the chat has not draft this is an empty string.
|
||||
@@ -1450,6 +1441,18 @@ pub fn get_chat_msgs(
|
||||
flags: u32,
|
||||
marker1before: Option<MsgId>,
|
||||
) -> Vec<MsgId> {
|
||||
match delete_device_expired_messages(context) {
|
||||
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
|
||||
Ok(messages_deleted) => {
|
||||
if messages_deleted {
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let process_row =
|
||||
|row: &rusqlite::Row| Ok((row.get::<_, MsgId>("id")?, row.get::<_, i64>("timestamp")?));
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
@@ -1584,6 +1587,47 @@ pub fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to "delete_device_after" setting.
|
||||
///
|
||||
/// Returns true if any message is deleted, so event can be emitted. If nothing
|
||||
/// has been deleted, returns false.
|
||||
pub fn delete_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after() {
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context.sql.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
params![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(rows_modified > 0)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_chat_media(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -1658,17 +1702,17 @@ pub fn get_next_media(
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
);
|
||||
for i in 0..list.len() {
|
||||
if curr_msg_id == list[i] {
|
||||
for (i, msg_id) in list.iter().enumerate() {
|
||||
if curr_msg_id == *msg_id {
|
||||
match direction {
|
||||
Direction::Forward => {
|
||||
if i + 1 < list.len() {
|
||||
ret = Some(list[i + 1]);
|
||||
ret = list.get(i + 1).copied();
|
||||
}
|
||||
}
|
||||
Direction::Backward => {
|
||||
if i >= 1 {
|
||||
ret = Some(list[i - 1]);
|
||||
ret = list.get(i - 1).copied();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1750,36 +1794,52 @@ pub fn create_group_chat(
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// add a contact to the chats_contact table
|
||||
pub(crate) fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> bool {
|
||||
// add a contact to a chat; the function does not check the type or if any of the record exist or are already
|
||||
// added to the chat!
|
||||
sql::execute(
|
||||
match sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
params![chat_id, contact_id as i32],
|
||||
)
|
||||
.is_ok()
|
||||
) {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"could not add {} to chat {} table: {}", contact_id, chat_id, err
|
||||
);
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// remove a contact from the chats_contact table
|
||||
pub(crate) fn remove_from_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> bool {
|
||||
// remove a contact from the chats_contact table unconditionally
|
||||
// the function does not check the type or if the record exist
|
||||
sql::execute(
|
||||
match sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
params![chat_id, contact_id as i32],
|
||||
)
|
||||
.is_ok()
|
||||
) {
|
||||
Ok(()) => true,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
context,
|
||||
"could not remove contact {:?} from chat {:?}", contact_id, chat_id
|
||||
);
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a contact to the chat.
|
||||
@@ -1873,16 +1933,9 @@ pub(crate) fn add_contact_to_chat_ex(
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
|
||||
msg.id = send_msg(context, chat_id, &mut msg)?;
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
Ok(true)
|
||||
}
|
||||
@@ -2018,18 +2071,17 @@ impl rusqlite::types::FromSql for MuteDuration {
|
||||
|
||||
pub fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<(), Error> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
if real_group_exists(context, chat_id)
|
||||
&& sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
params![duration, chat_id],
|
||||
)
|
||||
.is_ok()
|
||||
if sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
params![duration, chat_id],
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("Failed to set name");
|
||||
bail!("Failed to set mute duration, chat might not exist -");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2064,7 +2116,6 @@ pub fn remove_contact_from_chat(
|
||||
)
|
||||
);
|
||||
} else {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
|
||||
if chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
@@ -2087,16 +2138,20 @@ pub fn remove_contact_from_chat(
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.id = send_msg(context, chat_id, &mut msg)?;
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
if remove_from_chat_contacts_table(context, chat_id, contact_id) {
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
// we remove the member from the chat after constructing the
|
||||
// to-be-send message. If between send_msg() and here the
|
||||
// process dies the user will have to re-do the action. It's
|
||||
// better than the other way round: you removed
|
||||
// someone from DB but no peer or device gets to know about it and
|
||||
// group membership is thus different on different devices.
|
||||
// Note also that sending a message needs all recipients
|
||||
// in order to correctly determine encryption so if we
|
||||
// removed it first, it would complicate the
|
||||
// check/encryption logic.
|
||||
success = remove_from_chat_contacts_table(context, chat_id, contact_id);
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2541,7 +2596,6 @@ mod tests {
|
||||
"is_sending_locations": false,
|
||||
"color": 15895624,
|
||||
"profile_image": "",
|
||||
"subtitle": "bob@example.com",
|
||||
"draft": "",
|
||||
"is_muted": false
|
||||
}
|
||||
@@ -3051,4 +3105,16 @@ mod tests {
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent_is_encrypted() {
|
||||
let t = dummy_context();
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
|
||||
assert!(!chat_id.parent_is_encrypted(&t.ctx).unwrap());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t.ctx, Some(&mut msg));
|
||||
assert!(!chat_id.parent_is_encrypted(&t.ctx).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
128
src/chatlist.rs
128
src/chatlist.rs
@@ -1,10 +1,11 @@
|
||||
//! # Chat list module
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::error::Result;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::stock::StockMessage;
|
||||
@@ -73,6 +74,9 @@ impl Chatlist {
|
||||
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat,
|
||||
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
/// 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)
|
||||
@@ -88,6 +92,17 @@ impl Chatlist {
|
||||
query: Option<&str>,
|
||||
query_contact_id: Option<u32>,
|
||||
) -> Result<Self> {
|
||||
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
|
||||
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
|
||||
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
|
||||
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
|
||||
|
||||
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||
// messages get deleted to avoid reloading the same chatlist.
|
||||
if let Err(err) = delete_device_expired_messages(context) {
|
||||
warn!(context, "Failed to hide expired messages: {}", err);
|
||||
}
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
@@ -101,6 +116,14 @@ impl Chatlist {
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
// select with left join and minimum:
|
||||
//
|
||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||
@@ -137,8 +160,11 @@ impl Chatlist {
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
||||
} else if flag_archived_only {
|
||||
// show archived chats
|
||||
// (this includes the archived device-chat; we could skip it,
|
||||
// however, then the number of archived chats do not match, which might be even more irritating.
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
@@ -178,18 +204,25 @@ impl Chatlist {
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND c.name LIKE ?
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, str_like_cmd],
|
||||
params![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let sort_id_up = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
@@ -200,21 +233,23 @@ impl Chatlist {
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?2
|
||||
AND NOT c.archived=?3
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?;
|
||||
if 0 == listflags & DC_GCL_NO_SPECIALS {
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
if !flag_for_forwarding {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
add_archived_link_item = true;
|
||||
}
|
||||
@@ -222,7 +257,7 @@ impl Chatlist {
|
||||
};
|
||||
|
||||
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
|
||||
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
||||
}
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
||||
@@ -245,18 +280,20 @@ impl Chatlist {
|
||||
///
|
||||
/// To get the message object from the message ID, use dc_get_chat().
|
||||
pub fn get_chat_id(&self, index: usize) -> ChatId {
|
||||
if index >= self.ids.len() {
|
||||
return ChatId::new(0);
|
||||
match self.ids.get(index) {
|
||||
Some((chat_id, _msg_id)) => *chat_id,
|
||||
None => ChatId::new(0),
|
||||
}
|
||||
self.ids[index].0
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
ensure!(index < self.ids.len(), "Chatlist index out of range");
|
||||
Ok(self.ids[index].1)
|
||||
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.
|
||||
@@ -280,25 +317,27 @@ impl Chatlist {
|
||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||
let mut ret = Lot::new();
|
||||
|
||||
if index >= self.ids.len() {
|
||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||
return ret;
|
||||
}
|
||||
let (chat_id, lastmsg_id) = match self.ids.get(index) {
|
||||
Some(ids) => ids,
|
||||
None => {
|
||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
let chat_loaded: Chat;
|
||||
let chat = if let Some(chat) = chat {
|
||||
chat
|
||||
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
|
||||
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id) {
|
||||
chat_loaded = chat;
|
||||
&chat_loaded
|
||||
} else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let lastmsg_id = self.ids[index].1;
|
||||
let mut lastcontact = None;
|
||||
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id) {
|
||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
@@ -399,6 +438,25 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_self_talk_up_on_forward() {
|
||||
let t = dummy_context();
|
||||
t.ctx.update_device_chats().unwrap();
|
||||
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert!(chats.len() == 3);
|
||||
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).unwrap();
|
||||
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_special_chat_names() {
|
||||
let t = dummy_context();
|
||||
@@ -421,4 +479,18 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_summary_unwrap() {
|
||||
let t = dummy_context();
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
|
||||
chat_id1.set_draft(&t.ctx, Some(&mut msg));
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
let summary = chats.get_summary(&t.ctx, 0, None);
|
||||
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::events::Event;
|
||||
use crate::job::*;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::stock::StockMessage;
|
||||
use rusqlite::NO_PARAMS;
|
||||
@@ -62,9 +65,31 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
/// Equals to 0 by default, which means the message is never
|
||||
/// deleted.
|
||||
///
|
||||
/// Value 1 is treated as "delete at once": messages are deleted
|
||||
/// immediately, without moving to DeltaChat folder.
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteServerAfter,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// device.
|
||||
///
|
||||
/// Equals to 0 by default, which means the message is never
|
||||
/// deleted.
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
SaveMimeHeaders,
|
||||
ConfiguredAddr,
|
||||
ConfiguredMailServer,
|
||||
@@ -128,6 +153,29 @@ impl Context {
|
||||
self.get_config_int(key) != 0
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub fn get_config_delete_server_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteServerAfter) {
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x as i64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(x)` means delete
|
||||
/// after `x` seconds.
|
||||
pub fn get_config_delete_device_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter) {
|
||||
0 => None,
|
||||
x => Some(x as i64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||
@@ -171,6 +219,15 @@ impl Context {
|
||||
|
||||
self.sql.set_raw_config(self, key, val)
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value);
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.call_cb(Event::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
ret
|
||||
}
|
||||
_ => self.sql.set_raw_config(self, key, value),
|
||||
}
|
||||
}
|
||||
@@ -194,9 +251,11 @@ mod tests {
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::constants;
|
||||
use crate::constants::AVATAR_SIZE;
|
||||
use crate::test_utils::*;
|
||||
use image::GenericImageView;
|
||||
use num_traits::FromPrimitive;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
@@ -269,4 +328,44 @@ mod tests {
|
||||
assert_eq!(img.width(), AVATAR_SIZE);
|
||||
assert_eq!(img.height(), AVATAR_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selfavatar_copy_without_recode() {
|
||||
let t = dummy_context();
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
File::create(&avatar_src)
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.unwrap();
|
||||
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
|
||||
assert!(!avatar_blob.exists());
|
||||
t.ctx
|
||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists());
|
||||
assert_eq!(
|
||||
std::fs::metadata(&avatar_blob).unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_quality_config_option() {
|
||||
let t = dummy_context();
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality);
|
||||
assert_eq!(media_quality, 0);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Balanced);
|
||||
|
||||
t.ctx.set_config(Config::MediaQuality, Some("1")).unwrap();
|
||||
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality);
|
||||
assert_eq!(media_quality, 1);
|
||||
assert_eq!(constants::MediaQuality::Worse as i32, 1);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Worse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
use crate::constants::*;
|
||||
@@ -9,33 +8,7 @@ use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
|
||||
use super::read_url::read_url;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Invalid email address: {:?}", _0)]
|
||||
InvalidEmailAddress(String),
|
||||
|
||||
#[fail(display = "XML error at position {}", position)]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[cause]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[fail(display = "Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[fail(display = "Failed to get URL {}", _0)]
|
||||
ReadUrlError(#[cause] super::read_url::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<super::read_url::Error> for Error {
|
||||
fn from(err: super::read_url::Error) -> Error {
|
||||
Error::ReadUrlError(err)
|
||||
}
|
||||
}
|
||||
use super::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MozAutoconfigure<'a> {
|
||||
@@ -65,16 +38,16 @@ enum MozConfigTag {
|
||||
Username,
|
||||
}
|
||||
|
||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|
||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
|
||||
// Split address into local part and domain part.
|
||||
let p = in_emailaddr
|
||||
.find('@')
|
||||
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
|
||||
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
|
||||
let in_emaildomain = &in_emaildomain[1..];
|
||||
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
|
||||
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
|
||||
[domain, local] => (local, domain),
|
||||
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
|
||||
};
|
||||
|
||||
let mut moz_ac = MozAutoconfigure {
|
||||
in_emailaddr,
|
||||
@@ -125,7 +98,7 @@ pub fn moz_autoconfigure(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
param_in: &LoginParam,
|
||||
) -> Result<LoginParam> {
|
||||
) -> Result<LoginParam, Error> {
|
||||
let xml_raw = read_url(context, url)?;
|
||||
|
||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Outlook's Autodiscover
|
||||
|
||||
use quick_xml;
|
||||
use quick_xml::events::BytesEnd;
|
||||
|
||||
use crate::constants::*;
|
||||
@@ -8,33 +7,7 @@ use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
|
||||
use super::read_url::read_url;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "XML error at position {}", position)]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[cause]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[fail(display = "Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[fail(display = "Failed to get URL {}", _0)]
|
||||
ReadUrlError(#[cause] super::read_url::Error),
|
||||
|
||||
#[fail(display = "Number of redirection is exceeded")]
|
||||
RedirectionError,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<super::read_url::Error> for Error {
|
||||
fn from(err: super::read_url::Error) -> Error {
|
||||
Error::ReadUrlError(err)
|
||||
}
|
||||
}
|
||||
use super::Error;
|
||||
|
||||
struct OutlookAutodiscover {
|
||||
pub out: LoginParam,
|
||||
@@ -52,7 +25,7 @@ enum ParsingResult {
|
||||
RedirectUrl(String),
|
||||
}
|
||||
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
let mut outlk_ad = OutlookAutodiscover {
|
||||
out: LoginParam::new(),
|
||||
out_imap_set: false,
|
||||
@@ -143,7 +116,7 @@ pub fn outlk_autodiscover(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
_param_in: &LoginParam,
|
||||
) -> Result<LoginParam> {
|
||||
) -> Result<LoginParam, Error> {
|
||||
let mut url = url.to_string();
|
||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||
for _i in 0..10 {
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::format_err;
|
||||
use crate::job::{self, job_add, job_kill_action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::oauth2::*;
|
||||
@@ -68,6 +69,7 @@ pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
let mut smtp_connected_here = false;
|
||||
|
||||
let mut param_autoconfig: Option<LoginParam> = None;
|
||||
let was_configured_before = context.is_configured();
|
||||
|
||||
context
|
||||
.inbox_thread
|
||||
@@ -371,7 +373,7 @@ pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
let create_mvbox = context.get_config_bool(Config::MvboxWatch)
|
||||
|| context.get_config_bool(Config::MvboxMove);
|
||||
let imap = &context.inbox_thread.read().unwrap().imap;
|
||||
if let Err(err) = imap.ensure_configured_folders(context, create_mvbox) {
|
||||
if let Err(err) = imap.configure_folders(context, create_mvbox) {
|
||||
warn!(context, "configuring folders failed: {:?}", err);
|
||||
false
|
||||
} else {
|
||||
@@ -440,6 +442,15 @@ pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
}
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
if !was_configured_before {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
info!(context, "apply config_defaults {}={}", def.key, def.value);
|
||||
context.set_config(def.key, Some(def.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
@@ -651,6 +662,28 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid email address: {0:?}")]
|
||||
InvalidEmailAddress(String),
|
||||
|
||||
#[error("XML error at position {position}")]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[source]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[error("Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[error("Failed to get URL")]
|
||||
ReadUrlError(#[from] self::read_url::Error),
|
||||
|
||||
#[error("Number of redirection is exceeded")]
|
||||
RedirectionError,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "URL request error")]
|
||||
GetError(#[cause] reqwest::Error),
|
||||
#[error("URL request error")]
|
||||
GetError(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn read_url(context: &Context, url: &str) -> Result<String> {
|
||||
pub fn read_url(context: &Context, url: &str) -> Result<String, Error> {
|
||||
info!(context, "Requesting URL {}", url);
|
||||
|
||||
match reqwest::blocking::Client::new()
|
||||
|
||||
@@ -57,6 +57,19 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum MediaQuality {
|
||||
Balanced = 0,
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
impl Default for MediaQuality {
|
||||
fn default() -> Self {
|
||||
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
@@ -80,6 +93,7 @@ pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
|
||||
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
||||
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
||||
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
||||
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
|
||||
|
||||
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
|
||||
@@ -196,13 +210,13 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
|
||||
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
||||
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
||||
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
|
||||
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
|
||||
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
|
||||
|
||||
// QR code scanning (view from Bob, the joiner)
|
||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
||||
@@ -213,6 +227,9 @@ pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
// max. width/height of an avatar
|
||||
pub const AVATAR_SIZE: u32 = 192;
|
||||
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
@@ -310,8 +327,6 @@ const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
||||
const DC_STR_NOMESSAGES: usize = 1;
|
||||
const DC_STR_SELF: usize = 2;
|
||||
const DC_STR_DRAFT: usize = 3;
|
||||
const DC_STR_MEMBER: usize = 4;
|
||||
const DC_STR_CONTACT: usize = 6;
|
||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
||||
const DC_STR_DEADDROP: usize = 8;
|
||||
const DC_STR_IMAGE: usize = 9;
|
||||
@@ -343,7 +358,6 @@ const DC_STR_ARCHIVEDCHATS: usize = 40;
|
||||
const DC_STR_STARREDMSGS: usize = 41;
|
||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
|
||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::path::PathBuf;
|
||||
|
||||
use deltachat_derive::*;
|
||||
use itertools::Itertools;
|
||||
use rusqlite;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
@@ -12,10 +11,9 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::events::Event;
|
||||
use crate::key::*;
|
||||
use crate::key::{DcKey, Key, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{MessageState, MsgId};
|
||||
use crate::mimeparser::AvatarAction;
|
||||
@@ -24,9 +22,6 @@ use crate::peerstate::*;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// Contacts with at least this origin value are shown in the contact list.
|
||||
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
|
||||
|
||||
/// An object representing a single contact in memory.
|
||||
///
|
||||
/// The contact object is not updated.
|
||||
@@ -94,6 +89,7 @@ pub enum Origin {
|
||||
UnhandledQrScan = 0x80,
|
||||
|
||||
/// Reply-To: of incoming message of known sender
|
||||
/// Contacts with at least this origin value are shown in the contact list.
|
||||
IncomingReplyTo = 0x100,
|
||||
|
||||
/// Cc: of incoming message of known sender
|
||||
@@ -118,7 +114,7 @@ pub enum Origin {
|
||||
Internal = 0x40000,
|
||||
|
||||
/// address is in our address book
|
||||
AdressBook = 0x80000,
|
||||
AddressBook = 0x80000,
|
||||
|
||||
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
|
||||
SecurejoinInvited = 0x0100_0000,
|
||||
@@ -274,7 +270,7 @@ impl Contact {
|
||||
///
|
||||
/// To validate an e-mail address independently of the contact database
|
||||
/// use `dc_may_be_valid_addr()`.
|
||||
pub fn lookup_id_by_addr(context: &Context, addr: impl AsRef<str>) -> u32 {
|
||||
pub fn lookup_id_by_addr(context: &Context, addr: impl AsRef<str>, min_origin: Origin) -> u32 {
|
||||
if addr.as_ref().is_empty() {
|
||||
return 0;
|
||||
}
|
||||
@@ -287,14 +283,13 @@ impl Contact {
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
return DC_CONTACT_ID_SELF;
|
||||
}
|
||||
|
||||
context.sql.query_get_value(
|
||||
context,
|
||||
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
params![
|
||||
addr_normalized,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
DC_ORIGIN_MIN_CONTACT_LIST,
|
||||
min_origin as u32,
|
||||
],
|
||||
).unwrap_or_default()
|
||||
}
|
||||
@@ -497,9 +492,18 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
|
||||
let name = normalize_name(name);
|
||||
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
match Contact::add_or_lookup(context, name, addr, Origin::AddressBook) {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to add address {} from address book: {}", addr, err
|
||||
);
|
||||
}
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if modify_cnt > 0 {
|
||||
@@ -642,8 +646,6 @@ impl Contact {
|
||||
let peerstate = Peerstate::from_addr(context, &context.sql, &contact.addr);
|
||||
let loginparam = LoginParam::from_database(context, "configured_");
|
||||
|
||||
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
||||
|
||||
if peerstate.is_some()
|
||||
&& peerstate
|
||||
.as_ref()
|
||||
@@ -658,16 +660,11 @@ impl Contact {
|
||||
StockMessage::E2eAvailable
|
||||
});
|
||||
ret += &p;
|
||||
if self_key.is_none() {
|
||||
e2ee::ensure_secret_key_exists(context)?;
|
||||
self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
||||
}
|
||||
let self_key = Key::from(SignedPublicKey::load_self(context)?);
|
||||
let p = context.stock_str(StockMessage::FingerPrints);
|
||||
ret += &format!(" {}:", p);
|
||||
|
||||
let fingerprint_self = self_key
|
||||
.map(|k| k.formatted_fingerprint())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_self = self_key.formatted_fingerprint();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
|
||||
.map(|k| k.formatted_fingerprint())
|
||||
@@ -1058,7 +1055,7 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
||||
}
|
||||
|
||||
let len = full_name.len();
|
||||
if len > 0 {
|
||||
if len > 1 {
|
||||
let firstchar = full_name.as_bytes()[0];
|
||||
let lastchar = full_name.as_bytes()[len - 1];
|
||||
if firstchar == b'\'' && lastchar == b'\''
|
||||
@@ -1111,10 +1108,9 @@ fn cat_fingerprint(
|
||||
impl Context {
|
||||
/// determine whether the specified addr maps to the/a self addr
|
||||
pub fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
let self_addr = match self.get_config(Config::ConfiguredAddr) {
|
||||
Some(s) => s,
|
||||
None => return Err(Error::NotConfigured),
|
||||
};
|
||||
let self_addr = self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
}
|
||||
@@ -1167,6 +1163,10 @@ mod tests {
|
||||
fn test_normalize_name() {
|
||||
assert_eq!(&normalize_name("Doe, John"), "John Doe");
|
||||
assert_eq!(&normalize_name(" hello world "), "hello world");
|
||||
assert_eq!(&normalize_name("<"), "<");
|
||||
assert_eq!(&normalize_name(">"), ">");
|
||||
assert_eq!(&normalize_name("'"), "'");
|
||||
assert_eq!(&normalize_name("\""), "\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1229,6 +1229,7 @@ mod tests {
|
||||
let book = concat!(
|
||||
" Name one \n one@eins.org \n",
|
||||
"Name two\ntwo@deux.net\n",
|
||||
"Invalid\n+1234567890\n", // invalid, should be ignored
|
||||
"\nthree@drei.sam\n",
|
||||
"Name two\ntwo@deux.net\n" // should not be added again
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Context module
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
||||
@@ -9,18 +9,20 @@ use crate::chat::*;
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::dc_tools::duration_to_str;
|
||||
use crate::error::*;
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
use crate::job::*;
|
||||
use crate::job_thread::JobThread;
|
||||
use crate::key::Key;
|
||||
use crate::key::{DcKey, Key, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||
use crate::param::Params;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Callback function type for [Context]
|
||||
///
|
||||
@@ -57,6 +59,7 @@ pub struct Context {
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
pub generating_key_mutex: Mutex<()>,
|
||||
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
creation_time: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -71,8 +74,8 @@ pub struct RunningState {
|
||||
/// actual keys and their values which will be present are not
|
||||
/// guaranteed. Calling [Context::get_info] also includes information
|
||||
/// about the context on top of the information here.
|
||||
pub fn get_info() -> HashMap<&'static str, String> {
|
||||
let mut res = HashMap::new();
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
@@ -138,6 +141,7 @@ impl Context {
|
||||
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
};
|
||||
|
||||
ensure!(
|
||||
@@ -222,7 +226,7 @@ impl Context {
|
||||
* UI chat/message related API
|
||||
******************************************************************************/
|
||||
|
||||
pub fn get_info(&self) -> HashMap<&'static str, String> {
|
||||
pub fn get_info(&self) -> BTreeMap<&'static str, String> {
|
||||
let unset = "0";
|
||||
let l = LoginParam::from_database(self, "");
|
||||
let l2 = LoginParam::from_database(self, "configured_");
|
||||
@@ -236,7 +240,10 @@ impl Context {
|
||||
.sql
|
||||
.get_raw_config_int(self, "dbversion")
|
||||
.unwrap_or_default();
|
||||
|
||||
let journal_mode = self
|
||||
.sql
|
||||
.query_get_value(self, "PRAGMA journal_mode;", rusqlite::NO_PARAMS)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled);
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled);
|
||||
let bcc_self = self.get_config_int(Config::BccSelf);
|
||||
@@ -251,10 +258,9 @@ impl Context {
|
||||
rusqlite::NO_PARAMS,
|
||||
);
|
||||
|
||||
let fingerprint_str = if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql) {
|
||||
key.fingerprint()
|
||||
} else {
|
||||
"<Not yet calculated>".into()
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self) {
|
||||
Ok(key) => Key::from(key).fingerprint(),
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch);
|
||||
@@ -282,6 +288,7 @@ impl Context {
|
||||
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());
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
|
||||
res.insert(
|
||||
@@ -312,6 +319,9 @@ impl Context {
|
||||
);
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Result;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::job::*;
|
||||
@@ -17,7 +19,7 @@ use crate::message::{self, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::securejoin::{self, handle_securejoin_handshake};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{contact, location};
|
||||
@@ -32,6 +34,10 @@ enum CreateEvent {
|
||||
}
|
||||
|
||||
/// Receive a message and add it to the database.
|
||||
///
|
||||
/// Returns an error on recoverable errors, e.g. database errors. In this case,
|
||||
/// message parsing should be retried later. If message itself is wrong, logs
|
||||
/// the error and returns success.
|
||||
pub fn dc_receive_imf(
|
||||
context: &Context,
|
||||
imf_raw: &[u8],
|
||||
@@ -55,10 +61,19 @@ pub fn dc_receive_imf(
|
||||
println!("{}", String::from_utf8_lossy(imf_raw));
|
||||
}
|
||||
|
||||
let mut mime_parser = MimeMessage::from_bytes(context, imf_raw)?;
|
||||
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw) {
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf: can't parse MIME: {}", err);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(mime_parser) => mime_parser,
|
||||
};
|
||||
|
||||
// we can not add even an empty record if we have no info whatsoever
|
||||
ensure!(mime_parser.has_headers(), "No Headers Found");
|
||||
if !mime_parser.has_headers() {
|
||||
warn!(context, "dc_receive_imf: no headers found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// the function returns the number of created messages in the database
|
||||
let mut chat_id = ChatId::new(0);
|
||||
@@ -97,29 +112,23 @@ pub fn dc_receive_imf(
|
||||
// we do not check Return-Path any more as this is unreliable, see
|
||||
// https://github.com/deltachat/deltachat-core/issues/150)
|
||||
let (from_id, from_id_blocked, incoming_origin) =
|
||||
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
|
||||
from_field_to_contact_id(context, field_from)?
|
||||
} else {
|
||||
(0, false, Origin::Unknown)
|
||||
};
|
||||
from_field_to_contact_id(context, &mime_parser.from)?;
|
||||
|
||||
let incoming = from_id != DC_CONTACT_ID_SELF;
|
||||
|
||||
let mut to_ids = ContactIds::new();
|
||||
for header_def in &[HeaderDef::To, HeaderDef::Cc] {
|
||||
if let Some(field) = mime_parser.get(header_def.clone()) {
|
||||
to_ids.extend(&dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&field,
|
||||
if !incoming {
|
||||
Origin::OutgoingTo
|
||||
} else if incoming_origin.is_known() {
|
||||
Origin::IncomingTo
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
to_ids.extend(&dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
if !incoming {
|
||||
Origin::OutgoingTo
|
||||
} else if incoming_origin.is_known() {
|
||||
Origin::IncomingTo
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
)?);
|
||||
|
||||
// Add parts
|
||||
|
||||
@@ -192,17 +201,24 @@ pub fn dc_receive_imf(
|
||||
};
|
||||
}
|
||||
|
||||
// if we delete we don't need to try moving messages
|
||||
if needs_delete_job && !created_db_entries.is_empty() {
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
created_db_entries[0].1.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id);
|
||||
// Get user-configured server deletion
|
||||
let delete_server_after = context.get_config_delete_server_after();
|
||||
|
||||
if !created_db_entries.is_empty() {
|
||||
if needs_delete_job || delete_server_after == Some(0) {
|
||||
for db_entry in &created_db_entries {
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
db_entry.1.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Move message if we don't delete it immediately.
|
||||
context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -212,7 +228,7 @@ pub fn dc_receive_imf(
|
||||
|
||||
cleanup(context, &create_event_to_send, created_db_entries);
|
||||
|
||||
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
|
||||
mime_parser.handle_reports(context, from_id, sent_timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -222,11 +238,11 @@ pub fn dc_receive_imf(
|
||||
/// Also returns whether it is blocked or not and its origin.
|
||||
pub fn from_field_to_contact_id(
|
||||
context: &Context,
|
||||
field_from: &str,
|
||||
from_address_list: &[SingleInfo],
|
||||
) -> Result<(u32, bool, Origin)> {
|
||||
let from_ids = dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&field_from,
|
||||
from_address_list,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)?;
|
||||
|
||||
@@ -236,7 +252,7 @@ pub fn from_field_to_contact_id(
|
||||
if from_ids.len() > 1 {
|
||||
warn!(
|
||||
context,
|
||||
"mail has more than one From address, only using first: {:?}", field_from
|
||||
"mail has more than one From address, only using first: {:?}", from_address_list
|
||||
);
|
||||
}
|
||||
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
|
||||
@@ -249,7 +265,10 @@ pub fn from_field_to_contact_id(
|
||||
}
|
||||
Ok((from_id, from_id_blocked, incoming_origin))
|
||||
} else {
|
||||
warn!(context, "mail has an empty From header: {:?}", field_from);
|
||||
warn!(
|
||||
context,
|
||||
"mail has an empty From header: {:?}", from_address_list
|
||||
);
|
||||
// if there is no from given, from_id stays 0 which is just fine. These messages
|
||||
// are very rare, however, we have to add them to the database (they go to the
|
||||
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||
@@ -291,14 +310,15 @@ fn add_parts(
|
||||
// check, if the mail is already in our database - if so, just update the folder/uid
|
||||
// (if the mail was moved around) and finish. (we may get a mail twice eg. if it is
|
||||
// moved between folders. make sure, this check is done eg. before securejoin-processing) */
|
||||
if let Ok((old_server_folder, old_server_uid, _)) =
|
||||
message::rfc724_mid_exists(context, &rfc724_mid)
|
||||
if let Some((old_server_folder, old_server_uid, _)) =
|
||||
message::rfc724_mid_exists(context, &rfc724_mid)?
|
||||
{
|
||||
if old_server_folder != server_folder.as_ref() || old_server_uid != server_uid {
|
||||
message::update_server_uid(context, &rfc724_mid, server_folder.as_ref(), server_uid);
|
||||
}
|
||||
|
||||
bail!("Message already in DB");
|
||||
warn!(context, "Message already in DB");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut msgrmsg = if mime_parser.has_chat_version() {
|
||||
@@ -340,11 +360,9 @@ fn add_parts(
|
||||
};
|
||||
to_id = DC_CONTACT_ID_SELF;
|
||||
|
||||
// handshake messages must be processed _before_ chats are created
|
||||
// (eg. contacs may be marked as verified)
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
|
||||
// avoid discarding by show_emails setting
|
||||
msgrmsg = MessengerMessage::Yes;
|
||||
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||
*chat_id = ChatId::new(0);
|
||||
allow_creation = true;
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id) {
|
||||
@@ -358,14 +376,14 @@ fn add_parts(
|
||||
state = MessageState::InSeen;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// Message will still be processed as "member
|
||||
// added" or similar system message.
|
||||
// process messages as "member added" normally
|
||||
}
|
||||
Err(err) => {
|
||||
*hidden = true;
|
||||
context.bob.write().unwrap().status = 0; // secure-join failed
|
||||
context.stop_ongoing();
|
||||
error!(context, "Error in Secure-Join message handling: {}", err);
|
||||
warn!(context, "Error in Secure-Join message handling: {}", err);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,7 +407,11 @@ fn add_parts(
|
||||
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
allow_creation,
|
||||
if test_normal_chat_id.is_unset() {
|
||||
allow_creation
|
||||
} else {
|
||||
true
|
||||
},
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
@@ -472,6 +494,28 @@ fn add_parts(
|
||||
// We cannot recreate other states (read, error).
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
|
||||
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||
*chat_id = ChatId::new(0);
|
||||
allow_creation = true;
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id) {
|
||||
Ok(securejoin::HandshakeMessage::Done)
|
||||
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
*hidden = true;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// process messages as "member added" normally
|
||||
}
|
||||
Err(err) => {
|
||||
*hidden = true;
|
||||
warn!(context, "Error in Secure-Join watching: {}", err);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !to_ids.is_empty() {
|
||||
if chat_id.is_unset() {
|
||||
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
|
||||
@@ -579,10 +623,13 @@ fn add_parts(
|
||||
let subject = mime_parser.get_subject().unwrap_or_default();
|
||||
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
if mime_parser.location_kml.is_some()
|
||||
let is_mdn = !mime_parser.reports.is_empty();
|
||||
|
||||
let is_location_kml = mime_parser.location_kml.is_some()
|
||||
&& icnt == 1
|
||||
&& (part.msg == "-location-" || part.msg.is_empty())
|
||||
{
|
||||
&& (part.msg == "-location-" || part.msg.is_empty());
|
||||
|
||||
if is_mdn || is_location_kml {
|
||||
*hidden = true;
|
||||
if state == MessageState::InFresh {
|
||||
state = MessageState::InNoticed;
|
||||
@@ -642,7 +689,7 @@ fn add_parts(
|
||||
);
|
||||
|
||||
// check event to send
|
||||
if chat_id.is_trash() {
|
||||
if chat_id.is_trash() || *hidden {
|
||||
*create_event_to_send = None;
|
||||
} else if incoming && state == MessageState::InFresh {
|
||||
if from_id_blocked {
|
||||
@@ -768,7 +815,6 @@ fn create_or_lookup_group(
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
let mut recreate_member_list = false;
|
||||
let mut send_EVENT_CHAT_MODIFIED = false;
|
||||
let mut X_MrRemoveFromGrp = None;
|
||||
let mut X_MrAddToGrp = None;
|
||||
let mut X_MrGrpNameChanged = false;
|
||||
let mut better_msg: String = From::from("");
|
||||
@@ -816,22 +862,25 @@ fn create_or_lookup_group(
|
||||
// but we might not know about this group
|
||||
|
||||
let grpname = mime_parser.get(HeaderDef::ChatGroupName).cloned();
|
||||
let mut removed_id = 0;
|
||||
|
||||
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupMemberRemoved).cloned() {
|
||||
X_MrRemoveFromGrp = Some(optional_field);
|
||||
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
||||
let left_group = Contact::lookup_id_by_addr(context, X_MrRemoveFromGrp.as_ref().unwrap())
|
||||
== from_id as u32;
|
||||
better_msg = context.stock_system_msg(
|
||||
if left_group {
|
||||
StockMessage::MsgGroupLeft
|
||||
} else {
|
||||
StockMessage::MsgDelMember
|
||||
},
|
||||
X_MrRemoveFromGrp.as_ref().unwrap(),
|
||||
"",
|
||||
from_id as u32,
|
||||
)
|
||||
if let Some(removed_addr) = mime_parser.get(HeaderDef::ChatGroupMemberRemoved).cloned() {
|
||||
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown);
|
||||
if removed_id == 0 {
|
||||
warn!(context, "removed {:?} has no contact_id", removed_addr);
|
||||
} else {
|
||||
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
||||
better_msg = context.stock_system_msg(
|
||||
if removed_id == from_id as u32 {
|
||||
StockMessage::MsgGroupLeft
|
||||
} else {
|
||||
StockMessage::MsgDelMember
|
||||
},
|
||||
&removed_addr,
|
||||
"",
|
||||
from_id as u32,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned();
|
||||
if let Some(optional_field) = field {
|
||||
@@ -917,7 +966,7 @@ fn create_or_lookup_group(
|
||||
&& !grpid.is_empty()
|
||||
&& grpname.is_some()
|
||||
// otherwise, a pending "quit" message may pop up
|
||||
&& X_MrRemoveFromGrp.is_none()
|
||||
&& removed_id == 0
|
||||
// re-create explicitly left groups only if ourself is re-added
|
||||
&& (!group_explicitly_left
|
||||
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
|
||||
@@ -1023,6 +1072,17 @@ fn create_or_lookup_group(
|
||||
// add members to group/check members
|
||||
if recreate_member_list {
|
||||
if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
|
||||
// Members could have been removed while we were
|
||||
// absent. We can't use existing member list and need to
|
||||
// start from scratch.
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
params![chat_id],
|
||||
)
|
||||
.ok();
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF);
|
||||
}
|
||||
if from_id > DC_CONTACT_ID_LAST_SPECIAL
|
||||
@@ -1040,12 +1100,8 @@ fn create_or_lookup_group(
|
||||
}
|
||||
}
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
} else if let Some(removed_addr) = X_MrRemoveFromGrp {
|
||||
let contact_id = Contact::lookup_id_by_addr(context, removed_addr);
|
||||
if contact_id != 0 {
|
||||
info!(context, "remove {:?} from chat id={}", contact_id, chat_id);
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id);
|
||||
}
|
||||
} else if removed_id > 0 {
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, removed_id);
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
}
|
||||
|
||||
@@ -1151,9 +1207,9 @@ fn create_or_lookup_adhoc_group(
|
||||
return Ok((ChatId::new(0), Blocked::Not));
|
||||
}
|
||||
// use subject as initial chat name
|
||||
let grpname = mime_parser.get_subject().unwrap_or_else(|| {
|
||||
context.stock_string_repl_int(StockMessage::Member, member_ids.len() as i32)
|
||||
});
|
||||
let grpname = mime_parser
|
||||
.get_subject()
|
||||
.unwrap_or_else(|| "Unnamed group".to_string());
|
||||
|
||||
// create group record
|
||||
let new_chat_id: ChatId = create_group_record(
|
||||
@@ -1464,7 +1520,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||
if let Ok(ids) = parse_message_ids(mid_list) {
|
||||
for id in ids.iter() {
|
||||
if is_known_rfc724_mid(context, id) {
|
||||
return true;
|
||||
@@ -1477,6 +1533,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
|
||||
/// Check if a message is a reply to a known message (messenger or non-messenger).
|
||||
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -1511,7 +1568,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
|
||||
}
|
||||
|
||||
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||
if let Ok(ids) = parse_message_ids(mid_list) {
|
||||
for id in ids.iter() {
|
||||
if is_msgrmsg_rfc724_mid(context, id) {
|
||||
return true;
|
||||
@@ -1523,6 +1580,7 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -
|
||||
|
||||
/// Check if a message is a reply to any messenger message.
|
||||
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -1534,38 +1592,17 @@ fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
|
||||
fn dc_add_or_lookup_contacts_by_address_list(
|
||||
context: &Context,
|
||||
addr_list_raw: &str,
|
||||
address_list: &[SingleInfo],
|
||||
origin: Origin,
|
||||
) -> Result<ContactIds> {
|
||||
let addrs = match mailparse::addrparse(addr_list_raw) {
|
||||
Ok(addrs) => addrs,
|
||||
Err(err) => {
|
||||
bail!("could not parse {:?}: {:?}", addr_list_raw, err);
|
||||
}
|
||||
};
|
||||
|
||||
let mut contact_ids = ContactIds::new();
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(info) => {
|
||||
contact_ids.insert(add_or_lookup_contact_by_addr(
|
||||
context,
|
||||
&info.display_name,
|
||||
&info.addr,
|
||||
origin,
|
||||
)?);
|
||||
}
|
||||
mailparse::MailAddr::Group(infos) => {
|
||||
for info in &infos.addrs {
|
||||
contact_ids.insert(add_or_lookup_contact_by_addr(
|
||||
context,
|
||||
&info.display_name,
|
||||
&info.addr,
|
||||
origin,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
for info in address_list.iter() {
|
||||
contact_ids.insert(add_or_lookup_contact_by_addr(
|
||||
context,
|
||||
&info.display_name,
|
||||
&info.addr,
|
||||
origin,
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(contact_ids)
|
||||
@@ -1612,7 +1649,10 @@ fn dc_create_incoming_rfc724_mid(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::dummy_context;
|
||||
use crate::chat::ChatVisibility;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{dummy_context, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_hex_hash() {
|
||||
@@ -1671,4 +1711,435 @@ mod tests {
|
||||
Some("123-45-9@stub".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_known_rfc724_mid() {
|
||||
let t = dummy_context();
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("first message".to_string());
|
||||
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
|
||||
let msg = Message::load_from_db(&t.ctx, msg_id).unwrap();
|
||||
|
||||
// Message-IDs may or may not be surrounded by angle brackets
|
||||
assert!(is_known_rfc724_mid(
|
||||
&t.ctx,
|
||||
format!("<{}>", msg.rfc724_mid).as_str()
|
||||
));
|
||||
assert!(is_known_rfc724_mid(&t.ctx, &msg.rfc724_mid));
|
||||
assert!(!is_known_rfc724_mid(&t.ctx, "nonexistant@message.id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_msgrmsg_rfc724_mid() {
|
||||
let t = dummy_context();
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("first message".to_string());
|
||||
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
|
||||
let msg = Message::load_from_db(&t.ctx, msg_id).unwrap();
|
||||
|
||||
// Message-IDs may or may not be surrounded by angle brackets
|
||||
assert!(is_msgrmsg_rfc724_mid(
|
||||
&t.ctx,
|
||||
format!("<{}>", msg.rfc724_mid).as_str()
|
||||
));
|
||||
assert!(is_msgrmsg_rfc724_mid(&t.ctx, &msg.rfc724_mid));
|
||||
assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id"));
|
||||
}
|
||||
|
||||
fn configured_offline_context() -> TestContext {
|
||||
let t = dummy_context();
|
||||
t.ctx
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.unwrap();
|
||||
t.ctx.set_config(Config::Configured, Some("1")).unwrap();
|
||||
t
|
||||
}
|
||||
|
||||
static MSGRMSG: &[u8] = b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Chat: hello\n\
|
||||
Message-ID: <Mr.1111@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
static GRP_MAIL: &[u8] = b"From: bob@example.org\n\
|
||||
To: alice@example.org, claire@example.org\n\
|
||||
Subject: group with Alice, Bob and Claire\n\
|
||||
Message-ID: <3333@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_chats_only() {
|
||||
let t = configured_offline_context();
|
||||
assert_eq!(t.ctx.get_config_int(Config::ShowEmails), 0);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_accepted_contact_unknown() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_accepted_contact_known() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
|
||||
Contact::create(&t.ctx, "Bob", "bob@example.org").unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
|
||||
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
|
||||
// (and existent chat is required)
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
|
||||
|
||||
// accept Bob by accepting a delta-message from Bob
|
||||
dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.get_chat_id(0).is_deaddrop());
|
||||
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
assert!(!chat_id.is_special());
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.name, "Bob");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 1);
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 1);
|
||||
|
||||
// receive a non-delta-message from Bob, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false).unwrap();
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 2);
|
||||
|
||||
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 3, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_all() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("2")).unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=all will show up in the deaddrop
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.get_chat_id(0).is_deaddrop());
|
||||
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_receipt_and_unarchive() {
|
||||
// create alice's account
|
||||
let t = configured_offline_context();
|
||||
|
||||
// create one-to-one with bob, archive one-to-one
|
||||
let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org").unwrap();
|
||||
let one2one_id = chat::create_by_contact_id(&t.ctx, bob_id).unwrap();
|
||||
one2one_id
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.unwrap();
|
||||
let one2one = Chat::load_from_db(&t.ctx, one2one_id).unwrap();
|
||||
assert!(one2one.get_visibility() == ChatVisibility::Archived);
|
||||
|
||||
// create a group with bob, archive group
|
||||
let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
|
||||
chat::add_contact_to_chat(&t.ctx, group_id, bob_id);
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, group_id, 0, None).len(), 0);
|
||||
group_id
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.unwrap();
|
||||
let group = Chat::load_from_db(&t.ctx, group_id).unwrap();
|
||||
assert!(group.get_visibility() == ChatVisibility::Archived);
|
||||
|
||||
// everything archived, chatlist should be empty
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t.ctx, DC_GCL_NO_SPECIALS, None, None)
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
// send a message to group with bob
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
format!(
|
||||
"From: alice@example.org\n\
|
||||
To: bob@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <Gr.{}.12345678901@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: {}\n\
|
||||
Chat-Group-Name: foo\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
group.grpid, group.grpid
|
||||
)
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None);
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()).unwrap();
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert_eq!(msg.text.unwrap(), "hello");
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
let group = Chat::load_from_db(&t.ctx, group_id).unwrap();
|
||||
assert!(group.get_visibility() == ChatVisibility::Normal);
|
||||
|
||||
// bob sends a read receipt to the group
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
format!(
|
||||
"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <Mr.12345678902@example.org>\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
Read receipts do not guarantee sth. was read.\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Reporting-UA: Delta Chat 1.28.0\n\
|
||||
Original-Recipient: rfc822;bob@example.org\n\
|
||||
Final-Recipient: rfc822;bob@example.org\n\
|
||||
Original-Message-ID: <Gr.{}.12345678901@example.org>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP--",
|
||||
group.grpid
|
||||
)
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, group_id, 0, None).len(), 1);
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()).unwrap();
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
|
||||
// check, the read-receipt has not unarchived the one2one
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t.ctx, DC_GCL_NO_SPECIALS, None, None)
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let one2one = Chat::load_from_db(&t.ctx, one2one_id).unwrap();
|
||||
assert!(one2one.get_visibility() == ChatVisibility::Archived);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_from() {
|
||||
// if there is no from given, from_id stays 0 which is just fine. These messages
|
||||
// are very rare, however, we have to add them to the database (they go to the
|
||||
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||
|
||||
let t = configured_offline_context();
|
||||
let context = &t.ctx;
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert!(chats.get_msg_id(0).is_err());
|
||||
|
||||
dc_receive_imf(
|
||||
context,
|
||||
b"To: bob@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <3924@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
// Check that the message was added to the database:
|
||||
assert!(chats.get_msg_id(0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escaped_from() {
|
||||
let t = configured_offline_context();
|
||||
let contact_id = Contact::create(&t.ctx, "foobar", "foobar@example.com").unwrap();
|
||||
let chat_id = chat::create_by_contact_id(&t.ctx, contact_id).unwrap();
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
).unwrap();
|
||||
assert_eq!(
|
||||
Contact::load_from_db(&t.ctx, contact_id)
|
||||
.unwrap()
|
||||
.get_authname(),
|
||||
"Фамилия Имя", // The name was "Имя, Фамилия" and ("lastname, firstname") and should be swapped to "firstname, lastname"
|
||||
);
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None);
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()).unwrap();
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert_eq!(msg.text.unwrap(), "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escaped_recipients() {
|
||||
let t = configured_offline_context();
|
||||
Contact::create(&t.ctx, "foobar", "foobar@example.com").unwrap();
|
||||
|
||||
let carl_contact_id =
|
||||
Contact::add_or_lookup(&t.ctx, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom)
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: Foobar <foobar@example.com>\n\
|
||||
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\
|
||||
Cc: =?utf-8?q?=3Ch2=3E?= <carl@host.tld>\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
Contact::load_from_db(&t.ctx, carl_contact_id)
|
||||
.unwrap()
|
||||
.get_name(),
|
||||
"h2"
|
||||
);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
let msg = Message::load_from_db(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert_eq!(msg.text.unwrap(), "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cc_to_contact() {
|
||||
let t = configured_offline_context();
|
||||
Contact::create(&t.ctx, "foobar", "foobar@example.com").unwrap();
|
||||
|
||||
let carl_contact_id = Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"garabage",
|
||||
"carl@host.tld",
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: Foobar <foobar@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Cc: Carl <carl@host.tld>\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
Contact::load_from_db(&t.ctx, carl_contact_id)
|
||||
.unwrap()
|
||||
.get_name(),
|
||||
"Carl"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
181
src/dc_tools.rs
181
src/dc_tools.rs
@@ -5,24 +5,24 @@ use core::cmp::{max, min};
|
||||
use std::borrow::Cow;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::SystemTime;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::{fmt, fs};
|
||||
|
||||
use chrono::{Local, TimeZone};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::events::Event;
|
||||
|
||||
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
0 != v && 0 == v & (v - 1)
|
||||
}
|
||||
|
||||
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
|
||||
/// the shortened string.
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
|
||||
let ellipse = if do_unwrap { "..." } else { "[...]" };
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let ellipse = "[...]";
|
||||
|
||||
let count = buf.chars().count();
|
||||
if approx_chars > 0 && count > approx_chars + ellipse.len() {
|
||||
@@ -75,6 +75,14 @@ pub fn dc_timestamp_to_str(wanted: i64) -> String {
|
||||
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
pub fn duration_to_str(duration: Duration) -> String {
|
||||
let secs = duration.as_secs();
|
||||
let h = secs / 3600;
|
||||
let m = (secs % 3600) / 60;
|
||||
let s = (secs % 3600) % 60;
|
||||
format!("{}h {}m {}s", h, m, s)
|
||||
}
|
||||
|
||||
pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
||||
the function may return negative values. */
|
||||
@@ -452,6 +460,23 @@ pub(crate) fn time() -> i64 {
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// An invalid email address was encountered
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid email address: {message} ({addr})")]
|
||||
pub struct InvalidEmailError {
|
||||
message: String,
|
||||
addr: String,
|
||||
}
|
||||
|
||||
impl InvalidEmailError {
|
||||
fn new(msg: impl Into<String>, addr: impl Into<String>) -> InvalidEmailError {
|
||||
InvalidEmailError {
|
||||
message: msg.into(),
|
||||
addr: addr.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Very simple email address wrapper.
|
||||
///
|
||||
/// Represents an email address, right now just the `name@domain` portion.
|
||||
@@ -475,7 +500,7 @@ pub struct EmailAddress {
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(input: &str) -> Result<Self, Error> {
|
||||
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
|
||||
input.parse::<EmailAddress>()
|
||||
}
|
||||
}
|
||||
@@ -487,31 +512,55 @@ impl fmt::Display for EmailAddress {
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
type Err = InvalidEmailError;
|
||||
|
||||
/// Performs a dead-simple parse of an email address.
|
||||
fn from_str(input: &str) -> Result<EmailAddress, Error> {
|
||||
ensure!(!input.is_empty(), "empty string is not valid");
|
||||
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
|
||||
if input.is_empty() {
|
||||
return Err(InvalidEmailError::new("empty string is not valid", input));
|
||||
}
|
||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||
|
||||
ensure!(parts.len() > 1, "missing '@' character");
|
||||
let local = parts[1];
|
||||
let domain = parts[0];
|
||||
let err = |msg: &str| {
|
||||
Err(InvalidEmailError {
|
||||
message: msg.to_string(),
|
||||
addr: input.to_string(),
|
||||
})
|
||||
};
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => err("missing '@' character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(
|
||||
!local.is_empty(),
|
||||
"empty string is not valid for local part"
|
||||
);
|
||||
ensure!(domain.len() > 3, "domain is too short");
|
||||
|
||||
let dot = domain.find('.');
|
||||
ensure!(dot.is_some(), "invalid domain");
|
||||
ensure!(dot.unwrap() < domain.len() - 2, "invalid domain");
|
||||
|
||||
Ok(EmailAddress {
|
||||
local: local.to_string(),
|
||||
domain: domain.to_string(),
|
||||
})
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,54 +587,42 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate_1() {
|
||||
let s = "this is a little test string";
|
||||
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
|
||||
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
|
||||
assert_eq!(dc_truncate(s, 16), "this is a [...]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_2() {
|
||||
assert_eq!(dc_truncate("1234", 2, false), "1234");
|
||||
assert_eq!(dc_truncate("1234", 2, true), "1234");
|
||||
assert_eq!(dc_truncate("1234", 2), "1234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_3() {
|
||||
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
|
||||
assert_eq!(dc_truncate("1234567", 1, true), "1...");
|
||||
assert_eq!(dc_truncate("1234567", 1), "1[...]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_4() {
|
||||
assert_eq!(dc_truncate("123456", 4, false), "123456");
|
||||
assert_eq!(dc_truncate("123456", 4, true), "123456");
|
||||
assert_eq!(dc_truncate("123456", 4), "123456");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_edge() {
|
||||
assert_eq!(dc_truncate("", 4, false), "");
|
||||
assert_eq!(dc_truncate("", 4, true), "");
|
||||
assert_eq!(dc_truncate("", 4), "");
|
||||
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
|
||||
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 丽ⷐએ", 1, false),
|
||||
"𐠈[...]"
|
||||
);
|
||||
assert_eq!(
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
|
||||
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
|
||||
);
|
||||
|
||||
// 9 characters, so no truncation
|
||||
assert_eq!(
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
|
||||
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
|
||||
);
|
||||
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
|
||||
|
||||
// 12 characters, truncation
|
||||
assert_eq!(
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
|
||||
"𑒀ὐ¢🜀\u{1e01b}A[...]",
|
||||
);
|
||||
}
|
||||
@@ -701,11 +738,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate(
|
||||
buf: String,
|
||||
approx_chars in 0..10000usize,
|
||||
do_unwrap: bool,
|
||||
approx_chars in 0..10000usize
|
||||
) {
|
||||
let res = dc_truncate(&buf, approx_chars, do_unwrap);
|
||||
let el_len = if do_unwrap { 3 } else { 5 };
|
||||
let res = dc_truncate(&buf, approx_chars);
|
||||
let el_len = 5;
|
||||
let l = res.chars().count();
|
||||
if approx_chars > 0 {
|
||||
assert!(
|
||||
@@ -719,11 +755,7 @@ mod tests {
|
||||
|
||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
if do_unwrap {
|
||||
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
|
||||
} else {
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,4 +862,37 @@ mod tests {
|
||||
let next = dc_smeared_time(&t.ctx);
|
||||
assert!((start + count - 1) < next);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_to_str() {
|
||||
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s");
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(59 * 60 + 59)),
|
||||
"0h 59m 59s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(59 * 60 + 60)),
|
||||
"1h 0m 0s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)),
|
||||
"2h 59m 59s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)),
|
||||
"3h 0m 0s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)),
|
||||
"3h 0m 59s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)),
|
||||
"3h 1m 0s"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
//! A module to remove HTML tags from the email text
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
lazy_static! {
|
||||
@@ -35,6 +34,7 @@ pub fn dehtml(buf: &str) -> String {
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
reader.check_end_names(false);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
@@ -225,4 +225,23 @@ mod tests {
|
||||
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unclosed_tags() {
|
||||
let input = r##"
|
||||
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'
|
||||
'http://www.w3.org/TR/html4/loose.dtd'>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hi</title>
|
||||
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>
|
||||
</head>
|
||||
<body>
|
||||
lots of text
|
||||
</body>
|
||||
</html>
|
||||
"##;
|
||||
let txt = dehtml(input);
|
||||
assert_eq!(txt.trim(), "lots of text");
|
||||
}
|
||||
}
|
||||
|
||||
100
src/e2ee.rs
100
src/e2ee.rs
@@ -1,19 +1,17 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::error::*;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::headerdef::HeaderDefMap;
|
||||
use crate::key::{DcKey, Key, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::pgp;
|
||||
@@ -38,7 +36,7 @@ impl EncryptHelper {
|
||||
Some(addr) => addr,
|
||||
};
|
||||
|
||||
let public_key = load_or_generate_self_public_key(context, &addr)?;
|
||||
let public_key = SignedPublicKey::load_self(context)?;
|
||||
|
||||
Ok(EncryptHelper {
|
||||
prefer_encrypt,
|
||||
@@ -108,8 +106,7 @@ impl EncryptHelper {
|
||||
}
|
||||
let public_key = Key::from(self.public_key.clone());
|
||||
keyring.add_ref(&public_key);
|
||||
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
|
||||
.ok_or_else(|| format_err!("missing own private key"))?;
|
||||
let sign_key = Key::from(SignedSecretKey::load_self(context)?);
|
||||
|
||||
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
||||
|
||||
@@ -126,8 +123,8 @@ pub fn try_decrypt(
|
||||
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
||||
let from = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::From_)?
|
||||
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
|
||||
.get_header(HeaderDef::From_)
|
||||
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
|
||||
.and_then(|from| from.extract_single_info())
|
||||
.map(|from| from.addr)
|
||||
.unwrap_or_default();
|
||||
@@ -189,43 +186,6 @@ pub fn try_decrypt(
|
||||
Ok((out_mail, signatures))
|
||||
}
|
||||
|
||||
/// Load public key from database or generate a new one.
|
||||
///
|
||||
/// This will load a public key from the database, generating and
|
||||
/// storing a new one when one doesn't exist yet. Care is taken to
|
||||
/// only generate one key per context even when multiple threads call
|
||||
/// this function concurrently.
|
||||
fn load_or_generate_self_public_key(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
) -> Result<SignedPublicKey> {
|
||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
||||
return SignedPublicKey::try_from(key)
|
||||
.map_err(|_| Error::Message("Not a public key".into()));
|
||||
}
|
||||
let _guard = context.generating_key_mutex.lock().unwrap();
|
||||
|
||||
// Check again in case the key was generated while we were waiting for the lock.
|
||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
||||
return SignedPublicKey::try_from(key)
|
||||
.map_err(|_| Error::Message("Not a public key".into()));
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let keygen_type =
|
||||
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)).unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keygen_type);
|
||||
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
|
||||
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(keypair.public)
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
|
||||
ensure!(
|
||||
@@ -347,6 +307,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
||||
///
|
||||
/// If this succeeds you are also guaranteed that the
|
||||
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
||||
// TODO, remove this once deltachat::key::Key no longer exists.
|
||||
pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| {
|
||||
format_err!(concat!(
|
||||
@@ -354,7 +315,7 @@ pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
"cannot ensure secret key if not configured."
|
||||
))
|
||||
})?;
|
||||
load_or_generate_self_public_key(context, &self_addr)?;
|
||||
SignedPublicKey::load_self(context)?;
|
||||
Ok(self_addr)
|
||||
}
|
||||
|
||||
@@ -405,49 +366,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
);
|
||||
}
|
||||
|
||||
mod load_or_generate_self_public_key {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_existing() {
|
||||
let t = dummy_context();
|
||||
let addr = configure_alice_keypair(&t.ctx);
|
||||
let key = load_or_generate_self_public_key(&t.ctx, addr);
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // generating keys is expensive
|
||||
fn test_generate() {
|
||||
let t = dummy_context();
|
||||
let addr = "alice@example.org";
|
||||
let key0 = load_or_generate_self_public_key(&t.ctx, addr);
|
||||
assert!(key0.is_ok());
|
||||
let key1 = load_or_generate_self_public_key(&t.ctx, addr);
|
||||
assert!(key1.is_ok());
|
||||
assert_eq!(key0.unwrap(), key1.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_generate_concurrent() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let t = dummy_context();
|
||||
let ctx = Arc::new(t.ctx);
|
||||
let ctx0 = Arc::clone(&ctx);
|
||||
let thr0 =
|
||||
thread::spawn(move || load_or_generate_self_public_key(&ctx0, "alice@example.org"));
|
||||
let ctx1 = Arc::clone(&ctx);
|
||||
let thr1 =
|
||||
thread::spawn(move || load_or_generate_self_public_key(&ctx1, "alice@example.org"));
|
||||
let res0 = thr0.join().unwrap();
|
||||
let res1 = thr1.join().unwrap();
|
||||
assert_eq!(res0.unwrap(), res1.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_decrypted_pgp_armor() {
|
||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||
|
||||
190
src/error.rs
190
src/error.rs
@@ -1,189 +1,13 @@
|
||||
//! # Error handling
|
||||
|
||||
use lettre_email::mime;
|
||||
pub use anyhow::{bail, ensure, format_err, Error, Result};
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Failure(failure::Error),
|
||||
|
||||
#[fail(display = "SQL error: {:?}", _0)]
|
||||
SqlError(#[cause] crate::sql::Error),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Io(std::io::Error),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Message(String),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Image(image_meta::ImageError),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Utf8(std::str::Utf8Error),
|
||||
|
||||
#[fail(display = "PGP: {:?}", _0)]
|
||||
Pgp(pgp::errors::Error),
|
||||
|
||||
#[fail(display = "Base64Decode: {:?}", _0)]
|
||||
Base64Decode(base64::DecodeError),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
FromUtf8(std::string::FromUtf8Error),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
BlobError(#[cause] crate::blob::BlobError),
|
||||
|
||||
#[fail(display = "Invalid Message ID.")]
|
||||
InvalidMsgId,
|
||||
|
||||
#[fail(display = "Watch folder not found {:?}", _0)]
|
||||
WatchFolderNotFound(String),
|
||||
|
||||
#[fail(display = "Invalid Email: {:?}", _0)]
|
||||
MailParseError(#[cause] mailparse::MailParseError),
|
||||
|
||||
#[fail(display = "Building invalid Email: {:?}", _0)]
|
||||
LettreError(#[cause] lettre_email::error::Error),
|
||||
|
||||
#[fail(display = "SMTP error: {:?}", _0)]
|
||||
SmtpError(#[cause] async_smtp::error::Error),
|
||||
|
||||
#[fail(display = "FromStr error: {:?}", _0)]
|
||||
FromStr(#[cause] mime::FromStrError),
|
||||
|
||||
#[fail(display = "Not Configured")]
|
||||
NotConfigured,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<crate::sql::Error> for Error {
|
||||
fn from(err: crate::sql::Error) -> Error {
|
||||
Error::SqlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
fn from(err: base64::DecodeError) -> Error {
|
||||
Error::Base64Decode(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<failure::Error> for Error {
|
||||
fn from(err: failure::Error) -> Error {
|
||||
Error::Failure(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::str::Utf8Error> for Error {
|
||||
fn from(err: std::str::Utf8Error) -> Error {
|
||||
Error::Utf8(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image_meta::ImageError> for Error {
|
||||
fn from(err: image_meta::ImageError) -> Error {
|
||||
Error::Image(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::errors::Error> for Error {
|
||||
fn from(err: pgp::errors::Error) -> Error {
|
||||
Error::Pgp(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(err: std::string::FromUtf8Error) -> Error {
|
||||
Error::FromUtf8(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::blob::BlobError> for Error {
|
||||
fn from(err: crate::blob::BlobError) -> Error {
|
||||
Error::BlobError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::message::InvalidMsgId> for Error {
|
||||
fn from(_err: crate::message::InvalidMsgId) -> Error {
|
||||
Error::InvalidMsgId
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::key::SaveKeyError> for Error {
|
||||
fn from(err: crate::key::SaveKeyError) -> Error {
|
||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::pgp::PgpKeygenError> for Error {
|
||||
fn from(err: crate::pgp::PgpKeygenError) -> Error {
|
||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mailparse::MailParseError> for Error {
|
||||
fn from(err: mailparse::MailParseError) -> Error {
|
||||
Error::MailParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre_email::error::Error> for Error {
|
||||
fn from(err: lettre_email::error::Error) -> Error {
|
||||
Error::LettreError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mime::FromStrError> for Error {
|
||||
fn from(err: mime::FromStrError) -> Error {
|
||||
Error::FromStr(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($e:expr) => {
|
||||
return Err($crate::error::Error::Message($e.to_string()));
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
return Err($crate::error::Error::Message(format!($fmt, $($arg)+)));
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! format_err {
|
||||
($e:expr) => {
|
||||
$crate::error::Error::Message($e.to_string());
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
$crate::error::Error::Message(format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! ensure {
|
||||
($cond:expr, $e:expr) => {
|
||||
if !($cond) {
|
||||
bail!($e);
|
||||
}
|
||||
};
|
||||
($cond:expr, $fmt:expr, $($arg:tt)+) => {
|
||||
if !($cond) {
|
||||
bail!($fmt, $($arg)+);
|
||||
}
|
||||
};
|
||||
}
|
||||
// #[fail(display = "Invalid Message ID.")]
|
||||
// InvalidMsgId,
|
||||
// #[fail(display = "Watch folder not found {:?}", _0)]
|
||||
// WatchFolderNotFound(String),
|
||||
// #[fail(display = "Not Configured")]
|
||||
// NotConfigured,
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure_eq {
|
||||
|
||||
@@ -201,10 +201,4 @@ pub enum Event {
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
|
||||
/// This event is sent out to the inviter when a joiner successfully joined a group.
|
||||
/// @param data1 (int) chat_id
|
||||
/// @param data2 (int) contact_id
|
||||
#[strum(props(id = "2062"))]
|
||||
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::strum::AsStaticRef;
|
||||
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
|
||||
use mailparse::{MailHeader, MailHeaderMap};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
@@ -52,13 +52,17 @@ impl HeaderDef {
|
||||
}
|
||||
|
||||
pub trait HeaderDefMap {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
|
||||
}
|
||||
|
||||
impl HeaderDefMap for [MailHeader<'_>] {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||
self.get_first_value(headerdef.get_headername())
|
||||
}
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
|
||||
self.get_first_header(headerdef.get_headername())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -79,18 +83,13 @@ mod tests {
|
||||
let (headers, _) =
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||
assert_eq!(
|
||||
headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.unwrap(),
|
||||
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||
Some("v99".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::From_).unwrap(),
|
||||
headers.get_header_value(HeaderDef::From_),
|
||||
Some("Bob".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,28 +15,22 @@ use super::session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
||||
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP IDLE protocol timed out")]
|
||||
IdleTimeout(#[cause] async_std::future::TimeoutError),
|
||||
#[error("IMAP IDLE protocol timed out")]
|
||||
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||
|
||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
||||
#[error("IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
SelectFolderError(#[cause] select_folder::Error),
|
||||
#[error("IMAP select folder error")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[fail(display = "Setup handle error")]
|
||||
SetupHandleError(#[cause] super::Error),
|
||||
}
|
||||
|
||||
impl From<select_folder::Error> for Error {
|
||||
fn from(err: select_folder::Error) -> Error {
|
||||
Error::SelectFolderError(err)
|
||||
}
|
||||
#[error("Setup handle error")]
|
||||
SetupHandleError(#[from] super::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -71,9 +65,7 @@ impl Imap {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
}
|
||||
|
||||
self.setup_handle_if_needed(context)
|
||||
.await
|
||||
.map_err(Error::SetupHandleError)?;
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
@@ -84,9 +76,7 @@ impl Imap {
|
||||
// BEWARE: If you change the Secure branch you
|
||||
// typically also need to change the Insecure branch.
|
||||
IdleHandle::Secure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
handle.init().await?;
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
@@ -141,9 +131,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
IdleHandle::Insecure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
handle.init().await?;
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
285
src/imap/mod.rs
285
src/imap/mod.rs
@@ -25,6 +25,7 @@ use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid};
|
||||
use crate::mimeparser;
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::stock::StockMessage;
|
||||
@@ -39,78 +40,48 @@ use session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "IMAP Connect without configured params")]
|
||||
#[error("IMAP Connect without configured params")]
|
||||
ConnectWithoutConfigure,
|
||||
|
||||
#[fail(display = "IMAP Connection Failed params: {}", _0)]
|
||||
#[error("IMAP Connection Failed params: {0}")]
|
||||
ConnectionFailed(String),
|
||||
|
||||
#[fail(display = "IMAP No Connection established")]
|
||||
#[error("IMAP No Connection established")]
|
||||
NoConnection,
|
||||
|
||||
#[fail(display = "IMAP Could not get OAUTH token")]
|
||||
#[error("IMAP Could not get OAUTH token")]
|
||||
OauthError,
|
||||
|
||||
#[fail(display = "IMAP Could not login as {}", _0)]
|
||||
#[error("IMAP Could not login as {0}")]
|
||||
LoginFailed(String),
|
||||
|
||||
#[fail(display = "IMAP Could not fetch")]
|
||||
FetchFailed(#[cause] async_imap::error::Error),
|
||||
#[error("IMAP Could not fetch")]
|
||||
FetchFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
||||
#[error("IMAP operation attempted while it is torn down")]
|
||||
InTeardown,
|
||||
|
||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
||||
SqlError(#[cause] crate::sql::Error),
|
||||
#[error("IMAP operation attempted while it is torn down")]
|
||||
SqlError(#[from] crate::sql::Error),
|
||||
|
||||
#[fail(display = "IMAP got error from elsewhere")]
|
||||
WrappedError(#[cause] crate::error::Error),
|
||||
#[error("IMAP got error from elsewhere")]
|
||||
WrappedError(#[from] crate::error::Error),
|
||||
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
SelectFolderError(#[cause] select_folder::Error),
|
||||
#[error("IMAP select folder error")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[fail(display = "Mail parse error")]
|
||||
MailParseError(#[cause] mailparse::MailParseError),
|
||||
#[error("Mail parse error")]
|
||||
MailParseError(#[from] mailparse::MailParseError),
|
||||
|
||||
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
|
||||
#[error("No mailbox selected, folder: {0}")]
|
||||
NoMailbox(String),
|
||||
|
||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
||||
#[error("IMAP other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<crate::sql::Error> for Error {
|
||||
fn from(err: crate::sql::Error) -> Error {
|
||||
Error::SqlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::error::Error> for Error {
|
||||
fn from(err: crate::error::Error) -> Error {
|
||||
Error::WrappedError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for crate::error::Error {
|
||||
fn from(err: Error) -> crate::error::Error {
|
||||
crate::error::Error::Message(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<select_folder::Error> for Error {
|
||||
fn from(err: select_folder::Error) -> Error {
|
||||
Error::SelectFolderError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mailparse::MailParseError> for Error {
|
||||
fn from(err: mailparse::MailParseError) -> Error {
|
||||
Error::MailParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImapActionResult {
|
||||
Failed,
|
||||
@@ -164,7 +135,7 @@ impl async_imap::Authenticator for OAuth2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum FolderMeaning {
|
||||
Unknown,
|
||||
SentObjects,
|
||||
@@ -188,7 +159,6 @@ struct ImapConfig {
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// https://tools.ietf.org/html/rfc6851
|
||||
pub can_move: bool,
|
||||
pub imap_delimiter: char,
|
||||
}
|
||||
|
||||
impl Default for ImapConfig {
|
||||
@@ -206,7 +176,6 @@ impl Default for ImapConfig {
|
||||
selected_folder_needs_expunge: false,
|
||||
can_idle: false,
|
||||
can_move: false,
|
||||
imap_delimiter: '.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +269,7 @@ impl Imap {
|
||||
let imap_port = config.imap_port;
|
||||
context.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("{}:{}", imap_server, imap_port),
|
||||
format!("IMAP {}:{}", imap_server, imap_port),
|
||||
err.to_string(),
|
||||
)
|
||||
};
|
||||
@@ -636,7 +605,12 @@ impl Imap {
|
||||
|
||||
let headers = get_fetch_headers(fetch)?;
|
||||
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
|
||||
if precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
|
||||
if let Ok(true) =
|
||||
precheck_imf(context, &message_id, folder.as_ref(), cur_uid).map_err(|err| {
|
||||
warn!(context, "precheck_imf error: {}", err);
|
||||
err
|
||||
})
|
||||
{
|
||||
// we know the message-id already or don't want the message otherwise.
|
||||
info!(
|
||||
context,
|
||||
@@ -645,6 +619,9 @@ impl Imap {
|
||||
folder.as_ref(),
|
||||
);
|
||||
} else {
|
||||
// we do not know the message-id
|
||||
// or the message-id is missing (in this case, we create one in the further process)
|
||||
// or some other error happened
|
||||
let show = prefetch_should_download(context, &headers, show_emails)
|
||||
.map_err(|err| {
|
||||
warn!(context, "prefetch_should_download error: {}", err);
|
||||
@@ -765,13 +742,7 @@ impl Imap {
|
||||
if let Err(err) =
|
||||
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen)
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"dc_receive_imf failed for imap-message {}/{}: {:?}",
|
||||
folder.as_ref(),
|
||||
server_uid,
|
||||
err
|
||||
);
|
||||
return Err(Error::Other(format!("dc_receive_imf error: {}", err)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -796,7 +767,6 @@ impl Imap {
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
dest_folder: &str,
|
||||
dest_uid: &mut u32,
|
||||
) -> ImapActionResult {
|
||||
task::block_on(async move {
|
||||
if folder == dest_folder {
|
||||
@@ -813,10 +783,6 @@ impl Imap {
|
||||
return imapresult;
|
||||
}
|
||||
// we are connected, and the folder is selected
|
||||
|
||||
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
|
||||
*dest_uid = 0;
|
||||
|
||||
let set = format!("{}", uid);
|
||||
let display_folder_id = format!("{}/{}", folder, uid);
|
||||
|
||||
@@ -996,10 +962,10 @@ impl Imap {
|
||||
context: &Context,
|
||||
message_id: &str,
|
||||
folder: &str,
|
||||
uid: &mut u32,
|
||||
uid: u32,
|
||||
) -> ImapActionResult {
|
||||
task::block_on(async move {
|
||||
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, *uid) {
|
||||
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) {
|
||||
return imapresult;
|
||||
}
|
||||
// we are connected, and the folder is selected
|
||||
@@ -1021,7 +987,7 @@ impl Imap {
|
||||
display_imap_id,
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::Failed;
|
||||
return ImapActionResult::AlreadyDone;
|
||||
};
|
||||
|
||||
let remote_message_id = get_fetch_headers(fetch)
|
||||
@@ -1036,7 +1002,7 @@ impl Imap {
|
||||
remote_message_id,
|
||||
message_id,
|
||||
);
|
||||
*uid = 0;
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1044,18 +1010,18 @@ impl Imap {
|
||||
context,
|
||||
"Cannot delete {} on IMAP: {}", display_imap_id, err
|
||||
);
|
||||
*uid = 0;
|
||||
return ImapActionResult::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark the message for deletion
|
||||
if !self.add_flag_finalized(context, *uid, "\\Deleted").await {
|
||||
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot mark message {} as \"Deleted\".", display_imap_id
|
||||
);
|
||||
ImapActionResult::Failed
|
||||
ImapActionResult::RetryLater
|
||||
} else {
|
||||
emit_event!(
|
||||
context,
|
||||
@@ -1074,12 +1040,14 @@ impl Imap {
|
||||
let folders_configured = context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured");
|
||||
if folders_configured.unwrap_or_default() >= 3 {
|
||||
// the "3" here we increase if we have future updates to
|
||||
// to folder configuration
|
||||
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.configure_folders(context, create_mvbox)
|
||||
}
|
||||
|
||||
pub fn configure_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !self.is_connected().await {
|
||||
return Err(Error::NoConnection);
|
||||
@@ -1095,16 +1063,26 @@ impl Imap {
|
||||
}
|
||||
};
|
||||
|
||||
let sentbox_folder =
|
||||
folders
|
||||
.iter()
|
||||
.find(|folder| match get_folder_meaning(folder) {
|
||||
FolderMeaning::SentObjects => true,
|
||||
_ => false,
|
||||
});
|
||||
let sentbox_folder = folders
|
||||
.iter()
|
||||
.find(|folder| get_folder_meaning(folder) == FolderMeaning::SentObjects)
|
||||
.or_else(|| {
|
||||
info!(context, "can't find sentbox by attributes, checking names");
|
||||
folders.iter().find(|folder| {
|
||||
get_folder_meaning_by_name(folder) == FolderMeaning::SentObjects
|
||||
})
|
||||
});
|
||||
info!(context, "sentbox folder is {:?}", sentbox_folder);
|
||||
|
||||
let delimiter = self.config.read().await.imap_delimiter;
|
||||
let mut delimiter = ".";
|
||||
if let Some(folder) = folders.first() {
|
||||
if let Some(d) = folder.delimiter() {
|
||||
if !d.is_empty() {
|
||||
delimiter = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
|
||||
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
|
||||
|
||||
let mut mvbox_folder = folders
|
||||
@@ -1168,9 +1146,11 @@ impl Imap {
|
||||
Some(sentbox_folder.name()),
|
||||
)?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "folders_configured", 3)?;
|
||||
context.sql.set_raw_config_int(
|
||||
context,
|
||||
"folders_configured",
|
||||
DC_FOLDERS_CONFIGURED_VERSION,
|
||||
)?;
|
||||
}
|
||||
info!(context, "FINISHED configuring IMAP-folders.");
|
||||
Ok(())
|
||||
@@ -1205,13 +1185,13 @@ impl Imap {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
error!(context, "could not setup imap connection: {:?}", err);
|
||||
error!(context, "could not setup imap connection: {}", err);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.select_folder(context, Some(&folder)).await {
|
||||
error!(
|
||||
context,
|
||||
"Could not select {} for expunging: {:?}", folder, err
|
||||
"Could not select {} for expunging: {}", folder, err
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1231,9 +1211,21 @@ impl Imap {
|
||||
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "expunge failed {}: {:?}", folder, err);
|
||||
error!(context, "expunge failed {}: {}", folder, err);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = crate::sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
|
||||
params![folder],
|
||||
) {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to reset server_uid and server_folder for deleted messages: {}", err
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1245,7 +1237,7 @@ impl Imap {
|
||||
// CAVE: if possible, take care not to add a name here that is "sent" in one language
|
||||
// but sth. different in others - a hard job.
|
||||
fn get_folder_meaning_by_name(folder_name: &Name) -> FolderMeaning {
|
||||
let sent_names = vec!["sent", "sent objects", "gesendet"];
|
||||
let sent_names = vec!["sent", "sentmail", "sent objects", "gesendet"];
|
||||
let lower = folder_name.name().to_lowercase();
|
||||
|
||||
if sent_names.into_iter().any(|s| s == lower) {
|
||||
@@ -1256,53 +1248,84 @@ fn get_folder_meaning_by_name(folder_name: &Name) -> FolderMeaning {
|
||||
}
|
||||
|
||||
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
|
||||
if folder_name.attributes().is_empty() {
|
||||
return FolderMeaning::Unknown;
|
||||
}
|
||||
|
||||
let mut res = FolderMeaning::Unknown;
|
||||
let special_names = vec!["\\Spam", "\\Trash", "\\Drafts", "\\Junk"];
|
||||
|
||||
for attr in folder_name.attributes() {
|
||||
if let NameAttribute::Custom(ref label) = attr {
|
||||
if special_names.iter().any(|s| *s == label) {
|
||||
res = FolderMeaning::Other;
|
||||
return FolderMeaning::Other;
|
||||
} else if label == "\\Sent" {
|
||||
res = FolderMeaning::SentObjects
|
||||
return FolderMeaning::SentObjects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match res {
|
||||
FolderMeaning::Unknown => get_folder_meaning_by_name(folder_name),
|
||||
_ => res,
|
||||
}
|
||||
FolderMeaning::Unknown
|
||||
}
|
||||
|
||||
fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server_uid: u32) -> bool {
|
||||
if let Ok((old_server_folder, old_server_uid, msg_id)) =
|
||||
message::rfc724_mid_exists(context, &rfc724_mid)
|
||||
fn precheck_imf(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
server_folder: &str,
|
||||
server_uid: u32,
|
||||
) -> Result<bool> {
|
||||
if let Some((old_server_folder, old_server_uid, msg_id)) =
|
||||
message::rfc724_mid_exists(context, &rfc724_mid)?
|
||||
{
|
||||
if old_server_folder.is_empty() && old_server_uid == 0 {
|
||||
info!(context, "[move] detected bcc-self {}", rfc724_mid,);
|
||||
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
|
||||
job_add(
|
||||
info!(
|
||||
context,
|
||||
Action::MarkseenMsgOnImap,
|
||||
msg_id.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
|
||||
);
|
||||
|
||||
let delete_server_after = context.get_config_delete_server_after();
|
||||
|
||||
if delete_server_after != Some(0) {
|
||||
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
|
||||
job_add(
|
||||
context,
|
||||
Action::MarkseenMsgOnImap,
|
||||
msg_id.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
} else if old_server_folder != server_folder {
|
||||
info!(context, "[move] detected moved message {}", rfc724_mid,);
|
||||
info!(
|
||||
context,
|
||||
"[move] detected message {} moved by other device from {}/{} to {}/{}",
|
||||
rfc724_mid,
|
||||
old_server_folder,
|
||||
old_server_uid,
|
||||
server_folder,
|
||||
server_uid
|
||||
);
|
||||
} else if old_server_uid == 0 {
|
||||
info!(
|
||||
context,
|
||||
"[move] detected message {} moved by us from {}/{} to {}/{}",
|
||||
rfc724_mid,
|
||||
old_server_folder,
|
||||
old_server_uid,
|
||||
server_folder,
|
||||
server_uid
|
||||
);
|
||||
} else if old_server_uid != server_uid {
|
||||
warn!(
|
||||
context,
|
||||
"UID for message {} in folder {} changed from {} to {}",
|
||||
rfc724_mid,
|
||||
server_folder,
|
||||
old_server_uid,
|
||||
server_uid
|
||||
);
|
||||
}
|
||||
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
update_server_uid(context, &rfc724_mid, server_folder, server_uid);
|
||||
}
|
||||
true
|
||||
Ok(true)
|
||||
} else {
|
||||
false
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1316,30 +1339,27 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
|
||||
}
|
||||
|
||||
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
|
||||
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
|
||||
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
|
||||
Ok(crate::mimeparser::parse_message_id(&message_id)?)
|
||||
} else {
|
||||
Err(Error::Other("prefetch: No message ID found".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn prefetch_is_reply_to_chat_message(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader],
|
||||
) -> Result<bool> {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
|
||||
fn prefetch_is_reply_to_chat_message(context: &Context, headers: &[mailparse::MailHeader]) -> bool {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo) {
|
||||
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::References) {
|
||||
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
false
|
||||
}
|
||||
|
||||
fn prefetch_should_download(
|
||||
@@ -1347,19 +1367,16 @@ fn prefetch_should_download(
|
||||
headers: &[mailparse::MailHeader],
|
||||
show_emails: ShowEmails,
|
||||
) -> Result<bool> {
|
||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion)?.is_some();
|
||||
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
|
||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers);
|
||||
|
||||
// Autocrypt Setup Message should be shown even if it is from non-chat client.
|
||||
let is_autocrypt_setup_message = headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)?
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some();
|
||||
|
||||
let from_field = headers
|
||||
.get_header_value(HeaderDef::From_)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
|
||||
let (_contact_id, blocked_contact, origin) =
|
||||
from_field_to_contact_id(context, &mimeparser::get_from(headers))?;
|
||||
let accepted_contact = origin.is_known();
|
||||
|
||||
let show = is_autocrypt_setup_message
|
||||
|
||||
@@ -4,25 +4,53 @@ use crate::context::Context;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "IMAP Could not obtain imap-session object.")]
|
||||
#[error("IMAP Could not obtain imap-session object.")]
|
||||
NoSession,
|
||||
|
||||
#[fail(display = "IMAP Connection Lost or no connection established")]
|
||||
#[error("IMAP Connection Lost or no connection established")]
|
||||
ConnectionLost,
|
||||
|
||||
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
|
||||
#[error("IMAP Folder name invalid: {0}")]
|
||||
BadFolderName(String),
|
||||
|
||||
#[fail(display = "IMAP close/expunge failed: {}", _0)]
|
||||
CloseExpungeFailed(#[cause] async_imap::error::Error),
|
||||
#[error("IMAP close/expunge failed")]
|
||||
CloseExpungeFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
||||
#[error("IMAP other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
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
|
||||
async fn close_folder(&self, context: &Context) -> Result<()> {
|
||||
if let Some(ref folder) = self.config.read().await.selected_folder {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.close().await {
|
||||
Ok(_) => {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
}
|
||||
let mut cfg = self.config.write().await;
|
||||
cfg.selected_folder = None;
|
||||
cfg.selected_folder_needs_expunge = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// select a folder, possibly update uid_validity and, if needed,
|
||||
/// expunge the folder to remove delete-marked messages.
|
||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||
@@ -38,39 +66,14 @@ impl Imap {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(ref folder) = folder {
|
||||
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
|
||||
if folder.as_ref() == selected_folder {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let needs_expunge = self.config.read().await.selected_folder_needs_expunge;
|
||||
if needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
let needs_expunge = { self.config.read().await.selected_folder_needs_expunge };
|
||||
if needs_expunge {
|
||||
if let Some(ref folder) = self.config.read().await.selected_folder {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
|
||||
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
|
||||
// https://tools.ietf.org/html/rfc3501#section-6.4.2
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.close().await {
|
||||
Ok(_) => {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
}
|
||||
self.config.write().await.selected_folder_needs_expunge = false;
|
||||
let folder_str: Option<&str> = folder.as_ref().map(|x| x.as_ref());
|
||||
if self.config.read().await.selected_folder.as_deref() == folder_str {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// select new folder
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::e2ee;
|
||||
use crate::error::*;
|
||||
use crate::events::Event;
|
||||
use crate::job::*;
|
||||
use crate::key::{self, Key};
|
||||
use crate::key::{self, DcKey, Key, SignedSecretKey};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::*;
|
||||
@@ -175,9 +175,7 @@ pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String>
|
||||
passphrase.len() >= 2,
|
||||
"Passphrase must be at least 2 chars long."
|
||||
);
|
||||
let self_addr = e2ee::ensure_secret_key_exists(context)?;
|
||||
let private_key = Key::from_self_private(context, self_addr, &context.sql)
|
||||
.ok_or_else(|| format_err!("Failed to get private key."))?;
|
||||
let private_key = Key::from(SignedSecretKey::load_self(context)?);
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled) {
|
||||
false => None,
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
@@ -756,7 +754,6 @@ mod tests {
|
||||
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
|
||||
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
|
||||
assert!(msg.contains("Passphrase-Begin: he\n"));
|
||||
assert!(msg.contains("==\n"));
|
||||
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
|
||||
}
|
||||
|
||||
|
||||
355
src/job.rs
355
src/job.rs
@@ -9,6 +9,9 @@ use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use async_smtp::smtp::response::Category;
|
||||
use async_smtp::smtp::response::Code;
|
||||
use async_smtp::smtp::response::Detail;
|
||||
use async_std::task;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -19,7 +22,7 @@ use crate::constants::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::context::{Context, PerformJobsNeeded};
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::error::{bail, ensure, format_err, Error, Result};
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
use crate::imex::*;
|
||||
@@ -72,7 +75,19 @@ impl Default for Thread {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Action {
|
||||
Unknown = 0,
|
||||
@@ -80,10 +95,14 @@ pub enum Action {
|
||||
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
|
||||
Housekeeping = 105, // low priority ...
|
||||
EmptyServer = 107,
|
||||
DeleteMsgOnImap = 110,
|
||||
MarkseenMdnOnImap = 120,
|
||||
OldDeleteMsgOnImap = 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,
|
||||
ConfigureImap = 900,
|
||||
ImexImap = 910, // ... high priority
|
||||
|
||||
@@ -108,9 +127,9 @@ impl From<Action> for Thread {
|
||||
Unknown => Thread::Unknown,
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
OldDeleteMsgOnImap => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
EmptyServer => Thread::Imap,
|
||||
MarkseenMdnOnImap => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
ConfigureImap => Thread::Imap,
|
||||
@@ -143,30 +162,68 @@ impl fmt::Display for Job {
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Deletes the job from the database.
|
||||
fn delete(&self, context: &Context) -> bool {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", params![self.job_id as i32])
|
||||
.is_ok()
|
||||
fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
|
||||
let timestamp = time();
|
||||
|
||||
Self {
|
||||
job_id: 0,
|
||||
action,
|
||||
foreign_id,
|
||||
desired_timestamp: timestamp + delay_seconds,
|
||||
added_timestamp: timestamp,
|
||||
tries: 0,
|
||||
param,
|
||||
pending_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the job already stored in the database.
|
||||
/// Deletes the job from the database.
|
||||
fn delete(&self, context: &Context) -> bool {
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", params![self.job_id as i32])
|
||||
.is_ok()
|
||||
} else {
|
||||
// Already deleted.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the job to the database, creating a new entry if necessary.
|
||||
///
|
||||
/// To add a new job, use [job_add].
|
||||
fn update(&self, context: &Context) -> bool {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
params![
|
||||
self.desired_timestamp,
|
||||
self.tries as i64,
|
||||
self.param.to_string(),
|
||||
self.job_id as i32,
|
||||
],
|
||||
)
|
||||
.is_ok()
|
||||
/// The Job is consumed by this method.
|
||||
fn save(self, context: &Context) -> bool {
|
||||
let thread: Thread = self.action.into();
|
||||
|
||||
if self.job_id != 0 {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
params![
|
||||
self.desired_timestamp,
|
||||
self.tries as i64,
|
||||
self.param.to_string(),
|
||||
self.job_id as i32,
|
||||
],
|
||||
)
|
||||
.is_ok()
|
||||
} else {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||
params![
|
||||
self.added_timestamp,
|
||||
thread,
|
||||
self.action,
|
||||
self.foreign_id,
|
||||
self.param.to_string(),
|
||||
self.desired_timestamp
|
||||
]
|
||||
).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn smtp_send<F>(
|
||||
@@ -196,8 +253,37 @@ impl Job {
|
||||
self.pending_error = Some(err.to_string());
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(_) => {
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||
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
|
||||
|
||||
// Code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||
Code {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
..
|
||||
} => Status::RetryLater,
|
||||
|
||||
_ => {
|
||||
// If we do not retry, add an info message to the chat
|
||||
// Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM.
|
||||
match Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
Ok(message) => chat::add_info_msg(
|
||||
context,
|
||||
message.chat_id,
|
||||
err.to_string(),
|
||||
),
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"couldn't load chat_id to inform user about SMTP error: {}",
|
||||
e
|
||||
),
|
||||
};
|
||||
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
async_smtp::smtp::error::Error::Transient(_) => {
|
||||
// We got a transient 4xx response from SMTP server.
|
||||
@@ -223,7 +309,7 @@ impl Job {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect();
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
Status::Finished(Err(Error::SmtpError(err)))
|
||||
Status::Finished(Err(err.into()))
|
||||
}
|
||||
Err(crate::smtp::send::Error::NoTransport) => {
|
||||
// Should never happen.
|
||||
@@ -413,18 +499,12 @@ impl Job {
|
||||
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let mut dest_uid = 0;
|
||||
|
||||
match imap_inbox.mv(
|
||||
context,
|
||||
server_folder,
|
||||
msg.server_uid,
|
||||
&dest_folder,
|
||||
&mut dest_uid,
|
||||
) {
|
||||
match imap_inbox.mv(context, server_folder, msg.server_uid, &dest_folder) {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::Success => {
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, dest_uid);
|
||||
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
ImapActionResult::Failed => {
|
||||
@@ -437,14 +517,29 @@ impl Job {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[allow(non_snake_case)]
|
||||
fn DeleteMsgOnImap(&mut self, context: &Context) -> Status {
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
|
||||
let mut msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
|
||||
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
if message::rfc724_mid_cnt(context, &msg.rfc724_mid) > 1 {
|
||||
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid);
|
||||
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.",
|
||||
@@ -454,13 +549,47 @@ impl Job {
|
||||
we delete the message from the server */
|
||||
let mid = msg.rfc724_mid;
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let res = imap_inbox.delete_msg(context, &mid, server_folder, &mut msg.server_uid);
|
||||
if res == ImapActionResult::RetryLater {
|
||||
// XXX RetryLater is converted to RetryNow here
|
||||
return Status::RetryNow;
|
||||
let res = if msg.server_uid == 0 {
|
||||
// Message is already deleted on IMAP server.
|
||||
ImapActionResult::AlreadyDone
|
||||
} else {
|
||||
imap_inbox.delete_msg(context, &mid, server_folder, msg.server_uid)
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::delete_from_db(context, msg.id);
|
||||
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))
|
||||
} 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));
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
} else {
|
||||
/* eg. device messages have no Message-ID */
|
||||
@@ -512,43 +641,6 @@ impl Job {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn MarkseenMdnOnImap(&mut self, context: &Context) -> Status {
|
||||
let folder = self
|
||||
.param
|
||||
.get(Param::ServerFolder)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uid = self.param.get_int(Param::ServerUid).unwrap_or_default() as u32;
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
if imap_inbox.set_seen(context, &folder, uid) == ImapActionResult::RetryLater {
|
||||
return Status::RetryLater;
|
||||
}
|
||||
if self.param.get_bool(Param::AlsoMove).unwrap_or_default() {
|
||||
if let Err(err) = imap_inbox.ensure_configured_folders(context, true) {
|
||||
warn!(context, "configuring folders failed: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
let dest_folder = context
|
||||
.sql
|
||||
.get_raw_config(context, "configured_mvbox_folder");
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
let mut dest_uid = 0;
|
||||
if ImapActionResult::RetryLater
|
||||
== imap_inbox.mv(context, &folder, uid, &dest_folder, &mut dest_uid)
|
||||
{
|
||||
Status::RetryLater
|
||||
} else {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Err(format_err!("MVBOX is not configured")))
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* delete all pending jobs with the given action */
|
||||
@@ -821,7 +913,11 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately.
|
||||
if context.get_config_bool(Config::BccSelf)
|
||||
&& context.get_config_delete_server_after() != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
@@ -897,6 +993,36 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after() {
|
||||
let threshold_timestamp = time() - delete_server_after;
|
||||
|
||||
context.sql.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE timestamp < ? \
|
||||
AND server_uid != 0",
|
||||
params![threshold_timestamp],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
||||
let res = if let Some(msg_id) = load_imap_deletion_msgid(context)? {
|
||||
Some(Job::new(
|
||||
Action::DeleteMsgOnImap,
|
||||
msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn perform_inbox_jobs(context: &Context) {
|
||||
info!(context, "dc_perform_inbox_jobs starting.",);
|
||||
|
||||
@@ -917,7 +1043,17 @@ pub fn perform_sentbox_jobs(context: &Context) {
|
||||
}
|
||||
|
||||
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
let mut jobs_loaded = 0;
|
||||
|
||||
while let Some(mut job) = load_next_job(context, thread, probe_network) {
|
||||
jobs_loaded += 1;
|
||||
if thread == Thread::Imap && jobs_loaded > 20 {
|
||||
// Let the fetch run, but return back to the job afterwards.
|
||||
info!(context, "postponing {}-job {} to run fetch...", thread, job);
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = true;
|
||||
break;
|
||||
}
|
||||
|
||||
info!(context, "{}-job {} started...", thread, job);
|
||||
|
||||
// some configuration jobs are "exclusive":
|
||||
@@ -975,7 +1111,6 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
job.tries = tries;
|
||||
let time_offset = get_backoff_time_offset(tries);
|
||||
job.desired_timestamp = time() + time_offset;
|
||||
job.update(context);
|
||||
info!(
|
||||
context,
|
||||
"{}-job #{} not succeeded on try #{}, retry in {} seconds.",
|
||||
@@ -984,6 +1119,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
tries,
|
||||
time_offset
|
||||
);
|
||||
job.save(context);
|
||||
if thread == Thread::Smtp && tries < JOB_RETRIES - 1 {
|
||||
context
|
||||
.smtp_state
|
||||
@@ -1045,16 +1181,16 @@ fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, trie
|
||||
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
|
||||
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
|
||||
Action::EmptyServer => job.EmptyServer(context),
|
||||
Action::OldDeleteMsgOnImap => job.DeleteMsgOnImap(context),
|
||||
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
|
||||
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
|
||||
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
|
||||
Action::MoveMsg => job.MoveMsg(context),
|
||||
Action::SendMdn => job.SendMdn(context),
|
||||
Action::ConfigureImap => JobConfigureImap(context),
|
||||
Action::ImexImap => match JobImexImap(context, &job) {
|
||||
Ok(()) => Status::Finished(Ok(())),
|
||||
Err(err) => {
|
||||
error!(context, "{}", err);
|
||||
error!(context, "Import/export failed: {}", err);
|
||||
Status::Finished(Err(err))
|
||||
}
|
||||
},
|
||||
@@ -1141,27 +1277,16 @@ pub fn job_add(
|
||||
return;
|
||||
}
|
||||
|
||||
let timestamp = time();
|
||||
let thread: Thread = action.into();
|
||||
let job = Job::new(action, foreign_id as u32, param, delay_seconds);
|
||||
job.save(context);
|
||||
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||
params![
|
||||
timestamp,
|
||||
thread,
|
||||
action,
|
||||
foreign_id,
|
||||
param.to_string(),
|
||||
(timestamp + delay_seconds as i64)
|
||||
]
|
||||
).ok();
|
||||
|
||||
match thread {
|
||||
Thread::Imap => interrupt_inbox_idle(context),
|
||||
Thread::Smtp => interrupt_smtp_idle(context),
|
||||
Thread::Unknown => {}
|
||||
if delay_seconds == 0 {
|
||||
let thread: Thread = action.into();
|
||||
match thread {
|
||||
Thread::Imap => interrupt_inbox_idle(context),
|
||||
Thread::Smtp => interrupt_smtp_idle(context),
|
||||
Thread::Unknown => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1205,7 +1330,7 @@ fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Opti
|
||||
params_probe
|
||||
};
|
||||
|
||||
context
|
||||
let job = context
|
||||
.sql
|
||||
.query_map(
|
||||
query,
|
||||
@@ -1234,7 +1359,23 @@ fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Opti
|
||||
Ok(None)
|
||||
},
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
if thread == Thread::Imap {
|
||||
if let Some(job) = job {
|
||||
if job.action < Action::DeleteMsgOnImap {
|
||||
load_imap_deletion_job(context)
|
||||
.unwrap_or_default()
|
||||
.or(Some(job))
|
||||
} else {
|
||||
Some(job)
|
||||
}
|
||||
} else {
|
||||
load_imap_deletion_job(context).unwrap_or_default()
|
||||
}
|
||||
} else {
|
||||
job
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::error::{format_err, Result};
|
||||
use crate::imap::Imap;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -99,25 +99,21 @@ impl JobThread {
|
||||
|
||||
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
|
||||
let prefix = format!("{}-fetch", self.name);
|
||||
match self.imap.connect_configured(context) {
|
||||
Ok(()) => {
|
||||
if let Some(watch_folder) = self.get_watch_folder(context) {
|
||||
let start = std::time::Instant::now();
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self
|
||||
.imap
|
||||
.fetch(context, &watch_folder)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
|
||||
self.imap.connect_configured(context)?;
|
||||
if let Some(watch_folder) = self.get_watch_folder(context) {
|
||||
let start = std::time::Instant::now();
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self
|
||||
.imap
|
||||
.fetch(context, &watch_folder)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
|
||||
|
||||
res
|
||||
} else {
|
||||
Err(Error::WatchFolderNotFound("not-set".to_string()))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(crate::error::Error::Message(err.to_string())),
|
||||
res
|
||||
} else {
|
||||
Err(format_err!("WatchFolder not found: not-set"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
216
src/key.rs
216
src/key.rs
@@ -4,38 +4,39 @@ use std::collections::BTreeMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::sql::Sql;
|
||||
use crate::dc_tools::{dc_write_file, time, EmailAddress, InvalidEmailError};
|
||||
use crate::sql;
|
||||
|
||||
// Re-export key types
|
||||
pub use crate::pgp::KeyPair;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
|
||||
/// Error type for deltachat key handling.
|
||||
#[derive(Fail, Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[fail(display = "Could not decode base64")]
|
||||
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
|
||||
#[fail(display = "rPGP error: {}", _0)]
|
||||
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
fn from(err: base64::DecodeError) -> Error {
|
||||
Error::Base64Decode(err, failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::errors::Error> for Error {
|
||||
fn from(err: pgp::errors::Error) -> Error {
|
||||
Error::PgpError(err, failure::Backtrace::new())
|
||||
}
|
||||
#[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 load key: {}", _0)]
|
||||
LoadKey(#[from] sql::Error),
|
||||
#[error("Failed to save generated key: {}", _0)]
|
||||
StoreKey(#[from] SaveKeyError),
|
||||
#[error("No address configured")]
|
||||
NoConfiguredAddr,
|
||||
#[error("Configured address is invalid: {}", _0)]
|
||||
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -63,6 +64,9 @@ pub trait DcKey: Serialize + Deserializable {
|
||||
Self::from_slice(&bytes)
|
||||
}
|
||||
|
||||
/// Load the users' default key from the database.
|
||||
fn load_self(context: &Context) -> Result<Self::KeyType>;
|
||||
|
||||
/// Serialise the key to a base64 string.
|
||||
fn to_base64(&self) -> String {
|
||||
// Not using Serialize::to_bytes() to make clear *why* it is
|
||||
@@ -77,10 +81,91 @@ pub trait DcKey: Serialize + Deserializable {
|
||||
|
||||
impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
|
||||
fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context.sql.query_row(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
params![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
) {
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let keypair = generate_keypair(context)?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DcKey for SignedSecretKey {
|
||||
type KeyType = SignedSecretKey;
|
||||
|
||||
fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context.sql.query_row(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
params![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
) {
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let keypair = generate_keypair(context)?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.ok_or_else(|| Error::NoConfiguredAddr)?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().unwrap();
|
||||
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
match context.sql.query_row(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
params![addr],
|
||||
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
) {
|
||||
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let start = std::time::Instant::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType))
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
let keypair = crate::pgp::create_keypair(addr, keytype)?;
|
||||
store_self_keypair(context, &keypair, KeyPairUse::Default)?;
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(keypair)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cryptographic key
|
||||
@@ -197,34 +282,6 @@ impl Key {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_self_public(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
sql: &Sql,
|
||||
) -> Option<Self> {
|
||||
let addr = self_addr.as_ref();
|
||||
|
||||
sql.query_get_value(
|
||||
context,
|
||||
"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;",
|
||||
&[addr],
|
||||
)
|
||||
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Public))
|
||||
}
|
||||
|
||||
pub fn from_self_private(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
sql: &Sql,
|
||||
) -> Option<Self> {
|
||||
sql.query_get_value(
|
||||
context,
|
||||
"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;",
|
||||
&[self_addr.as_ref()],
|
||||
)
|
||||
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Private))
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Key::Public(k) => k.to_bytes().unwrap_or_default(),
|
||||
@@ -316,21 +373,19 @@ pub enum KeyPairUse {
|
||||
}
|
||||
|
||||
/// Error saving a keypair to the database.
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display = "SaveKeyError: {}", message)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("SaveKeyError: {message}")]
|
||||
pub struct SaveKeyError {
|
||||
message: String,
|
||||
#[cause]
|
||||
cause: failure::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
}
|
||||
|
||||
impl SaveKeyError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
||||
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,6 +608,59 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = dummy_context();
|
||||
configure_alice_keypair(&t.ctx);
|
||||
let pubkey = SignedPublicKey::load_self(&t.ctx).unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t.ctx).unwrap();
|
||||
assert_eq!(alice.secret, seckey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // generating keys is expensive
|
||||
fn test_load_self_generate_public() {
|
||||
let t = dummy_context();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.unwrap();
|
||||
let key = SignedPublicKey::load_self(&t.ctx);
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // generating keys is expensive
|
||||
fn test_load_self_generate_secret() {
|
||||
let t = dummy_context();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.unwrap();
|
||||
let key = SignedSecretKey::load_self(&t.ctx);
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // generating keys is expensive
|
||||
fn test_load_self_generate_concurrent() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let t = dummy_context();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.unwrap();
|
||||
let ctx = Arc::new(t.ctx);
|
||||
let ctx0 = Arc::clone(&ctx);
|
||||
let thr0 = thread::spawn(move || SignedPublicKey::load_self(&ctx0));
|
||||
let ctx1 = Arc::clone(&ctx);
|
||||
let thr1 = thread::spawn(move || SignedPublicKey::load_self(&ctx1));
|
||||
let res0 = thr0.join().unwrap();
|
||||
let res1 = thr1.join().unwrap();
|
||||
assert_eq!(res0.unwrap(), res1.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ascii_roundtrip() {
|
||||
let public_key = Key::from(KEYPAIR.public.clone());
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||
#![allow(clippy::match_bool)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
#[macro_use]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Location handling
|
||||
|
||||
use bitflags::bitflags;
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
@@ -9,7 +8,7 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Error;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::job::{self, job_action_exists, job_add, Job};
|
||||
use crate::message::{Message, MsgId};
|
||||
@@ -123,9 +122,9 @@ impl Kml {
|
||||
}
|
||||
} else if self.tag.contains(KmlTag::COORDINATES) {
|
||||
let parts = val.splitn(2, ',').collect::<Vec<_>>();
|
||||
if parts.len() == 2 {
|
||||
self.curr.longitude = parts[0].parse().unwrap_or_default();
|
||||
self.curr.latitude = parts[1].parse().unwrap_or_default();
|
||||
if let [longitude, latitude] = &parts[..] {
|
||||
self.curr.longitude = longitude.parse().unwrap_or_default();
|
||||
self.curr.latitude = latitude.parse().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ impl Lot {
|
||||
}
|
||||
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
self.text1.as_ref().map(|s| s.as_str())
|
||||
self.text1.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<&str> {
|
||||
self.text2.as_ref().map(|s| s.as_str())
|
||||
self.text2.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_text1_meaning(&self) -> Meaning {
|
||||
|
||||
173
src/message.rs
173
src/message.rs
@@ -3,7 +3,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use failure::Fail;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
@@ -11,7 +11,7 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Error;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::job::*;
|
||||
use crate::lot::{Lot, LotState, Meaning};
|
||||
@@ -21,6 +21,10 @@ use crate::pgp::*;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
lazy_static! {
|
||||
static ref UNWRAP_RE: regex::Regex = regex::Regex::new(r"\s+").unwrap();
|
||||
}
|
||||
|
||||
// In practice, the user additionally cuts the string themselves
|
||||
// pixel-accurate.
|
||||
const SUMMARY_CHARACTERS: usize = 160;
|
||||
@@ -80,6 +84,55 @@ impl MsgId {
|
||||
self.0 == DC_MSG_ID_DAYMARKER
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
///
|
||||
/// It means the message is deleted locally, but not on the server
|
||||
/// yet.
|
||||
pub fn trash(self, context: &Context) -> crate::sql::Result<()> {
|
||||
let chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
|
||||
params![chat_id, self],
|
||||
)
|
||||
}
|
||||
|
||||
/// Deletes a message and corresponding MDNs from the database.
|
||||
pub fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> {
|
||||
// We don't use transactions yet, so remove MDNs first to make
|
||||
// sure they are not left while the message is deleted.
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs_mdns WHERE msg_id=?;",
|
||||
params![self],
|
||||
)?;
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs WHERE id=?;",
|
||||
params![self],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes IMAP server UID and folder from the database record.
|
||||
///
|
||||
/// It is used to avoid trying to remove the message from the
|
||||
/// server multiple times when there are multiple message records
|
||||
/// pointing to the same server UID.
|
||||
pub(crate) fn unlink(self, context: &Context) -> sql::Result<()> {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs \
|
||||
SET server_folder='', server_uid=0 \
|
||||
WHERE id=?",
|
||||
params![self],
|
||||
)
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -116,7 +169,7 @@ impl rusqlite::types::ToSql for MsgId {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
||||
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
|
||||
InvalidMsgId.compat(),
|
||||
InvalidMsgId,
|
||||
)));
|
||||
}
|
||||
let val = rusqlite::types::Value::Integer(self.0 as i64);
|
||||
@@ -144,8 +197,8 @@ impl rusqlite::types::FromSql for MsgId {
|
||||
/// This usually occurs when trying to use a message ID of
|
||||
/// [DC_MSG_ID_LAST_SPECIAL] or below in a situation where this is not
|
||||
/// possible.
|
||||
#[derive(Debug, Fail)]
|
||||
#[fail(display = "Invalid Message ID.")]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid Message ID.")]
|
||||
pub struct InvalidMsgId;
|
||||
|
||||
#[derive(
|
||||
@@ -300,25 +353,6 @@ impl Message {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn delete_from_db(context: &Context, msg_id: MsgId) {
|
||||
if let Ok(msg) = Message::load_from_db(context, msg_id) {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs WHERE id=?;",
|
||||
params![msg.id],
|
||||
)
|
||||
.ok();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs_mdns WHERE msg_id=?;",
|
||||
params![msg.id],
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_filemime(&self) -> Option<String> {
|
||||
if let Some(m) = self.param.get(Param::MimeType) {
|
||||
return Some(m.to_string());
|
||||
@@ -438,7 +472,7 @@ impl Message {
|
||||
pub fn get_text(&self) -> Option<String> {
|
||||
self.text
|
||||
.as_ref()
|
||||
.map(|text| dc_truncate(text, 30000, false).to_string())
|
||||
.map(|text| dc_truncate(text, 30000).to_string())
|
||||
}
|
||||
|
||||
pub fn get_filename(&self) -> Option<String> {
|
||||
@@ -809,7 +843,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
return ret;
|
||||
}
|
||||
let rawtxt = rawtxt.unwrap_or_default();
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
|
||||
|
||||
let fts = dc_timestamp_to_str(msg.get_timestamp());
|
||||
ret += &format!("Sent: {}", fts);
|
||||
@@ -946,13 +980,15 @@ pub fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
for msg_id in msg_ids.iter() {
|
||||
for msg_id in msg_ids {
|
||||
if let Ok(msg) = Message::load_from_db(context, *msg_id) {
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id);
|
||||
}
|
||||
}
|
||||
update_msg_chat_id(context, *msg_id, ChatId::new(DC_CHAT_ID_TRASH));
|
||||
if let Err(err) = msg_id.trash(context) {
|
||||
error!(context, "Unable to trash message {}: {}", msg_id, err);
|
||||
}
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
@@ -972,16 +1008,6 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
};
|
||||
}
|
||||
|
||||
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: ChatId) -> bool {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET chat_id=? WHERE id=?;",
|
||||
params![chat_id, msg_id],
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn delete_poi_location(context: &Context, location_id: u32) -> bool {
|
||||
sql::execute(
|
||||
context,
|
||||
@@ -1141,18 +1167,20 @@ pub fn get_summarytext_by_raw(
|
||||
return prefix;
|
||||
}
|
||||
|
||||
if let Some(text) = text {
|
||||
let summary = if let Some(text) = text {
|
||||
if text.as_ref().is_empty() {
|
||||
prefix
|
||||
} else if prefix.is_empty() {
|
||||
dc_truncate(text.as_ref(), approx_characters, true).to_string()
|
||||
dc_truncate(text.as_ref(), approx_characters).to_string()
|
||||
} else {
|
||||
let tmp = format!("{} – {}", prefix, text.as_ref());
|
||||
dc_truncate(&tmp, approx_characters, true).to_string()
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
}
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
};
|
||||
|
||||
UNWRAP_RE.replace_all(&summary, " ").to_string()
|
||||
}
|
||||
|
||||
// as we do not cut inside words, this results in about 32-42 characters.
|
||||
@@ -1187,7 +1215,7 @@ pub fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl AsRef
|
||||
}
|
||||
if let Some(error) = error {
|
||||
msg.param.set(Param::Error, error.as_ref());
|
||||
error!(context, "{}", error.as_ref());
|
||||
warn!(context, "Message failed: {}", error.as_ref());
|
||||
}
|
||||
|
||||
if sql::execute(
|
||||
@@ -1340,10 +1368,55 @@ pub fn get_deaddrop_msg_cnt(context: &Context) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_deletion_cnt(
|
||||
context: &Context,
|
||||
from_server: bool,
|
||||
seconds: i64,
|
||||
) -> Result<usize, Error> {
|
||||
let self_chat_id = chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let threshold_timestamp = time() - seconds;
|
||||
|
||||
let cnt: isize = if from_server {
|
||||
context.sql.query_row(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs m
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND server_uid != 0;",
|
||||
params![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|
||||
|row| row.get(0),
|
||||
)?
|
||||
} else {
|
||||
context.sql.query_row(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs m
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ? AND hidden = 0;",
|
||||
params![
|
||||
DC_MSG_ID_LAST_SPECIAL,
|
||||
threshold_timestamp,
|
||||
self_chat_id,
|
||||
ChatId::new(DC_CHAT_ID_TRASH)
|
||||
],
|
||||
|row| row.get(0),
|
||||
)?
|
||||
};
|
||||
Ok(cnt as usize)
|
||||
}
|
||||
|
||||
/// Counts number of database records pointing to specified
|
||||
/// Message-ID.
|
||||
///
|
||||
/// Unlinked messages are excluded.
|
||||
pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
|
||||
// check the number of messages with the same rfc724_mid
|
||||
match context.sql.query_row(
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;",
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
|
||||
&[rfc724_mid],
|
||||
|row| row.get(0),
|
||||
) {
|
||||
@@ -1358,12 +1431,15 @@ pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
|
||||
pub(crate) fn rfc724_mid_exists(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<(String, u32, MsgId), Error> {
|
||||
ensure!(!rfc724_mid.is_empty(), "empty rfc724_mid");
|
||||
) -> Result<Option<(String, u32, MsgId)>, Error> {
|
||||
if rfc724_mid.is_empty() {
|
||||
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_row_optional(
|
||||
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
|
||||
&[rfc724_mid],
|
||||
|row| {
|
||||
@@ -1384,7 +1460,8 @@ pub fn update_server_uid(
|
||||
server_uid: u32,
|
||||
) {
|
||||
match context.sql.execute(
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;",
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? \
|
||||
WHERE rfc724_mid=?",
|
||||
params![server_folder.as_ref(), server_uid, rfc724_mid],
|
||||
) {
|
||||
Ok(_) => {}
|
||||
|
||||
@@ -9,12 +9,13 @@ use crate::contact::*;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::location;
|
||||
use crate::message::{self, Message};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
// attachments of 25 mb brutto should work on the majority of providers
|
||||
@@ -106,21 +107,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
let command = msg.param.get_cmd();
|
||||
|
||||
/* for added members, the list is just fine */
|
||||
if command == SystemMessage::MemberRemovedFromGroup {
|
||||
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !email_to_remove.is_empty()
|
||||
&& !addr_cmp(email_to_remove, self_addr)
|
||||
&& !recipients_contain_addr(&recipients, &email_to_remove)
|
||||
{
|
||||
recipients.push(("".to_string(), email_to_remove.to_string()));
|
||||
}
|
||||
}
|
||||
if command != SystemMessage::AutocryptSetupMessage
|
||||
&& command != SystemMessage::SecurejoinMessage
|
||||
&& context.get_config_bool(Config::MdnsEnabled)
|
||||
@@ -368,7 +354,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
let mut to = Vec::with_capacity(self.recipients.len());
|
||||
let mut to = Vec::new();
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
@@ -380,6 +366,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
|
||||
if !self.references.is_empty() {
|
||||
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
|
||||
}
|
||||
@@ -818,7 +808,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let message_text = format!(
|
||||
"{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
&final_text,
|
||||
escape_message_footer_marks(final_text),
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
@@ -1106,7 +1096,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
||||
fn render_rfc724_mid(rfc724_mid: &str) -> String {
|
||||
let rfc724_mid = rfc724_mid.trim().to_string();
|
||||
|
||||
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
|
||||
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
|
||||
rfc724_mid
|
||||
} else {
|
||||
format!("<{}>", rfc724_mid)
|
||||
|
||||
@@ -2,22 +2,19 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{DispositionType, MailAddr, MailHeaderMap};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::bail;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::e2ee;
|
||||
use crate::error::Result;
|
||||
use crate::error::{bail, Result};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::location;
|
||||
use crate::message;
|
||||
use crate::param::*;
|
||||
@@ -39,6 +36,11 @@ use crate::stock::StockMessage;
|
||||
pub struct MimeMessage {
|
||||
pub parts: Vec<Part>,
|
||||
header: HashMap<String, String>,
|
||||
|
||||
/// Addresses are normalized and lowercased:
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
pub signatures: HashSet<String>,
|
||||
pub gossipped_addr: HashSet<String>,
|
||||
@@ -85,14 +87,27 @@ impl MimeMessage {
|
||||
|
||||
let message_time = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Date)?
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut recipients = Default::default();
|
||||
let mut from = Default::default();
|
||||
let mut chat_disposition_notification_to = None;
|
||||
|
||||
// init known headers with what mailparse provided us
|
||||
MimeMessage::merge_headers(&mut headers, &mail.headers);
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut from,
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail.headers,
|
||||
);
|
||||
|
||||
// remove headers that are allowed _only_ in the encrypted part
|
||||
headers.remove("secure-join-fingerprint");
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mail_raw;
|
||||
@@ -111,14 +126,20 @@ impl MimeMessage {
|
||||
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip")?;
|
||||
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossipped_addr =
|
||||
update_gossip_peerstates(context, message_time, &mail, gossip_headers)?;
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
// part override the unencrypted top-level
|
||||
MimeMessage::merge_headers(&mut headers, &decrypted_mail.headers);
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut from,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
|
||||
(decrypted_mail, signatures)
|
||||
} else {
|
||||
@@ -142,6 +163,9 @@ impl MimeMessage {
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
header: headers,
|
||||
recipients,
|
||||
from,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: false,
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
@@ -203,10 +227,8 @@ impl MimeMessage {
|
||||
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
||||
/// containing an explanation. If such a message is detected, first part can be safely dropped.
|
||||
fn squash_attachment_parts(&mut self) {
|
||||
if self.has_chat_version() && self.parts.len() == 2 {
|
||||
if let [textpart, filepart] = &self.parts[..] {
|
||||
let need_drop = {
|
||||
let textpart = &self.parts[0];
|
||||
let filepart = &self.parts[1];
|
||||
textpart.typ == Viewtype::Text
|
||||
&& (filepart.typ == Viewtype::Image
|
||||
|| filepart.typ == Viewtype::Gif
|
||||
@@ -312,11 +334,9 @@ impl MimeMessage {
|
||||
|
||||
// See if an MDN is requested from the other side
|
||||
if !self.decrypting_failed && !self.parts.is_empty() {
|
||||
if let Some(ref dn_to_addr) =
|
||||
self.parse_first_addr(context, HeaderDef::ChatDispositionNotificationTo)
|
||||
{
|
||||
if let Some(ref from_addr) = self.parse_first_addr(context, HeaderDef::From_) {
|
||||
if compare_addrs(from_addr, dn_to_addr) {
|
||||
if let Some(ref dn_to) = self.chat_disposition_notification_to {
|
||||
if let Some(ref from) = self.from.get(0) {
|
||||
if from.addr == dn_to.addr {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
@@ -390,20 +410,6 @@ impl MimeMessage {
|
||||
self.header.get(headerdef.get_headername())
|
||||
}
|
||||
|
||||
fn parse_first_addr(&self, context: &Context, headerdef: HeaderDef) -> Option<MailAddr> {
|
||||
if let Some(value) = self.get(headerdef.clone()) {
|
||||
match mailparse::addrparse(&value) {
|
||||
Ok(ref addrs) => {
|
||||
return addrs.first().cloned();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "header {} parse error: {:?}", headerdef, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_mime_recursive(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -474,7 +480,9 @@ impl MimeMessage {
|
||||
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
|
||||
(mime::MULTIPART, "alternative") => {
|
||||
for cur_data in &mail.subparts {
|
||||
if get_mime_type(cur_data)?.0 == "multipart/mixed" {
|
||||
if get_mime_type(cur_data)?.0 == "multipart/mixed"
|
||||
|| get_mime_type(cur_data)?.0 == "multipart/related"
|
||||
{
|
||||
any_part_added = self.parse_mime_recursive(context, cur_data)?;
|
||||
break;
|
||||
}
|
||||
@@ -498,15 +506,6 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
(mime::MULTIPART, "related") => {
|
||||
/* add the "root part" - the other parts may be referenced which is
|
||||
not interesting for us (eg. embedded images) we assume he "root part"
|
||||
being the first one, which may not be always true ...
|
||||
however, most times it seems okay. */
|
||||
if let Some(first) = mail.subparts.iter().next() {
|
||||
any_part_added = self.parse_mime_recursive(context, first)?;
|
||||
}
|
||||
}
|
||||
(mime::MULTIPART, "encrypted") => {
|
||||
// we currently do not try to decrypt non-autocrypt messages
|
||||
// at all. If we see an encrypted part, we set
|
||||
@@ -545,6 +544,16 @@ impl MimeMessage {
|
||||
if let Some(report) = self.process_report(context, mail)? {
|
||||
self.reports.push(report);
|
||||
}
|
||||
|
||||
// Add MDN part so we can track it, avoid
|
||||
// downloading the message again and
|
||||
// delete if automatic message deletion is
|
||||
// enabled.
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Unknown;
|
||||
self.parts.push(part);
|
||||
|
||||
any_part_added = true;
|
||||
} else {
|
||||
/* eg. `report-type=delivery-status`;
|
||||
maybe we should show them as a little error icon */
|
||||
@@ -744,20 +753,41 @@ impl MimeMessage {
|
||||
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||
}
|
||||
|
||||
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
|
||||
fn merge_headers(
|
||||
context: &Context,
|
||||
headers: &mut HashMap<String, String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
from: &mut Vec<SingleInfo>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
) {
|
||||
for field in fields {
|
||||
if let Ok(key) = field.get_key() {
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = key.to_lowercase();
|
||||
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = field.get_key().to_lowercase();
|
||||
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
||||
is_known(&key) || key.starts_with("chat-")
|
||||
{
|
||||
if let Ok(value) = field.get_value() {
|
||||
headers.insert(key, value);
|
||||
{
|
||||
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
|
||||
match addrparse_header(field) {
|
||||
Ok(addrlist) => {
|
||||
*chat_disposition_notification_to = addrlist.extract_single_info();
|
||||
}
|
||||
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
|
||||
}
|
||||
} else {
|
||||
let value = field.get_value();
|
||||
headers.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
let recipients_new = get_recipients(fields);
|
||||
if !recipients_new.is_empty() {
|
||||
*recipients = recipients_new;
|
||||
}
|
||||
let from_new = get_from(fields);
|
||||
if !from_new.is_empty() {
|
||||
*from = from_new;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_report(
|
||||
@@ -770,21 +800,13 @@ impl MimeMessage {
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
if let Some(_disposition) = report_fields
|
||||
.get_header_value(HeaderDef::Disposition)
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
if let Some(original_message_id) = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
@@ -800,26 +822,18 @@ impl MimeMessage {
|
||||
warn!(
|
||||
context,
|
||||
"ignoring unknown disposition-notification, Message-Id: {:?}",
|
||||
report_fields.get_header_value(HeaderDef::MessageId).ok()
|
||||
report_fields.get_header_value(HeaderDef::MessageId)
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Handle reports (only MDNs for now)
|
||||
pub fn handle_reports(
|
||||
&self,
|
||||
context: &Context,
|
||||
from_id: u32,
|
||||
sent_timestamp: i64,
|
||||
server_folder: impl AsRef<str>,
|
||||
server_uid: u32,
|
||||
) {
|
||||
pub fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) {
|
||||
if self.reports.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut mdn_recognized = false;
|
||||
for report in &self.reports {
|
||||
for original_message_id in
|
||||
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
|
||||
@@ -828,20 +842,9 @@ impl MimeMessage {
|
||||
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
|
||||
{
|
||||
context.call_cb(Event::MsgRead { chat_id, msg_id });
|
||||
mdn_recognized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.has_chat_version() || mdn_recognized {
|
||||
let mut param = Params::new();
|
||||
param.set(Param::ServerFolder, server_folder.as_ref());
|
||||
param.set_int(Param::ServerUid, server_uid as i32);
|
||||
if self.has_chat_version() && context.get_config_bool(Config::MvboxMove) {
|
||||
param.set_int(Param::AlsoMove, 1);
|
||||
}
|
||||
job_add(context, Action::MarkseenMdnOnImap, 0, param, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,28 +855,15 @@ fn update_gossip_peerstates(
|
||||
gossip_headers: Vec<String>,
|
||||
) -> Result<HashSet<String>> {
|
||||
// XXX split the parsing from the modification part
|
||||
let mut recipients: Option<HashSet<String>> = None;
|
||||
let mut gossipped_addr: HashSet<String> = Default::default();
|
||||
|
||||
for value in &gossip_headers {
|
||||
let gossip_header = value.parse::<Aheader>();
|
||||
|
||||
if let Ok(ref header) = gossip_header {
|
||||
if recipients.is_none() {
|
||||
recipients = Some(get_recipients(mail.headers.iter().filter_map(|v| {
|
||||
let key = v.get_key();
|
||||
let value = v.get_value();
|
||||
if key.is_err() || value.is_err() {
|
||||
return None;
|
||||
}
|
||||
Some((v.get_key().unwrap(), v.get_value().unwrap()))
|
||||
})));
|
||||
}
|
||||
|
||||
if recipients
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains(&header.addr.to_lowercase())
|
||||
if get_recipients(&mail.headers)
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
{
|
||||
let mut peerstate = Peerstate::from_addr(context, &context.sql, &header.addr);
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
@@ -911,19 +901,29 @@ pub(crate) struct Report {
|
||||
additional_message_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_message_id(value: &str) -> crate::error::Result<String> {
|
||||
let ids = mailparse::msgidparse(value)
|
||||
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
|
||||
|
||||
if ids.len() == 1 {
|
||||
let id = &ids[0];
|
||||
if id.starts_with('<') && id.ends_with('>') {
|
||||
Ok(id.chars().skip(1).take(id.len() - 2).collect())
|
||||
} else {
|
||||
bail!("message-ID {} is not enclosed in < and >", value);
|
||||
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
|
||||
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
|
||||
let mut msgids = Vec::new();
|
||||
for id in ids.split_whitespace() {
|
||||
let mut id = id.to_string();
|
||||
if id.starts_with('<') {
|
||||
id = id[1..].to_string();
|
||||
}
|
||||
if id.ends_with('>') {
|
||||
id = id[..id.len() - 1].to_string();
|
||||
}
|
||||
if !id.is_empty() {
|
||||
msgids.push(id);
|
||||
}
|
||||
}
|
||||
Ok(msgids)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
if let Some(id) = parse_message_ids(ids)?.first() {
|
||||
Ok(id.to_string())
|
||||
} else {
|
||||
bail!("could not parse message_id: {}", value);
|
||||
bail!("could not parse message_id: {}", ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,15 +987,12 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
}
|
||||
|
||||
fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
||||
if let Ok(ct) = mail.get_content_disposition() {
|
||||
return ct.disposition == DispositionType::Attachment
|
||||
&& ct
|
||||
.params
|
||||
.iter()
|
||||
.any(|(key, _value)| key.starts_with("filename"));
|
||||
}
|
||||
|
||||
false
|
||||
let ct = mail.get_content_disposition();
|
||||
ct.disposition == DispositionType::Attachment
|
||||
&& ct
|
||||
.params
|
||||
.iter()
|
||||
.any(|(key, _value)| key.starts_with("filename"))
|
||||
}
|
||||
|
||||
/// Tries to get attachment filename.
|
||||
@@ -1010,7 +1007,7 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
|
||||
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
|
||||
// or `Content-Disposition: ... filename=...`
|
||||
|
||||
let ct = mail.get_content_disposition()?;
|
||||
let ct = mail.get_content_disposition();
|
||||
|
||||
let desired_filename: Option<String> = ct
|
||||
.params
|
||||
@@ -1027,6 +1024,11 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
|
||||
let desired_filename =
|
||||
desired_filename.or_else(|| ct.params.get("name").map(|s| s.to_string()));
|
||||
|
||||
// MS Outlook is known to specify filename in the "name" attribute of
|
||||
// Content-Type and omit Content-Disposition.
|
||||
let desired_filename =
|
||||
desired_filename.or_else(|| mail.ctype.params.get("name").map(|s| s.to_string()));
|
||||
|
||||
// If there is no filename, but part is an attachment, guess filename
|
||||
if ct.disposition == DispositionType::Attachment && desired_filename.is_none() {
|
||||
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
|
||||
@@ -1042,50 +1044,50 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
|
||||
}
|
||||
}
|
||||
|
||||
// returned addresses are normalized and lowercased.
|
||||
fn get_recipients<S: AsRef<str>, T: Iterator<Item = (S, S)>>(headers: T) -> HashSet<String> {
|
||||
let mut recipients: HashSet<String> = Default::default();
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| {
|
||||
header_key == "to" || header_key == "cc"
|
||||
})
|
||||
}
|
||||
|
||||
for (hkey, hvalue) in headers {
|
||||
let hkey = hkey.as_ref().to_lowercase();
|
||||
let hvalue = hvalue.as_ref();
|
||||
if hkey == "to" || hkey == "cc" {
|
||||
if let Ok(addrs) = mailparse::addrparse(hvalue) {
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(ref info) => {
|
||||
recipients.insert(addr_normalize(&info.addr).to_lowercase());
|
||||
}
|
||||
mailparse::MailAddr::Group(ref infos) => {
|
||||
for info in &infos.addrs {
|
||||
recipients.insert(addr_normalize(&info.addr).to_lowercase());
|
||||
}
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "from")
|
||||
}
|
||||
|
||||
fn get_all_addresses_from_header<F>(headers: &[MailHeader], pred: F) -> Vec<SingleInfo>
|
||||
where
|
||||
F: Fn(String) -> bool,
|
||||
{
|
||||
let mut result: Vec<SingleInfo> = Default::default();
|
||||
|
||||
headers
|
||||
.iter()
|
||||
.filter(|header| pred(header.get_key().to_lowercase()))
|
||||
.filter_map(|header| mailparse::addrparse_header(header).ok())
|
||||
.for_each(|addrs| {
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(ref info) => {
|
||||
result.push(SingleInfo {
|
||||
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||
display_name: info.display_name.clone(),
|
||||
});
|
||||
}
|
||||
mailparse::MailAddr::Group(ref infos) => {
|
||||
for info in &infos.addrs {
|
||||
result.push(SingleInfo {
|
||||
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||
display_name: info.display_name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
recipients
|
||||
}
|
||||
|
||||
/// Check if the only addrs match, ignoring names.
|
||||
fn compare_addrs(a: &mailparse::MailAddr, b: &mailparse::MailAddr) -> bool {
|
||||
match a {
|
||||
mailparse::MailAddr::Group(group_a) => match b {
|
||||
mailparse::MailAddr::Group(group_b) => group_a
|
||||
.addrs
|
||||
.iter()
|
||||
.zip(group_b.addrs.iter())
|
||||
.all(|(a, b)| a.addr == b.addr),
|
||||
_ => false,
|
||||
},
|
||||
mailparse::MailAddr::Single(single_a) => match b {
|
||||
mailparse::MailAddr::Single(single_b) => single_a.addr == single_b.addr,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1134,12 +1136,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_recipients() {
|
||||
let context = dummy_context();
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
let recipients = get_recipients(mimeparser.header.iter());
|
||||
assert!(recipients.contains("abc@bcd.com"));
|
||||
assert!(recipients.contains("def@def.de"));
|
||||
let mail = mailparse::parse_mail(&raw[..]).unwrap();
|
||||
let recipients = get_recipients(&mail.headers);
|
||||
assert!(recipients.iter().any(|info| info.addr == "abc@bcd.com"));
|
||||
assert!(recipients.iter().any(|info| info.addr == "def@def.de"));
|
||||
assert_eq!(recipients.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1194,14 +1195,10 @@ mod tests {
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
|
||||
let of = mimeparser
|
||||
.parse_first_addr(&context.ctx, HeaderDef::From_)
|
||||
.unwrap();
|
||||
assert_eq!(of, mailparse::addrparse("hello@one.org").unwrap()[0]);
|
||||
let of = &mimeparser.from[0];
|
||||
assert_eq!(of.addr, "hello@one.org");
|
||||
|
||||
let of =
|
||||
mimeparser.parse_first_addr(&context.ctx, HeaderDef::ChatDispositionNotificationTo);
|
||||
assert!(of.is_none());
|
||||
assert!(mimeparser.chat_disposition_notification_to.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1211,14 +1208,16 @@ mod tests {
|
||||
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
||||
Subject: outer-subject\n\
|
||||
Secure-Join-Group: no\n\
|
||||
Test-Header: Bar\nChat-Version: 0.0\n\
|
||||
Secure-Join-Fingerprint: 123456\n\
|
||||
Test-Header: Bar\n\
|
||||
chat-VERSION: 0.0\n\
|
||||
\n\
|
||||
--==break==\n\
|
||||
Content-Type: text/plain; protected-headers=\"v1\";\n\
|
||||
Subject: inner-subject\n\
|
||||
SecureBar-Join-Group: yes\n\
|
||||
Test-Header: Xy\n\
|
||||
Chat-Version: 1.0\n\
|
||||
chat-VERSION: 1.0\n\
|
||||
\n\
|
||||
test1\n\
|
||||
\n\
|
||||
@@ -1237,12 +1236,17 @@ mod tests {
|
||||
|
||||
// the following fields would bubble up
|
||||
// if the test would really use encryption for the protected part
|
||||
// however, as this is not the case, the outer things stay valid
|
||||
// however, as this is not the case, the outer things stay valid.
|
||||
// for Chat-Version, also the case-insensivity is tested.
|
||||
assert_eq!(mimeparser.get_subject(), Some("outer-subject".into()));
|
||||
|
||||
let of = mimeparser.get(HeaderDef::ChatVersion).unwrap();
|
||||
assert_eq!(of, "0.0");
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
|
||||
// make sure, headers that are only allowed in the encrypted part
|
||||
// cannot be set from the outer part
|
||||
assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1369,7 +1373,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
Some("Chat: Message opened".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 0);
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.reports.len(), 1);
|
||||
}
|
||||
|
||||
@@ -1447,7 +1451,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
Some("Chat: Message opened".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 0);
|
||||
assert_eq!(message.parts.len(), 2);
|
||||
assert_eq!(message.reports.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1492,7 +1496,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
Some("Chat: Message opened".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 0);
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.reports.len(), 1);
|
||||
assert_eq!(message.reports[0].original_message_id, "foo@example.org");
|
||||
assert_eq!(
|
||||
@@ -1534,6 +1538,260 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
Some("Mail with inline attachment".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 2);
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.parts[0].typ, Viewtype::File);
|
||||
assert_eq!(message.parts[0].msg, "Hello!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_inline_image() {
|
||||
let context = dummy_context();
|
||||
let raw = br#"Message-ID: <foobar@example.org>
|
||||
From: foo <foo@example.org>
|
||||
Subject: example
|
||||
To: bar@example.org
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="--11019878869865180"
|
||||
|
||||
----11019878869865180
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Test
|
||||
|
||||
----11019878869865180
|
||||
Content-Type: image/jpeg;
|
||||
name="JPEG_filename.jpg"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: inline;
|
||||
filename="JPEG_filename.jpg"
|
||||
|
||||
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
|
||||
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
|
||||
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
|
||||
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
|
||||
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
|
||||
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
|
||||
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
|
||||
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
|
||||
|
||||
----11019878869865180--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(message.get_subject(), Some("example".to_string()));
|
||||
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.parts[0].typ, Viewtype::Image);
|
||||
assert_eq!(message.parts[0].msg, "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_thunderbird_html_embedded_image() {
|
||||
let context = dummy_context();
|
||||
let raw = br#"To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.org>
|
||||
Subject: Test subject
|
||||
Message-ID: <foobarbaz@example.org>
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101
|
||||
Thunderbird/68.7.0
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="------------779C1631600DF3DB8C02E53A"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------779C1631600DF3DB8C02E53A
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Test
|
||||
|
||||
|
||||
--------------779C1631600DF3DB8C02E53A
|
||||
Content-Type: multipart/related;
|
||||
boundary="------------10CC6C2609EB38DA782C5CA9"
|
||||
|
||||
|
||||
--------------10CC6C2609EB38DA782C5CA9
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
Test<br>
|
||||
<p><img moz-do-not-send="false" src="cid:part1.9DFA679B.52A88D69@example.org" alt=""></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--------------10CC6C2609EB38DA782C5CA9
|
||||
Content-Type: image/png;
|
||||
name="1.png"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <part1.9DFA679B.52A88D69@example.org>
|
||||
Content-Disposition: inline;
|
||||
filename="1.png"
|
||||
|
||||
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
|
||||
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
|
||||
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
|
||||
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
|
||||
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
|
||||
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
|
||||
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
|
||||
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
--------------10CC6C2609EB38DA782C5CA9--
|
||||
|
||||
--------------779C1631600DF3DB8C02E53A--"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(message.get_subject(), Some("Test subject".to_string()));
|
||||
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.parts[0].typ, Viewtype::Image);
|
||||
assert_eq!(message.parts[0].msg, "Test");
|
||||
}
|
||||
|
||||
// Outlook specifies filename in the "name" attribute of Content-Type
|
||||
#[test]
|
||||
fn parse_outlook_html_embedded_image() {
|
||||
let context = dummy_context();
|
||||
let raw = br##"From: Anonymous <anonymous@example.org>
|
||||
To: Anonymous <anonymous@example.org>
|
||||
Subject: Delta Chat is great stuff!
|
||||
Date: Tue, 5 May 2020 01:23:45 +0000
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related;
|
||||
boundary="----=_NextPart_000_0003_01D622B3.CA753E60"
|
||||
X-Mailer: Microsoft Outlook 15.0
|
||||
|
||||
This is a multipart message in MIME format.
|
||||
|
||||
------=_NextPart_000_0003_01D622B3.CA753E60
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_NextPart_001_0004_01D622B3.CA753E60"
|
||||
|
||||
|
||||
------=_NextPart_001_0004_01D622B3.CA753E60
|
||||
Content-Type: text/plain;
|
||||
charset="us-ascii"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
|
||||
|
||||
------=_NextPart_001_0004_01D622B3.CA753E60
|
||||
Content-Type: text/html;
|
||||
charset="us-ascii"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Test<img src="cid:image001.jpg@01D622B3.C9D8D750">
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
------=_NextPart_001_0004_01D622B3.CA753E60--
|
||||
|
||||
------=_NextPart_000_0003_01D622B3.CA753E60
|
||||
Content-Type: image/jpeg;
|
||||
name="image001.jpg"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <image001.jpg@01D622B3.C9D8D750>
|
||||
|
||||
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
|
||||
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
|
||||
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
|
||||
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
|
||||
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
|
||||
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
|
||||
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
|
||||
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
|
||||
------=_NextPart_000_0003_01D622B3.CA753E60--
|
||||
"##;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(
|
||||
message.get_subject(),
|
||||
Some("Delta Chat is great stuff!".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.parts[0].typ, Viewtype::Image);
|
||||
assert_eq!(message.parts[0].msg, "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_id() {
|
||||
let test = parse_message_id("<foobar>");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foobar");
|
||||
|
||||
let test = parse_message_id("<foo> <bar>");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foo");
|
||||
|
||||
let test = parse_message_id(" < foo > <bar>");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foo");
|
||||
|
||||
let test = parse_message_id("foo");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foo");
|
||||
|
||||
let test = parse_message_id(" foo ");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foo");
|
||||
|
||||
let test = parse_message_id("foo bar");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foo");
|
||||
|
||||
let test = parse_message_id(" foo bar ");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "foo");
|
||||
|
||||
let test = parse_message_id("");
|
||||
assert!(test.is_err());
|
||||
|
||||
let test = parse_message_id(" ");
|
||||
assert!(test.is_err());
|
||||
|
||||
let test = parse_message_id("<>");
|
||||
assert!(test.is_err());
|
||||
|
||||
let test = parse_message_id("<> bar");
|
||||
assert!(test.is_ok());
|
||||
assert_eq!(test.unwrap(), "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_ids() {
|
||||
let test = parse_message_ids(" foo bar <foobar>").unwrap();
|
||||
assert_eq!(test.len(), 3);
|
||||
assert_eq!(test[0], "foo");
|
||||
assert_eq!(test[1], "bar");
|
||||
assert_eq!(test[2], "foobar");
|
||||
|
||||
let test = parse_message_ids(" < foobar >").unwrap();
|
||||
assert_eq!(test.len(), 1);
|
||||
assert_eq!(test[0], "foobar");
|
||||
|
||||
let test = parse_message_ids("").unwrap();
|
||||
assert!(test.is_empty());
|
||||
|
||||
let test = parse_message_ids(" ").unwrap();
|
||||
assert!(test.is_empty());
|
||||
|
||||
let test = parse_message_ids(" < ").unwrap();
|
||||
assert!(test.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blob::{BlobError, BlobObject};
|
||||
use crate::context::Context;
|
||||
use crate::error;
|
||||
use crate::error::{self, bail, ensure};
|
||||
use crate::message::MsgId;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
|
||||
@@ -88,12 +88,6 @@ pub enum Param {
|
||||
/// For Jobs
|
||||
SetLongitude = b'n',
|
||||
|
||||
/// For Jobs
|
||||
ServerFolder = b'Z',
|
||||
|
||||
/// For Jobs
|
||||
ServerUid = b'z',
|
||||
|
||||
/// For Jobs
|
||||
AlsoMove = b'M',
|
||||
|
||||
|
||||
103
src/pgp.rs
103
src/pgp.rs
@@ -18,7 +18,7 @@ use rand::{thread_rng, CryptoRng, Rng};
|
||||
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::error::Result;
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::key::*;
|
||||
use crate::keyring::*;
|
||||
|
||||
@@ -117,21 +117,19 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
||||
///
|
||||
/// Most of these are likely coding errors rather than user errors
|
||||
/// since all variability is hardcoded.
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display = "PgpKeygenError: {}", message)]
|
||||
pub(crate) struct PgpKeygenError {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("PgpKeygenError: {message}")]
|
||||
pub struct PgpKeygenError {
|
||||
message: String,
|
||||
#[cause]
|
||||
cause: failure::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
}
|
||||
|
||||
impl PgpKeygenError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
||||
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +187,7 @@ pub(crate) fn create_keypair(
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
|
||||
.map_err(|err| PgpKeygenError::new("invalid key params", format_err!(err)))?;
|
||||
let key = key_params
|
||||
.generate()
|
||||
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
||||
@@ -394,7 +392,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // is too expensive
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(
|
||||
EmailAddress::new("foo@bar.de").unwrap(),
|
||||
@@ -573,88 +570,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
}
|
||||
|
||||
fn test_encrypt_decrypt_fuzz(key_type: KeyGenType) {
|
||||
// sending messages from Alice to Bob
|
||||
let alice = create_keypair(EmailAddress::new("a@e.org").unwrap(), key_type).unwrap();
|
||||
let bob = create_keypair(EmailAddress::new("b@e.org").unwrap(), key_type).unwrap();
|
||||
|
||||
let alice_secret = Key::from(alice.secret.clone());
|
||||
let alice_public = Key::from(alice.public.clone());
|
||||
let bob_secret = Key::from(bob.secret.clone());
|
||||
let bob_public = Key::from(bob.public.clone());
|
||||
|
||||
let plain: &[u8] = b"just a test";
|
||||
|
||||
let mut encr_keyring = Keyring::default();
|
||||
encr_keyring.add_ref(&bob_public);
|
||||
|
||||
let mut decr_keyring = Keyring::default();
|
||||
decr_keyring.add_ref(&bob_secret);
|
||||
let mut validate_keyring = Keyring::default();
|
||||
validate_keyring.add_ref(&alice_public);
|
||||
|
||||
for _ in 0..1000 {
|
||||
let ctext = pk_encrypt(plain.as_ref(), &encr_keyring, Some(&alice_secret)).unwrap();
|
||||
let plain2 =
|
||||
pk_decrypt(ctext.as_ref(), &decr_keyring, &validate_keyring, None).unwrap();
|
||||
assert_eq!(plain2, plain);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_encrypt_decrypt_fuzz_rsa() {
|
||||
test_encrypt_decrypt_fuzz(KeyGenType::Rsa2048)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_fuzz_ecc() {
|
||||
test_encrypt_decrypt_fuzz(KeyGenType::Ed25519)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_fuzz_alice() -> Result<()> {
|
||||
let plain: &[u8] = b"just a test";
|
||||
let lit_msg = Message::new_literal_bytes("", plain);
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let enc_key =
|
||||
SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap();
|
||||
let enc_subkey = &enc_key.public_subkeys[0];
|
||||
assert!(enc_subkey.is_encryption_key());
|
||||
let pkeys: Vec<&SignedPublicSubKey> = vec![&enc_subkey];
|
||||
|
||||
let skey = SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap();
|
||||
let skeys: Vec<&SignedSecretKey> = vec![&skey];
|
||||
|
||||
for _ in 0..1000 {
|
||||
let msg = lit_msg.encrypt_to_keys(
|
||||
&mut rng,
|
||||
pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,
|
||||
&pkeys[..],
|
||||
)?;
|
||||
let ctext = msg.to_armored_string(None)?;
|
||||
|
||||
println!("{}", ctext);
|
||||
|
||||
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||
|
||||
let dec_msg = &msgs[0];
|
||||
|
||||
let plain2: Vec<u8> = match dec_msg.get_content()? {
|
||||
Some(content) => content,
|
||||
None => bail!("Decrypted message is empty"),
|
||||
};
|
||||
|
||||
assert_eq!(plain2, plain);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// aol.md: aol.com
|
||||
@@ -28,6 +29,7 @@ lazy_static::lazy_static! {
|
||||
overview_page: "https://providers.delta.chat/aol",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// autistici.org.md: autistici.org
|
||||
@@ -40,6 +42,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "mail.autistici.org", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// bluewin.ch.md: bluewin.ch
|
||||
@@ -52,6 +55,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imaps.bluewin.ch", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// comcast.md: xfinity.com, comcast.net
|
||||
@@ -73,6 +77,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.example.com", port: 1337, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// fastmail.md: fastmail.com
|
||||
@@ -83,6 +88,7 @@ lazy_static::lazy_static! {
|
||||
overview_page: "https://providers.delta.chat/fastmail",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// freenet.de.md: freenet.de
|
||||
@@ -95,6 +101,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "mx.freenet.de", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// gmail.md: gmail.com, googlemail.com
|
||||
@@ -107,6 +114,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmail.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
|
||||
@@ -120,6 +128,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "mail.gmx.net", port: 465, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// i.ua.md: i.ua
|
||||
@@ -135,6 +144,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.me.com", port: 993, username_pattern: EMAILLOCALPART },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// kolst.com.md: kolst.com
|
||||
@@ -159,6 +169,15 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.nauta.cu", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.nauta.cu", port: 25, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault { key: Config::DeleteServerAfter, value: "1" },
|
||||
ConfigDefault { key: Config::BccSelf, value: "0" },
|
||||
ConfigDefault { key: Config::SentboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxMove, value: "0" },
|
||||
ConfigDefault { key: Config::E2eeEnabled, value: "0" },
|
||||
ConfigDefault { key: Config::MediaQuality, value: "1" },
|
||||
]),
|
||||
};
|
||||
|
||||
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
|
||||
@@ -171,6 +190,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap-mail.outlook.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// posteo.md: posteo.de
|
||||
@@ -183,6 +203,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "posteo.de", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// protonmail.md: protonmail.com, protonmail.ch
|
||||
@@ -193,6 +214,7 @@ lazy_static::lazy_static! {
|
||||
overview_page: "https://providers.delta.chat/protonmail",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// riseup.net.md: riseup.net
|
||||
@@ -201,6 +223,9 @@ lazy_static::lazy_static! {
|
||||
// rogers.com.md: rogers.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// systemli.org.md: systemli.org
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// t-online.md: t-online.de, magenta.de
|
||||
static ref P_T_ONLINE: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
@@ -209,6 +234,7 @@ lazy_static::lazy_static! {
|
||||
overview_page: "https://providers.delta.chat/t-online",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// testrun.md: testrun.org
|
||||
@@ -222,6 +248,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// tiscali.it.md: tiscali.it
|
||||
@@ -234,6 +261,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.tiscali.it", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// ukr.net.md: ukr.net
|
||||
@@ -253,6 +281,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.web.de", port: 143, username_pattern: EMAILLOCALPART },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
|
||||
@@ -265,10 +294,19 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.yahoo.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// yandex.ru.md: yandex.ru, yandex.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
static ref P_YANDEX_RU: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "For Yandex accounts, you have to set IMAP protocol option turned on.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/yandex-ru",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
// ziggo.nl.md: ziggo.nl
|
||||
static ref P_ZIGGO_NL: Provider = Provider {
|
||||
@@ -280,6 +318,7 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.ziggo.nl", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
};
|
||||
|
||||
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
|
||||
@@ -360,6 +399,8 @@ lazy_static::lazy_static! {
|
||||
("ymail.com", &*P_YAHOO),
|
||||
("rocketmail.com", &*P_YAHOO),
|
||||
("yahoodns.net", &*P_YAHOO),
|
||||
("yandex.ru", &*P_YANDEX_RU),
|
||||
("yandex.com", &*P_YANDEX_RU),
|
||||
("ziggo.nl", &*P_ZIGGO_NL),
|
||||
].iter().copied().collect();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
mod data;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::provider::data::PROVIDER_DATA;
|
||||
|
||||
@@ -57,6 +58,12 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConfigDefault {
|
||||
pub key: Config,
|
||||
pub value: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Provider {
|
||||
pub status: Status,
|
||||
@@ -64,6 +71,7 @@ pub struct Provider {
|
||||
pub after_login_hint: &'static str,
|
||||
pub overview_page: &'static str,
|
||||
pub server: Vec<Server>,
|
||||
pub config_defaults: Option<Vec<ConfigDefault>>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
|
||||
@@ -9,6 +9,9 @@ out_all = ""
|
||||
out_domains = ""
|
||||
domains_dict = {}
|
||||
|
||||
def camel(name):
|
||||
words = name.split("_")
|
||||
return "".join(w.capitalize() for i, w in enumerate(words))
|
||||
|
||||
def cleanstr(s):
|
||||
s = s.strip()
|
||||
@@ -31,6 +34,18 @@ def file2url(f):
|
||||
return "https://providers.delta.chat/" + f
|
||||
|
||||
|
||||
def process_config_defaults(data):
|
||||
if not "config_defaults" in data:
|
||||
return "None"
|
||||
defaults = "Some(vec![\n"
|
||||
config_defaults = data.get("config_defaults", "")
|
||||
for key in config_defaults:
|
||||
value = str(config_defaults[key])
|
||||
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
|
||||
defaults += " ])"
|
||||
return defaults
|
||||
|
||||
|
||||
def process_data(data, file):
|
||||
status = data.get("status", "")
|
||||
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
|
||||
@@ -83,6 +98,8 @@ def process_data(data, file):
|
||||
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
|
||||
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
|
||||
|
||||
config_defaults = process_config_defaults(data)
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", ""))
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
@@ -93,6 +110,7 @@ def process_data(data, file):
|
||||
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
|
||||
provider += " overview_page: \"" + file2url(file) + "\",\n"
|
||||
provider += " server: vec![\n" + server + " ],\n"
|
||||
provider += " config_defaults: " + config_defaults + ",\n"
|
||||
provider += " };\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
@@ -103,7 +121,7 @@ def process_data(data, file):
|
||||
# finally, add the provider
|
||||
global out_all, out_domains
|
||||
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
|
||||
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "":
|
||||
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "" and config_defaults == "None":
|
||||
out_all += " // - skipping provider with status OK and no special things to do\n\n"
|
||||
else:
|
||||
out_all += provider
|
||||
|
||||
80
src/qr.rs
80
src/qr.rs
@@ -2,20 +2,20 @@
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::chat;
|
||||
use crate::config::*;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::key::dc_format_fingerprint;
|
||||
use crate::key::dc_normalize_fingerprint;
|
||||
use crate::lot::{Lot, LotState};
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
@@ -37,6 +37,10 @@ impl Into<Lot> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
||||
string.to_lowercase().starts_with(&pattern.to_lowercase())
|
||||
}
|
||||
|
||||
/// Check a scanned QR code.
|
||||
/// The function should be called after a QR code is scanned.
|
||||
/// The function takes the raw text scanned and checks what can be done with it.
|
||||
@@ -45,9 +49,9 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
|
||||
info!(context, "Scanned QR code: {}", qr);
|
||||
|
||||
if qr.starts_with(OPENPGP4FPR_SCHEME) {
|
||||
if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
|
||||
decode_openpgp(context, qr)
|
||||
} else if qr.starts_with(DCACCOUNT_SCHEME) {
|
||||
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
|
||||
decode_account(context, qr)
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
decode_mailto(context, qr)
|
||||
@@ -221,27 +225,20 @@ pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
|
||||
let response = reqwest::blocking::Client::new().post(url_str).send();
|
||||
if response.is_err() {
|
||||
return Err(format_err!(
|
||||
"Cannot create account, request to {} failed",
|
||||
url_str
|
||||
));
|
||||
bail!("Cannot create account, request to {} failed", url_str);
|
||||
}
|
||||
let response = response.unwrap();
|
||||
if !response.status().is_success() {
|
||||
return Err(format_err!(
|
||||
"Request to {} unsuccessful: {:?}",
|
||||
url_str,
|
||||
response
|
||||
));
|
||||
bail!("Request to {} unsuccessful: {:?}", url_str, response);
|
||||
}
|
||||
|
||||
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
|
||||
if parsed.is_err() {
|
||||
return Err(format_err!(
|
||||
bail!(
|
||||
"Failed to parse JSON response from {}: error: {:?}",
|
||||
url_str,
|
||||
parsed
|
||||
));
|
||||
);
|
||||
}
|
||||
println!("response: {:?}", &parsed);
|
||||
let parsed = parsed.unwrap();
|
||||
@@ -289,7 +286,6 @@ fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => return err.into(),
|
||||
};
|
||||
|
||||
let name = "".to_string();
|
||||
Lot::from_address(context, name, addr)
|
||||
}
|
||||
@@ -525,6 +521,17 @@ mod tests {
|
||||
assert_ne!(res.get_id(), 0);
|
||||
assert_eq!(res.get_text1().unwrap(), "test ? test !");
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
);
|
||||
|
||||
println!("{:?}", res);
|
||||
assert_eq!(res.get_state(), LotState::QrAskVerifyGroup);
|
||||
assert_ne!(res.get_id(), 0);
|
||||
assert_eq!(res.get_text1().unwrap(), "test ? test !");
|
||||
|
||||
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap();
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
}
|
||||
@@ -542,6 +549,16 @@ mod tests {
|
||||
assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
|
||||
assert_ne!(res.get_id(), 0);
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
);
|
||||
|
||||
println!("{:?}", res);
|
||||
assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
|
||||
assert_ne!(res.get_id(), 0);
|
||||
|
||||
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap();
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_name(), "Jörn P. P.");
|
||||
@@ -562,6 +579,19 @@ mod tests {
|
||||
);
|
||||
assert_eq!(res.get_id(), 0);
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrFprWithoutAddr);
|
||||
assert_eq!(
|
||||
res.get_text1().unwrap(),
|
||||
"1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890"
|
||||
);
|
||||
assert_eq!(res.get_id(), 0);
|
||||
|
||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890");
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
assert_eq!(res.get_id(), 0);
|
||||
@@ -577,6 +607,14 @@ mod tests {
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrAccount);
|
||||
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrAccount);
|
||||
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -589,5 +627,13 @@ mod tests {
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
assert!(res.get_text1().is_some());
|
||||
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
assert!(res.get_text1().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{dc_normalize_fingerprint, Key};
|
||||
use crate::key::{dc_normalize_fingerprint, DcKey, Key, SignedPublicKey};
|
||||
use crate::lot::LotState;
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::*;
|
||||
@@ -135,12 +135,13 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
|
||||
}
|
||||
|
||||
fn get_self_fingerprint(context: &Context) -> Option<String> {
|
||||
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
|
||||
if let Some(key) = Key::from_self_public(context, self_addr, &context.sql) {
|
||||
return Some(key.fingerprint());
|
||||
match SignedPublicKey::load_self(context) {
|
||||
Ok(key) => Some(Key::from(key).fingerprint()),
|
||||
Err(_) => {
|
||||
warn!(context, "get_self_fingerprint(): failed to load key");
|
||||
None
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
|
||||
@@ -343,24 +344,21 @@ fn fingerprint_equals_sender(
|
||||
}
|
||||
false
|
||||
}
|
||||
#[derive(Fail, Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum HandshakeError {
|
||||
#[fail(display = "Can not be called with special contact ID")]
|
||||
#[error("Can not be called with special contact ID")]
|
||||
SpecialContactId,
|
||||
#[fail(display = "Not a Secure-Join message")]
|
||||
#[error("Not a Secure-Join message")]
|
||||
NotSecureJoinMsg,
|
||||
#[fail(
|
||||
display = "Failed to look up or create chat for contact #{}",
|
||||
contact_id
|
||||
)]
|
||||
#[error("Failed to look up or create chat for contact #{contact_id}")]
|
||||
NoChat {
|
||||
contact_id: u32,
|
||||
#[cause]
|
||||
#[source]
|
||||
cause: Error,
|
||||
},
|
||||
#[fail(display = "Chat for group {} not found", group)]
|
||||
#[error("Chat for group {group} not found")]
|
||||
ChatNotFound { group: String },
|
||||
#[fail(display = "No configured self address found")]
|
||||
#[error("No configured self address found")]
|
||||
NoSelfAddr,
|
||||
}
|
||||
|
||||
@@ -393,8 +391,6 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
mime_message: &MimeMessage,
|
||||
contact_id: u32,
|
||||
) -> Result<HandshakeMessage, HandshakeError> {
|
||||
let own_fingerprint: String;
|
||||
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return Err(HandshakeError::SpecialContactId);
|
||||
}
|
||||
@@ -510,7 +506,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
own_fingerprint = get_self_fingerprint(context).unwrap();
|
||||
let own_fingerprint = get_self_fingerprint(context).unwrap();
|
||||
joiner_progress!(context, contact_id, 400);
|
||||
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
|
||||
|
||||
@@ -622,10 +618,17 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
}
|
||||
} else {
|
||||
// Alice -> Bob
|
||||
send_handshake_msg(context, contact_chat_id, "vc-contact-confirm", "", None, "");
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"vc-contact-confirm",
|
||||
"",
|
||||
Some(fingerprint.clone()),
|
||||
"",
|
||||
);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
}
|
||||
Ok(HandshakeMessage::Done)
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
"vg-member-added" | "vc-contact-confirm" => {
|
||||
/*=======================================================
|
||||
@@ -710,26 +713,30 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id);
|
||||
context.bob.write().unwrap().expects = 0;
|
||||
if join_vg {
|
||||
// Bob -> Alice
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"vg-member-added-received",
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// Bob -> Alice
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg {
|
||||
"vg-member-added-received"
|
||||
} else {
|
||||
"vc-contact-confirm-received" // only for observe_securejoin_on_other_device()
|
||||
},
|
||||
"",
|
||||
Some(scanned_fingerprint_of_alice),
|
||||
"",
|
||||
);
|
||||
|
||||
context.bob.write().unwrap().status = 1;
|
||||
context.stop_ongoing();
|
||||
Ok(if join_vg {
|
||||
HandshakeMessage::Propagate
|
||||
} else {
|
||||
HandshakeMessage::Done
|
||||
HandshakeMessage::Ignore // "Done" deletes the message and breaks multi-device
|
||||
})
|
||||
}
|
||||
"vg-member-added-received" => {
|
||||
"vg-member-added-received" | "vc-contact-confirm-received" => {
|
||||
/*==========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 8 in "Out-of-band verified groups" protocol ====
|
||||
@@ -737,29 +744,26 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
|
||||
if contact.is_verified(context) == VerifiedStatus::Unverified {
|
||||
warn!(context, "vg-member-added-received invalid.",);
|
||||
warn!(context, "{} invalid.", step);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
let field_grpid = mime_message
|
||||
.get(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid)
|
||||
.map_err(|err| {
|
||||
if join_vg {
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
let field_grpid = mime_message
|
||||
.get(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid) {
|
||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||
HandshakeError::ChatNotFound {
|
||||
return Err(HandshakeError::ChatNotFound {
|
||||
group: field_grpid.to_string(),
|
||||
}
|
||||
})?;
|
||||
context.call_cb(Event::SecurejoinMemberAdded {
|
||||
chat_id: group_chat_id,
|
||||
contact_id,
|
||||
});
|
||||
Ok(HandshakeMessage::Done)
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
|
||||
} else {
|
||||
warn!(context, "vg-member-added-received invalid.",);
|
||||
warn!(context, "{} invalid.", step);
|
||||
Ok(HandshakeMessage::Ignore)
|
||||
}
|
||||
}
|
||||
@@ -770,6 +774,98 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
}
|
||||
}
|
||||
|
||||
/// observe_securejoin_on_other_device() must be called when a self-sent securejoin message is seen.
|
||||
///
|
||||
/// in a multi-device-setup, there may be other devices that "see" the handshake messages.
|
||||
/// if the seen messages seen are self-sent messages encrypted+signed correctly with our key,
|
||||
/// we can make some conclusions of it:
|
||||
///
|
||||
/// - if we see the self-sent-message vg-member-added/vc-contact-confirm,
|
||||
/// we know that we're an inviter-observer.
|
||||
/// the inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth
|
||||
/// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm,
|
||||
/// we can mark the peer as verified as well.
|
||||
///
|
||||
/// - if we see the self-sent-message vg-member-added-received
|
||||
/// we know that we're an joiner-observer.
|
||||
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
|
||||
/// before sending vg-member-added-received - so, if we observe vg-member-added-received,
|
||||
/// we can mark the peer as verified as well.
|
||||
pub(crate) fn observe_securejoin_on_other_device(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
contact_id: u32,
|
||||
) -> Result<HandshakeMessage, HandshakeError> {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return Err(HandshakeError::SpecialContactId);
|
||||
}
|
||||
let step = mime_message
|
||||
.get(HeaderDef::SecureJoin)
|
||||
.ok_or(HandshakeError::NotSecureJoinMsg)?;
|
||||
info!(context, "observing secure-join message \'{}\'", step);
|
||||
|
||||
let contact_chat_id =
|
||||
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) {
|
||||
Ok((chat_id, blocked)) => {
|
||||
if blocked != Blocked::Not {
|
||||
chat_id.unblock(context);
|
||||
}
|
||||
chat_id
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(HandshakeError::NoChat {
|
||||
contact_id,
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
match step.as_str() {
|
||||
"vg-member-added"
|
||||
| "vc-contact-confirm"
|
||||
| "vg-member-added-received"
|
||||
| "vc-contact-confirm-received" => {
|
||||
if !encrypted_and_signed(
|
||||
context,
|
||||
mime_message,
|
||||
get_self_fingerprint(context).unwrap_or_default(),
|
||||
) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Message not encrypted correctly.",
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided, please update Delta Chat on all your devices.",
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
if mark_peer_as_verified(context, fingerprint).is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
Ok(if step.as_str() == "vg-member-added" {
|
||||
HandshakeMessage::Propagate
|
||||
} else {
|
||||
HandshakeMessage::Ignore
|
||||
})
|
||||
}
|
||||
_ => Ok(HandshakeMessage::Ignore),
|
||||
}
|
||||
}
|
||||
|
||||
fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
|
||||
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id);
|
||||
let contact = Contact::get_by_id(context, contact_id);
|
||||
|
||||
149
src/simplify.rs
149
src/simplify.rs
@@ -1,13 +1,41 @@
|
||||
// protect lines starting with `--` against being treated as a footer.
|
||||
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
// this should be invisible on most systems and there is no need to unescape it again
|
||||
// (which won't be done by non-deltas anyway)
|
||||
//
|
||||
// this escapes a bit more than actually needed by delta (eg. also lines as "-- footer"),
|
||||
// but for non-delta-compatibility, that seems to be better.
|
||||
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if text.starts_with("--") {
|
||||
"-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-")
|
||||
} else {
|
||||
text.replace("\n--", "\n-\u{200B}-")
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
// quoted-printable may encode `-- ` to `-- =20` which is converted
|
||||
// back to `-- `
|
||||
match line {
|
||||
// some providers encode `-- ` to `-- =20` which results in `-- `
|
||||
"-- " | "-- " => return &lines[..ix],
|
||||
// some providers encode `-- ` to `=2D-` which results in only `--`;
|
||||
// use that only when no other footer is found
|
||||
// and if the line before is empty and the line after is not empty
|
||||
"--" => {
|
||||
if (ix == 0 || lines[ix - 1] == "") && ix != lines.len() - 1 && lines[ix + 1] != ""
|
||||
{
|
||||
nearly_standard_footer = Some(ix);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
if let Some(ix) = nearly_standard_footer {
|
||||
return &lines[..ix];
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
@@ -41,25 +69,27 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
|
||||
let lines = split_lines(&input);
|
||||
let (lines, is_forwarded) = skip_forward_header(&lines);
|
||||
|
||||
let lines = remove_message_footer(lines);
|
||||
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
|
||||
let (lines, has_bottom_quote) = if !is_chat_message {
|
||||
remove_bottom_quote(lines)
|
||||
} else {
|
||||
(lines, false)
|
||||
};
|
||||
let (lines, has_top_quote) = if !is_chat_message {
|
||||
remove_top_quote(lines)
|
||||
} else {
|
||||
(lines, false)
|
||||
};
|
||||
let original_lines = &lines;
|
||||
|
||||
// re-create buffer from the remaining lines
|
||||
let text = render_message(
|
||||
lines,
|
||||
has_top_quote,
|
||||
has_nonstandard_footer || has_bottom_quote,
|
||||
);
|
||||
let lines = remove_message_footer(lines);
|
||||
|
||||
let text = if is_chat_message {
|
||||
render_message(lines, false, false)
|
||||
} else {
|
||||
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
|
||||
let (lines, has_bottom_quote) = remove_bottom_quote(lines);
|
||||
let (lines, has_top_quote) = remove_top_quote(lines);
|
||||
|
||||
if lines.iter().all(|it| it.trim().is_empty()) {
|
||||
render_message(original_lines, false, false)
|
||||
} else {
|
||||
render_message(
|
||||
lines,
|
||||
has_top_quote,
|
||||
has_nonstandard_footer || has_bottom_quote,
|
||||
)
|
||||
}
|
||||
};
|
||||
(text, is_forwarded)
|
||||
}
|
||||
|
||||
@@ -67,14 +97,13 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
|
||||
/// Returns message body lines and a boolean indicating whether
|
||||
/// a message is forwarded or not.
|
||||
fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
if lines.len() >= 3
|
||||
&& lines[0] == "---------- Forwarded message ----------"
|
||||
&& lines[1].starts_with("From: ")
|
||||
&& lines[2].is_empty()
|
||||
{
|
||||
(&lines[3..], true)
|
||||
} else {
|
||||
(lines, false)
|
||||
match lines {
|
||||
["---------- Forwarded message ----------", first_line, "", rest @ ..]
|
||||
if first_line.starts_with("From: ") =>
|
||||
{
|
||||
(rest, true)
|
||||
}
|
||||
_ => (lines, false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +184,8 @@ fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) ->
|
||||
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
|
||||
ret += " [...]";
|
||||
}
|
||||
ret
|
||||
// redo escaping done by escape_message_footer_marks()
|
||||
ret.replace("\u{200B}", "")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,6 +234,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dont_remove_whole_message() {
|
||||
let input = "\n------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text".to_string();
|
||||
let (plain, is_forwarded) = simplify(input, false);
|
||||
assert_eq!(
|
||||
plain,
|
||||
"------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text"
|
||||
);
|
||||
assert!(!is_forwarded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chat_message() {
|
||||
let input = "Hi! How are you?\n\n---\n\nI am good.\n-- \nSent with my Delta Chat Messenger: https://delta.chat".to_string();
|
||||
let (plain, is_forwarded) = simplify(input, true);
|
||||
assert_eq!(plain, "Hi! How are you?\n\n---\n\nI am good.");
|
||||
assert!(!is_forwarded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplify_trim() {
|
||||
let input = "line1\n\r\r\rline2".to_string();
|
||||
@@ -248,4 +297,46 @@ mod tests {
|
||||
assert_eq!(lines, &["not a quote", "> first", "> second"]);
|
||||
assert!(!has_top_quote);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_message_footer_marks() {
|
||||
let esc = escape_message_footer_marks("--\n--text --in line");
|
||||
assert_eq!(esc, "-\u{200B}-\n-\u{200B}-text --in line");
|
||||
|
||||
let esc = escape_message_footer_marks("--\r\n--text");
|
||||
assert_eq!(esc, "-\u{200B}-\r\n-\u{200B}-text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_message_footer() {
|
||||
let input = "text\n--\nno footer".to_string();
|
||||
let (plain, _) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n--\nno footer");
|
||||
|
||||
let input = "text\n\n--\n\nno footer".to_string();
|
||||
let (plain, _) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n\n--\n\nno footer");
|
||||
|
||||
let input = "text\n\n-- no footer\n\n".to_string();
|
||||
let (plain, _) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n\n-- no footer");
|
||||
|
||||
let input = "text\n\n--\nno footer\n-- \nfooter".to_string();
|
||||
let (plain, _) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n\n--\nno footer");
|
||||
|
||||
let input = "text\n\n--\ntreated as footer when unescaped".to_string();
|
||||
let (plain, _) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, "text"); // see remove_message_footer() for some explanations
|
||||
let escaped = escape_message_footer_marks(&input);
|
||||
let (plain, _) = simplify(escaped, true);
|
||||
assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped");
|
||||
|
||||
let input = "--\ntreated as footer when unescaped".to_string();
|
||||
let (plain, _) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, ""); // see remove_message_footer() for some explanations
|
||||
let escaped = escape_message_footer_marks(&input);
|
||||
let (plain, _) = simplify(escaped, true);
|
||||
assert_eq!(plain, "--\ntreated as footer when unescaped");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,39 +12,34 @@ use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::login_param::{dc_build_tls, LoginParam};
|
||||
use crate::oauth2::*;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// SMTP write and read timeout in seconds.
|
||||
const SMTP_TIMEOUT: u64 = 30;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Bad parameters")]
|
||||
#[error("Bad parameters")]
|
||||
BadParameters,
|
||||
|
||||
#[fail(display = "Invalid login address {}: {}", address, error)]
|
||||
#[error("Invalid login address {address}: {error}")]
|
||||
InvalidLoginAddress {
|
||||
address: String,
|
||||
#[cause]
|
||||
#[source]
|
||||
error: error::Error,
|
||||
},
|
||||
|
||||
#[fail(display = "SMTP failed to connect: {:?}", _0)]
|
||||
ConnectionFailure(#[cause] smtp::error::Error),
|
||||
#[error("SMTP: failed to connect: {0:?}")]
|
||||
ConnectionFailure(#[source] smtp::error::Error),
|
||||
|
||||
#[fail(display = "SMTP: failed to setup connection {:?}", _0)]
|
||||
ConnectionSetupFailure(#[cause] smtp::error::Error),
|
||||
#[error("SMTP: failed to setup connection {0:?}")]
|
||||
ConnectionSetupFailure(#[source] smtp::error::Error),
|
||||
|
||||
#[fail(display = "SMTP: oauth2 error {:?}", _0)]
|
||||
#[error("SMTP: oauth2 error {address}")]
|
||||
Oauth2Error { address: String },
|
||||
|
||||
#[fail(display = "TLS error")]
|
||||
Tls(#[cause] async_native_tls::Error),
|
||||
}
|
||||
|
||||
impl From<async_native_tls::Error> for Error {
|
||||
fn from(err: async_native_tls::Error) -> Error {
|
||||
Error::Tls(err)
|
||||
}
|
||||
#[error("TLS error")]
|
||||
Tls(#[from] async_native_tls::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -175,7 +170,18 @@ impl Smtp {
|
||||
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
|
||||
|
||||
let mut trans = client.into_transport();
|
||||
trans.connect().await.map_err(Error::ConnectionFailure)?;
|
||||
|
||||
trans.connect().await.map_err(|err| {
|
||||
let message = {
|
||||
context.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("SMTP {}:{}", domain, port),
|
||||
err.to_string(),
|
||||
)
|
||||
};
|
||||
emit_event!(context, Event::ErrorNetwork(message));
|
||||
Error::ConnectionFailure(err)
|
||||
})?;
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(Instant::now());
|
||||
|
||||
@@ -8,15 +8,15 @@ use crate::events::Event;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Envelope error: {}", _0)]
|
||||
EnvelopeError(#[cause] async_smtp::error::Error),
|
||||
#[error("Envelope error: {}", _0)]
|
||||
EnvelopeError(#[from] async_smtp::error::Error),
|
||||
|
||||
#[fail(display = "Send error: {}", _0)]
|
||||
SendError(#[cause] async_smtp::smtp::error::Error),
|
||||
#[error("Send error: {}", _0)]
|
||||
SendError(#[from] async_smtp::smtp::error::Error),
|
||||
|
||||
#[fail(display = "SMTP has no transport")]
|
||||
#[error("SMTP has no transport")]
|
||||
NoTransport,
|
||||
}
|
||||
|
||||
|
||||
131
src/sql.rs
131
src/sql.rs
@@ -8,56 +8,34 @@ use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
|
||||
use thread_local_object::ThreadLocal;
|
||||
|
||||
use crate::chat::{update_device_icon, update_saved_messages_icon};
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Sqlite Error: {:?}", _0)]
|
||||
Sql(#[cause] rusqlite::Error),
|
||||
#[fail(display = "Sqlite Connection Pool Error: {:?}", _0)]
|
||||
ConnectionPool(#[cause] r2d2::Error),
|
||||
#[fail(display = "Sqlite: Connection closed")]
|
||||
#[error("Sqlite Error: {0:?}")]
|
||||
Sql(#[from] rusqlite::Error),
|
||||
#[error("Sqlite Connection Pool Error: {0:?}")]
|
||||
ConnectionPool(#[from] r2d2::Error),
|
||||
#[error("Sqlite: Connection closed")]
|
||||
SqlNoConnection,
|
||||
#[fail(display = "Sqlite: Already open")]
|
||||
#[error("Sqlite: Already open")]
|
||||
SqlAlreadyOpen,
|
||||
#[fail(display = "Sqlite: Failed to open")]
|
||||
#[error("Sqlite: Failed to open")]
|
||||
SqlFailedToOpen,
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Io(#[cause] std::io::Error),
|
||||
#[fail(display = "{:?}", _0)]
|
||||
BlobError(#[cause] crate::blob::BlobError),
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0:?}")]
|
||||
BlobError(#[from] crate::blob::BlobError),
|
||||
#[error("{0}")]
|
||||
Other(#[from] crate::error::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
fn from(err: rusqlite::Error) -> Error {
|
||||
Error::Sql(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<r2d2::Error> for Error {
|
||||
fn from(err: r2d2::Error) -> Error {
|
||||
Error::ConnectionPool(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::blob::BlobError> for Error {
|
||||
fn from(err: crate::blob::BlobError) -> Error {
|
||||
Error::BlobError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around the underlying Sqlite3 object.
|
||||
#[derive(DebugStub)]
|
||||
pub struct Sql {
|
||||
@@ -96,7 +74,7 @@ impl Sql {
|
||||
pub fn open(&self, context: &Context, dbfile: &std::path::Path, readonly: bool) -> bool {
|
||||
match open(context, self, dbfile, readonly) {
|
||||
Ok(_) => true,
|
||||
Err(crate::error::Error::SqlError(Error::SqlAlreadyOpen)) => false,
|
||||
Err(Error::SqlAlreadyOpen) => false,
|
||||
Err(_) => {
|
||||
self.close(context);
|
||||
false
|
||||
@@ -212,6 +190,30 @@ impl Sql {
|
||||
self.with_conn(|conn| conn.query_row(sql.as_ref(), params, f).map_err(Into::into))
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or one row.
|
||||
pub fn query_row_optional<T, P, F>(
|
||||
&self,
|
||||
sql: impl AsRef<str>,
|
||||
params: P,
|
||||
f: F,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
P: IntoIterator,
|
||||
P::Item: rusqlite::ToSql,
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
{
|
||||
match self.query_row(sql, params, f) {
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
|
||||
_,
|
||||
_,
|
||||
rusqlite::types::Type::Null,
|
||||
))) => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn table_exists(&self, name: impl AsRef<str>) -> bool {
|
||||
self.with_conn(|conn| table_exists(conn, name))
|
||||
.unwrap_or_default()
|
||||
@@ -226,16 +228,7 @@ impl Sql {
|
||||
P::Item: rusqlite::ToSql,
|
||||
T: rusqlite::types::FromSql,
|
||||
{
|
||||
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
|
||||
_,
|
||||
_,
|
||||
rusqlite::types::Type::Null,
|
||||
))) => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
self.query_row_optional(query, params, |row| row.get::<_, T>(0))
|
||||
}
|
||||
|
||||
/// Not resultified version of `query_get_value_result`. Returns
|
||||
@@ -249,7 +242,7 @@ impl Sql {
|
||||
match self.query_get_value_result(query, params) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
error!(context, "sql: Failed query_row: {}", err);
|
||||
warn!(context, "sql: Failed query_row: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -388,14 +381,14 @@ fn open(
|
||||
sql: &Sql,
|
||||
dbfile: impl AsRef<std::path::Path>,
|
||||
readonly: bool,
|
||||
) -> crate::error::Result<()> {
|
||||
) -> Result<()> {
|
||||
if sql.is_open() {
|
||||
error!(
|
||||
context,
|
||||
"Cannot open, database \"{:?}\" already opened.",
|
||||
dbfile.as_ref(),
|
||||
);
|
||||
return Err(Error::SqlAlreadyOpen.into());
|
||||
return Err(Error::SqlAlreadyOpen);
|
||||
}
|
||||
|
||||
let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
|
||||
@@ -405,6 +398,10 @@ fn open(
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
|
||||
}
|
||||
|
||||
// this actually creates min_idle database handles just now.
|
||||
// therefore, with_init() must not try to modify the database as otherwise
|
||||
// we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle)
|
||||
let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile.as_ref())
|
||||
.with_flags(open_flags)
|
||||
.with_init(|c| c.execute_batch("PRAGMA secure_delete=on;"));
|
||||
@@ -420,6 +417,17 @@ fn open(
|
||||
}
|
||||
|
||||
if !readonly {
|
||||
// journal_mode is persisted, it is sufficient to change it only for one handle.
|
||||
// (nb: execute() always returns errors for this PRAGMA call, just discard it.
|
||||
// but even if execute() would handle errors more gracefully, we should continue on errors -
|
||||
// systems might not be able to handle WAL, in which case the standard-journal is used.
|
||||
// that may be not optimal, but better than not working at all :)
|
||||
|
||||
// some tests are failing reproducible with journal_mode=WAL but pass with journal_mode=DELETE -
|
||||
// until we have more details on that or the tests pass again, we disable WAL.
|
||||
// as no core was shipped with WAL, there is no need to disable it explicitly.
|
||||
//sql.execute("PRAGMA journal_mode=WAL;", NO_PARAMS).ok();
|
||||
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update = 0;
|
||||
/* Init tables to dbversion=0 */
|
||||
@@ -545,7 +553,7 @@ fn open(
|
||||
dbfile.as_ref(),
|
||||
);
|
||||
// cannot create the tables - maybe we cannot write?
|
||||
return Err(Error::SqlFailedToOpen.into());
|
||||
return Err(Error::SqlFailedToOpen);
|
||||
} else {
|
||||
sql.set_raw_config_int(context, "dbversion", 0)?;
|
||||
}
|
||||
@@ -1053,6 +1061,18 @@ pub fn get_rowid2_with_conn(
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes from the database locally deleted messages that also don't
|
||||
/// have a server UID.
|
||||
fn prune_tombstones(context: &Context) -> Result<()> {
|
||||
context.sql.execute(
|
||||
"DELETE FROM msgs \
|
||||
WHERE (chat_id = ? OR hidden) \
|
||||
AND server_uid = 0",
|
||||
params![DC_CHAT_ID_TRASH],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn housekeeping(context: &Context) {
|
||||
let mut files_in_use = HashSet::new();
|
||||
let mut unreferenced_count = 0;
|
||||
@@ -1165,6 +1185,13 @@ pub fn housekeeping(context: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = prune_tombstones(context) {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: Cannot prune message tombstones: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
info!(context, "Housekeeping done.",);
|
||||
}
|
||||
|
||||
|
||||
22
src/stock.rs
22
src/stock.rs
@@ -10,7 +10,7 @@ use crate::chat;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::message::Message;
|
||||
use crate::param::Param;
|
||||
use crate::stock::StockMessage::{DeviceMessagesHint, WelcomeMessage};
|
||||
@@ -35,12 +35,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Draft"))]
|
||||
Draft = 3,
|
||||
|
||||
#[strum(props(fallback = "%1$s member(s)"))]
|
||||
Member = 4,
|
||||
|
||||
#[strum(props(fallback = "%1$s contact(s)"))]
|
||||
Contact = 6,
|
||||
|
||||
#[strum(props(fallback = "Voice message"))]
|
||||
VoiceMessage = 7,
|
||||
|
||||
@@ -136,9 +130,6 @@ pub enum StockMessage {
|
||||
))]
|
||||
AcSetupMsgBody = 43,
|
||||
|
||||
#[strum(props(fallback = "Messages I sent to myself"))]
|
||||
SelfTalkSubTitle = 50,
|
||||
|
||||
#[strum(props(fallback = "Cannot login as %1$s."))]
|
||||
CannotLogin = 60,
|
||||
|
||||
@@ -314,7 +305,7 @@ impl Context {
|
||||
from_id: u32,
|
||||
) -> String {
|
||||
let insert1 = if id == StockMessage::MsgAddMember || id == StockMessage::MsgDelMember {
|
||||
let contact_id = Contact::lookup_id_by_addr(self, param1.as_ref());
|
||||
let contact_id = Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown);
|
||||
if contact_id != 0 {
|
||||
Contact::get_by_id(self, contact_id)
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
@@ -430,8 +421,9 @@ mod tests {
|
||||
let t = dummy_context();
|
||||
// uses %1$s substitution
|
||||
assert_eq!(
|
||||
t.ctx.stock_string_repl_str(StockMessage::Member, "42"),
|
||||
"42 member(s)"
|
||||
t.ctx
|
||||
.stock_string_repl_str(StockMessage::MsgAddMember, "Foo"),
|
||||
"Member Foo added."
|
||||
);
|
||||
// We have no string using %1$d to test...
|
||||
}
|
||||
@@ -440,8 +432,8 @@ mod tests {
|
||||
fn test_stock_string_repl_int() {
|
||||
let t = dummy_context();
|
||||
assert_eq!(
|
||||
t.ctx.stock_string_repl_int(StockMessage::Member, 42),
|
||||
"42 member(s)"
|
||||
t.ctx.stock_string_repl_int(StockMessage::MsgAddMember, 42),
|
||||
"Member 42 added."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,14 +42,14 @@ pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContex
|
||||
/// specified in [test_context] but there is no callback hooked up,
|
||||
/// i.e. [Context::call_cb] will always return `0`.
|
||||
pub(crate) fn dummy_context() -> TestContext {
|
||||
test_context(None)
|
||||
test_context(Some(Box::new(logging_cb)))
|
||||
}
|
||||
|
||||
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
|
||||
match evt {
|
||||
Event::Info(msg) => println!("I: {}", msg),
|
||||
Event::Warning(msg) => println!("W: {}", msg),
|
||||
Event::Error(msg) => println!("E: {}", msg),
|
||||
Event::Warning(msg) => eprintln!("=== WARNING ===\n{}\n===============", msg),
|
||||
Event::Error(msg) => eprintln!("\n===================== ERROR =====================\n{}\n=================================================\n", msg),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
BIN
test-data/image/avatar64x64.png
Normal file
BIN
test-data/image/avatar64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -44,9 +44,9 @@ fn stress_functions(context: &Context) {
|
||||
// assert!(dc_is_configured(context) != 0, "Missing configured context");
|
||||
|
||||
// let setupcode = dc_create_setup_code(context);
|
||||
// let setupcode_c = CString::yolo(setupcode.clone());
|
||||
// let setupcode_c = CString::new(setupcode.clone()).unwrap();
|
||||
// let setupfile = dc_render_setup_file(context, &setupcode).unwrap();
|
||||
// let setupfile_c = CString::yolo(setupfile);
|
||||
// let setupfile_c = CString::new(setupfile).unwrap();
|
||||
// let mut headerline_2: *const libc::c_char = ptr::null();
|
||||
// let payload = dc_decrypt_setup_file(context, setupcode_c.as_ptr(), setupfile_c.as_ptr());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user