mirror of
https://github.com/chatmail/core.git
synced 2026-08-18 06:36:29 +03:00
Compare commits
1 Commits
py-1.80.0
...
futures-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7db65afd8 |
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,58 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 1.80.0
|
||||
|
||||
### Changes
|
||||
- update provider database #3284
|
||||
- improve python bindings, tests and ci #3287 #3286 #3287 #3289 #3290 #3292
|
||||
|
||||
### Fixes
|
||||
- fix escaping in generated QR-code-SVG #3295
|
||||
|
||||
|
||||
## 1.79.0
|
||||
|
||||
### Changes
|
||||
- Send locations in the background regardless of SMTP loop activity #3247
|
||||
- refactorings #3268
|
||||
- improve tests and ci #3266 #3271
|
||||
|
||||
### Fixes
|
||||
- simplify `dc_stop_io()` and remove potential panics and race conditions #3273
|
||||
- fix correct message escaping consisting of a dot in SMTP protocol #3265
|
||||
|
||||
|
||||
## 1.78.0
|
||||
## Unreleased
|
||||
|
||||
### API-Changes
|
||||
|
||||
- replaced stock string `DC_STR_ONE_MOMENT` by `DC_STR_NOT_CONNECTED` #3222
|
||||
- add `dc_resend_msgs()` #3238
|
||||
- `dc_provider_new_from_email()` does no longer do an DNS lookup for checking custom domains,
|
||||
this is done by `dc_provider_new_from_email_with_dns()` now #3256
|
||||
|
||||
### Fixes
|
||||
|
||||
- Take `delete_device_after` into account when calculating ephemeral loop timeout #3211
|
||||
- Fix a bug where a blocked contact could send a contact request #3218
|
||||
- Make sure, videochat-room-names are always URL-safe #3231
|
||||
- Try removing account folder multiple times in case of failure #3229
|
||||
- Ignore messages from all spam folders if there are many #3246
|
||||
|
||||
### Changes
|
||||
- introduce multiple self addresses with the "configured" address always being the primary one #2896
|
||||
|
||||
- Further improve finding the correct server after logging in #3208
|
||||
- `get_connectivity_html()` returns HTML as non-scalable #3213
|
||||
- add update-serial to `DC_EVENT_WEBXDC_STATUS_UPDATE` #3215
|
||||
- Speed up message receiving via IMAP a bit #3225
|
||||
- mark messages as seen on IMAP in batches #3223
|
||||
- remove Received: based draft detection heuristic #3230
|
||||
- Use pkgconfig for building Python package #2590
|
||||
- don't start io on unconfigured context #2664
|
||||
- do not assign group IDs to ad-hoc groups #2798
|
||||
- dynamic libraries use dylib extension on Darwin #3226
|
||||
- refactorings #3217 #3219 #3224 #3235 #3239 #3244 #3254
|
||||
- improve documentation #3214 #3220 #3237
|
||||
- improve tests and ci #3212 #3233 #3241 #3242 #3252 #3250 #3255 #3260
|
||||
|
||||
### Fixes
|
||||
- Take `delete_device_after` into account when calculating ephemeral loop timeout #3211 #3221
|
||||
- Fix a bug where a blocked contact could send a contact request #3218
|
||||
- Make sure, videochat-room-names are always URL-safe #3231
|
||||
- Try removing account folder multiple times in case of failure #3229
|
||||
- Ignore messages from all spam folders if there are many #3246
|
||||
- Hide location-only messages instead of displaying empty bubbles #3248
|
||||
|
||||
|
||||
## 1.77.0
|
||||
|
||||
138
Cargo.lock
generated
138
Cargo.lock
generated
@@ -114,9 +114,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.57"
|
||||
version = "1.0.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||
checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
@@ -210,7 +210,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/async-email/async-imap#d07ea4019f2ca724244f7a4503a8ac4946a722b3"
|
||||
source = "git+https://github.com/async-email/async-imap#7ddd1c1c7d5013a4b3369235f9f9bc233e9d7398"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
@@ -297,7 +297,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/async-email/async-smtp?branch=master#0cbf7043923b06612ce0dea7a3a3dae5ceaef578"
|
||||
source = "git+https://github.com/async-email/async-smtp?branch=master#3e7a8f3de19fdeb19d14cf123a8696c49685d3fa"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
@@ -421,24 +421,24 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.65"
|
||||
version = "0.3.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
|
||||
checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.4.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base-x"
|
||||
version = "0.2.10"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc19a4937b4fbd3fe3379793130e42060d10627a360f2127802b10b87e7baf74"
|
||||
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
@@ -711,9 +711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clear_on_drop"
|
||||
version = "0.2.5"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38508a63f4979f0048febc9966fadbd48e5dab31fd0ec6a3f151bbf4a74f7423"
|
||||
checksum = "c9cc5db465b294c3fa986d5bbb0f3017cd850bff6dd6c52f9ccff8b4d21b7b08"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -1067,7 +1067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.80.0"
|
||||
version = "1.77.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1090,7 +1090,6 @@ dependencies = [
|
||||
"escaper",
|
||||
"fast-socks5",
|
||||
"futures",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"humansize",
|
||||
"image",
|
||||
@@ -1146,7 +1145,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.80.0"
|
||||
version = "1.77.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1531,9 +1530,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.16"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
|
||||
checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
@@ -1543,14 +1542,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.23"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
|
||||
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crc32fast",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1754,9 +1753,9 @@ checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.4"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
|
||||
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1892,9 +1891,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.7.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
|
||||
checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4"
|
||||
|
||||
[[package]]
|
||||
name = "human-panic"
|
||||
@@ -1945,9 +1944,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.2"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212"
|
||||
checksum = "db207d030ae38f1eb6f240d5a1c1c88ff422aa005d10f8c6c6fc5e75286ab30e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
@@ -2004,9 +2003,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.5.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
|
||||
checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
@@ -2116,9 +2115,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.124"
|
||||
version = "0.2.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
|
||||
checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2146,9 +2145,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.0.46"
|
||||
version = "0.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
|
||||
checksum = "5284f00d480e1c39af34e72f8ad60b94f47007e3481cd3b731c1d67190ddc7b7"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -2244,6 +2243,16 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"autocfg 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.1"
|
||||
@@ -2386,9 +2395,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.43"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
||||
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"num-integer",
|
||||
@@ -2427,9 +2436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.28.3"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
|
||||
checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2679,9 +2688,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@@ -2732,7 +2741,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"deflate",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2833,9 +2842,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "qrcodegen"
|
||||
version = "1.8.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
|
||||
checksum = "135e6754eed8ca897dd70584d895e72e36860b3e163b6bcedce48571cbaef343"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
@@ -2860,9 +2869,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||
checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2993,9 +3002,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.5.2"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221"
|
||||
checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"crossbeam-deque",
|
||||
@@ -3005,13 +3014,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.9.2"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4"
|
||||
checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"lazy_static",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
@@ -3157,9 +3167,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.34.5"
|
||||
version = "0.34.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d275ec42d7bd48e2863912dd8075a4d7376c6f1433b922a0175578b5c7725ce"
|
||||
checksum = "96619609a54d638872db136f56941d34e2a00bb0acf3fa783a90d6b96a093ba2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@@ -3589,13 +3599,11 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
|
||||
|
||||
[[package]]
|
||||
name = "stop-token"
|
||||
version = "0.7.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
|
||||
checksum = "2d5e0f9b25bdd9bf18e33d1f4dd0194c77034b66b4a4842b0bf7f5cf53ca733a"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"cfg-if 1.0.0",
|
||||
"futures-core",
|
||||
"async-std",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -3660,9 +3668,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.92"
|
||||
version = "1.0.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52"
|
||||
checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3817,9 +3825,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
@@ -3832,18 +3840,18 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.18.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f48b6d60512a392e34dbf7fd456249fd2de3c83669ab642e021903f4015185b"
|
||||
checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -3928,9 +3936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.8"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
@@ -4142,9 +4150,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.6"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4"
|
||||
checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-ffi"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.80.0"
|
||||
version = "1.77.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -80,7 +80,6 @@ zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = { version = "0.3.4", features = ["async_std"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.80.0"
|
||||
version = "1.77.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1801,25 +1801,6 @@ void dc_delete_msgs (dc_context_t* context, const uint3
|
||||
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Resend messages and make information available for newly added chat members.
|
||||
* Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
* Clients that already have the original message can still ignore the resent message as
|
||||
* they have tracked the state by dedicated updates.
|
||||
*
|
||||
* Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
|
||||
* In this case, the return value indicates an error and the error string from dc_get_last_error() should be shown to the user.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_ids An array of uint32_t containing all message IDs that should be resend.
|
||||
* All messages must belong to the same chat.
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||
* @return 1=all messages are queued for resending, 0=error
|
||||
*/
|
||||
int dc_resend_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Mark messages as presented to the user.
|
||||
* Typically, UIs call this function on scrolling through the message list,
|
||||
@@ -4648,10 +4629,9 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
* Create a provider struct for the given e-mail address.
|
||||
*
|
||||
* Lookup is done from the local database by extracting the domain from the e-mail address.
|
||||
* Therefore the provider for custom domains cannot be identified.
|
||||
* The provider is extracted from the e-mail address and it's information is returned.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
@@ -4663,23 +4643,6 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local and DNS lookup.
|
||||
*
|
||||
* First lookup is done from the local database as of dc_provider_new_from_email().
|
||||
* If the first lookup fails, an additional DNS lookup is done,
|
||||
* trying to figure out the provider belonging to custom domains.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
* @param email The user's e-mail address to extract the provider info form.
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
* URL of the overview page.
|
||||
*
|
||||
|
||||
@@ -657,7 +657,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_stop_io(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_stop_io()");
|
||||
eprintln!("ignoring careless call to dc_shutdown()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
@@ -1751,27 +1751,6 @@ pub unsafe extern "C" fn dc_forward_msgs(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_resend_msgs(
|
||||
context: *mut dc_context_t,
|
||||
msg_ids: *const u32,
|
||||
msg_cnt: libc::c_int,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
|
||||
eprintln!("ignoring careless call to dc_resend_msgs()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
|
||||
error!(ctx, "Resending failed: {}", err);
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_markseen_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3973,25 +3952,6 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
}
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
context: *const dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
if context.is_null() || addr.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_new_from_email_with_dns()");
|
||||
return ptr::null();
|
||||
}
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
let socks5_enabled = block_on(async move {
|
||||
ctx.get_config_bool(config::Config::Socks5Enabled)
|
||||
|
||||
@@ -399,7 +399,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
html <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
resend <msg-id>\n\
|
||||
markseen <msg-id>\n\
|
||||
delmsg <msg-id>\n\
|
||||
===========================Contact commands==\n\
|
||||
@@ -1100,13 +1099,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
chat::forward_msgs(&context, &msg_ids, chat_id).await?;
|
||||
}
|
||||
"resend" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments <msg-id> expected");
|
||||
|
||||
let mut msg_ids = [MsgId::new(0); 1];
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
chat::resend_msgs(&context, &msg_ids).await?;
|
||||
}
|
||||
"markseen" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut msg_ids = vec![MsgId::new(0)];
|
||||
|
||||
@@ -207,12 +207,11 @@ const CHAT_COMMANDS: [&str; 36] = [
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
const MESSAGE_COMMANDS: [&str; 7] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"resend",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
ac1, = acfactory.get_online_accounts(1)
|
||||
ac1 = acfactory.get_one_online_account()
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
@@ -42,7 +42,7 @@ 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_online_accounts(2)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
|
||||
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_configure_completed*
|
||||
|
||||
@@ -17,7 +17,3 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-_pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-imap_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
|
||||
@@ -11,11 +11,8 @@ def main():
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imap-tools', 'requests'],
|
||||
setup_requires=[
|
||||
'setuptools_scm', # required for compatibility with `python3 setup.py sdist`
|
||||
'pkgconfig',
|
||||
],
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
|
||||
from . import capi, const, hookspec # noqa
|
||||
from .capi import ffi # noqa
|
||||
from .account import Account, get_core_info # noqa
|
||||
from .account import Account # noqa
|
||||
from .message import Message # noqa
|
||||
from .contact import Contact # noqa
|
||||
from .chat import Chat # noqa
|
||||
|
||||
@@ -8,9 +8,9 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
import types
|
||||
|
||||
import cffi
|
||||
import pkgconfig # type: ignore
|
||||
|
||||
|
||||
def local_build_flags(projdir, target):
|
||||
@@ -19,31 +19,36 @@ def local_build_flags(projdir, target):
|
||||
:param projdir: The root directory of the deltachat-core-rust project.
|
||||
:param target: The rust build target, `debug` or `release`.
|
||||
"""
|
||||
flags = {}
|
||||
flags = types.SimpleNamespace()
|
||||
if platform.system() == 'Darwin':
|
||||
flags['libraries'] = ['resolv', 'dl']
|
||||
flags['extra_link_args'] = [
|
||||
flags.libs = ['resolv', 'dl']
|
||||
flags.extra_link_args = [
|
||||
'-framework', 'CoreFoundation',
|
||||
'-framework', 'CoreServices',
|
||||
'-framework', 'Security',
|
||||
]
|
||||
elif platform.system() == 'Linux':
|
||||
flags['libraries'] = ['rt', 'dl', 'm']
|
||||
flags['extra_link_args'] = []
|
||||
flags.libs = ['rt', 'dl', 'm']
|
||||
flags.extra_link_args = []
|
||||
else:
|
||||
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
||||
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
||||
if target_dir is None:
|
||||
target_dir = os.path.join(projdir, 'target')
|
||||
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects']
|
||||
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(flags.objs[0]), flags.objs
|
||||
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
return flags
|
||||
|
||||
|
||||
def system_build_flags():
|
||||
"""Construct build flags for building against an installed libdeltachat."""
|
||||
return pkgconfig.parse('deltachat')
|
||||
flags = types.SimpleNamespace()
|
||||
flags.libs = ['deltachat']
|
||||
flags.objs = []
|
||||
flags.incs = []
|
||||
flags.extra_link_args = []
|
||||
return flags
|
||||
|
||||
|
||||
def extract_functions(flags):
|
||||
@@ -64,7 +69,7 @@ def extract_functions(flags):
|
||||
src_fp.write('#include <deltachat.h>')
|
||||
cc.preprocess(source=src_name,
|
||||
output_file=dst_name,
|
||||
include_dirs=flags['include_dirs'],
|
||||
include_dirs=flags.incs,
|
||||
macros=[('PY_CFFI', '1')])
|
||||
with open(dst_name, "r") as dst_fp:
|
||||
return dst_fp.read()
|
||||
@@ -100,7 +105,7 @@ def find_header(flags):
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
cc.compile(sources=["where.c"],
|
||||
include_dirs=flags['include_dirs'],
|
||||
include_dirs=flags.incs,
|
||||
macros=[("PY_CFFI_INC", "1")])
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
@@ -178,7 +183,10 @@ def ffibuilder():
|
||||
return DC_EVENT_DATA2_IS_STRING(e);
|
||||
}
|
||||
""",
|
||||
**flags,
|
||||
include_dirs=flags.incs,
|
||||
libraries=flags.libs,
|
||||
extra_objects=flags.objs,
|
||||
extra_link_args=flags.extra_link_args,
|
||||
)
|
||||
builder.cdef("""
|
||||
typedef int... time_t;
|
||||
|
||||
@@ -22,29 +22,6 @@ class MissingCredentials(ValueError):
|
||||
""" Account is missing `addr` and `mail_pw` config values. """
|
||||
|
||||
|
||||
def get_core_info():
|
||||
""" get some system info. """
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile() as path:
|
||||
path.close()
|
||||
return get_dc_info_as_dict(ffi.gc(
|
||||
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
))
|
||||
|
||||
|
||||
def get_dc_info_as_dict(dc_context):
|
||||
lines = from_dc_charpointer(lib.dc_get_info(dc_context))
|
||||
info_dict = {}
|
||||
for line in lines.split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
info_dict[key.lower()] = value
|
||||
return info_dict
|
||||
|
||||
|
||||
class Account(object):
|
||||
""" Each account is tied to a sqlite database file which is fully managed
|
||||
by the underlying deltachat core library. All public Account methods are
|
||||
@@ -90,9 +67,6 @@ class Account(object):
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
def __repr__(self):
|
||||
return "<Account path={}>".format(self.db_path)
|
||||
|
||||
# def __del__(self):
|
||||
# self.shutdown()
|
||||
|
||||
@@ -107,7 +81,14 @@ class Account(object):
|
||||
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
return get_dc_info_as_dict(self._dc_context)
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
for line in lines.split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
d[key.lower()] = value
|
||||
return d
|
||||
|
||||
def dump_account_info(self, logfile):
|
||||
def log(*args, **kwargs):
|
||||
@@ -148,8 +129,6 @@ class Account(object):
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if isinstance(value, (int, bool)):
|
||||
value = str(int(value))
|
||||
if value is not None:
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
@@ -190,7 +169,7 @@ class Account(object):
|
||||
:returns: None
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, value)
|
||||
self.set_config(key, str(value))
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
""" determine if the account is configured already; an initial connection
|
||||
@@ -590,8 +569,6 @@ class Account(object):
|
||||
""" add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
"""
|
||||
if name and self._pm.has_plugin(name=name):
|
||||
self._pm.unregister(name=name)
|
||||
self._pm.register(plugin, name=name)
|
||||
self._pm.check_pending()
|
||||
return plugin
|
||||
@@ -695,25 +672,28 @@ class Account(object):
|
||||
if self._dc_context is None:
|
||||
return
|
||||
|
||||
# mark the event thread for shutdown (latest on next incoming event)
|
||||
self._event_thread.mark_shutdown()
|
||||
|
||||
# stop_io also causes an info event which will wake up
|
||||
# the EventThread's inner loop and let it notice the shutdown marker.
|
||||
self.stop_io()
|
||||
|
||||
self.log("remove dc_context references")
|
||||
|
||||
# if _dc_context is unref'ed the event thread should quickly
|
||||
# receive the termination signal. However, some python code might
|
||||
# still hold a reference and so we use a secondary signal
|
||||
# to make sure the even thread terminates if it receives any new
|
||||
# event, indepedently from waiting for the core to send NULL to
|
||||
# get_next_event().
|
||||
self._event_thread.mark_shutdown()
|
||||
self._dc_context = None
|
||||
|
||||
self.log("wait for event thread to finish")
|
||||
try:
|
||||
self._event_thread.wait(timeout=5)
|
||||
self._event_thread.wait(timeout=2)
|
||||
except RuntimeError as e:
|
||||
self.log("Waiting for event thread failed: {}".format(e))
|
||||
|
||||
if self._event_thread.is_alive():
|
||||
self.log("WARN: event thread did not terminate yet, ignoring.")
|
||||
|
||||
self.log("remove dc_context references, making the Account unusable")
|
||||
self._dc_context = None
|
||||
|
||||
self._shutdown_event.set()
|
||||
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
|
||||
@@ -4,20 +4,61 @@ and for cleaning up inbox/mvbox for each test function run.
|
||||
"""
|
||||
|
||||
import io
|
||||
import email
|
||||
import ssl
|
||||
import pathlib
|
||||
from contextlib import contextmanager
|
||||
from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage
|
||||
from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const, Account
|
||||
from typing import List
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
DELETED = b'\\Deleted'
|
||||
FLAGS = b'FLAGS'
|
||||
FETCH = b'FETCH'
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_extra_configure(account):
|
||||
""" Reset the account (we reuse accounts across tests)
|
||||
and make 'account.direct_imap' available for direct IMAP ops.
|
||||
"""
|
||||
try:
|
||||
|
||||
if not hasattr(account, "direct_imap"):
|
||||
imap = DirectImap(account)
|
||||
|
||||
for folder in imap.list_folders():
|
||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
||||
assert imap.select_folder(folder)
|
||||
imap.delete(ALL, expunge=True)
|
||||
else:
|
||||
imap.conn.delete_folder(folder)
|
||||
# We just deleted the folder, so we have to make DC forget about it, too
|
||||
if account.get_config("configured_sentbox_folder") == folder:
|
||||
account.set_config("configured_sentbox_folder", None)
|
||||
|
||||
setattr(account, "direct_imap", imap)
|
||||
|
||||
except Exception as e:
|
||||
# Uncaught exceptions here would lead to a timeout without any note written to the log
|
||||
# start with DC_EVENT_WARNING so that the line is printed in yellow and won't be overlooked when reading
|
||||
account.log("DC_EVENT_WARNING =================== DIRECT_IMAP CAN'T RESET ACCOUNT: ===================")
|
||||
account.log("DC_EVENT_WARNING =================== " + str(e) + " ===================")
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_after_shutdown(account):
|
||||
""" shutdown the imap connection if there is one. """
|
||||
imap = getattr(account, "direct_imap", None)
|
||||
if imap is not None:
|
||||
imap.shutdown()
|
||||
del account.direct_imap
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
@@ -45,30 +86,37 @@ class DirectImap:
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.DC_SOCKET_STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn = IMAPClient(host, port, ssl=False)
|
||||
self.conn.starttls(ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN:
|
||||
self.conn = IMAPClient(host, port, ssl=False)
|
||||
elif security == const.DC_SOCKET_SSL:
|
||||
self.conn = IMAPClient(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.conn.idle_done()
|
||||
except (OSError, IMAPClientError):
|
||||
pass
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
except (OSError, IMAPClientError):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
self.conn.create_folder(foldername)
|
||||
except imaplib.IMAP4.error as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
def select_folder(self, foldername):
|
||||
assert not self._idling
|
||||
return self.conn.folder.set(foldername)
|
||||
return self.conn.select_folder(foldername)
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
def select_config_folder(self, config_name):
|
||||
""" Return info about selected folder if it is
|
||||
configured, otherwise None. """
|
||||
if "_" not in config_name:
|
||||
@@ -77,36 +125,50 @@ class DirectImap:
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
|
||||
def list_folders(self) -> List[str]:
|
||||
def list_folders(self):
|
||||
""" return list of all existing folder names"""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
folders = []
|
||||
for meta, sep, foldername in self.conn.list_folders():
|
||||
folders.append(foldername)
|
||||
return folders
|
||||
|
||||
def delete(self, uid_list: str, expunge=True):
|
||||
def delete(self, range, expunge=True):
|
||||
""" delete a range of messages (imap-syntax).
|
||||
If expunge is true, perform the expunge-operation
|
||||
to make sure the messages are really gone and not
|
||||
just flagged as deleted.
|
||||
"""
|
||||
self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)')
|
||||
self.conn.set_flags(range, [DELETED])
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
def get_all_messages(self) -> List[MailMessage]:
|
||||
def get_all_messages(self):
|
||||
assert not self._idling
|
||||
return [mail for mail in self.conn.fetch()]
|
||||
|
||||
def get_unread_messages(self) -> List[str]:
|
||||
# Flush unsolicited responses. IMAPClient has problems
|
||||
# dealing with them: https://github.com/mjs/imapclient/issues/334
|
||||
# When this NOOP was introduced, next FETCH returned empty
|
||||
# result instead of a single message, even though IMAP server
|
||||
# can only return more untagged responses than required, not
|
||||
# less.
|
||||
self.conn.noop()
|
||||
|
||||
return self.conn.fetch(ALL, [FLAGS])
|
||||
|
||||
def get_unread_messages(self):
|
||||
assert not self._idling
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
res = self.conn.fetch(ALL, [FLAGS])
|
||||
return [uid for uid in res
|
||||
if SEEN not in res[uid][FLAGS]]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
res = self.conn.set_flags(messages, [SEEN])
|
||||
print("marked seen:", messages, res)
|
||||
|
||||
def get_unread_cnt(self) -> int:
|
||||
def get_unread_cnt(self):
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
@@ -128,81 +190,83 @@ class DirectImap:
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
for msg in self.conn.fetch(mark_seen=False):
|
||||
body = getattr(msg.obj, "text", None)
|
||||
if not body:
|
||||
body = getattr(msg.obj, "html", None)
|
||||
if not body:
|
||||
log("Message", msg.uid, "has empty body")
|
||||
requested = [b'BODY.PEEK[]', FLAGS]
|
||||
for uid, data in self.conn.fetch(messages, requested).items():
|
||||
body_bytes = data[b'BODY[]']
|
||||
if not body_bytes:
|
||||
log("Message", uid, "has empty body")
|
||||
continue
|
||||
|
||||
flags = data[FLAGS]
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
fn = path.joinpath(str(msg.uid))
|
||||
fn.write_bytes(body)
|
||||
log("Message", msg.uid, fn)
|
||||
log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id"))
|
||||
fn = path.joinpath(str(uid))
|
||||
fn.write_bytes(body_bytes)
|
||||
log("Message", uid, fn)
|
||||
email_message = email.message_from_bytes(body_bytes)
|
||||
log("Message", uid, flags, "Message-Id:", email_message.get("Message-Id"))
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
|
||||
print(stream.getvalue(), file=logfile)
|
||||
|
||||
@contextmanager
|
||||
def idle(self):
|
||||
""" return Idle ContextManager. """
|
||||
idle_manager = IdleManager(self)
|
||||
try:
|
||||
yield idle_manager
|
||||
finally:
|
||||
idle_manager.done()
|
||||
def idle_start(self):
|
||||
""" switch this connection to idle mode. non-blocking. """
|
||||
assert not self._idling
|
||||
res = self.conn.idle()
|
||||
self._idling = True
|
||||
return res
|
||||
|
||||
def append(self, folder: str, msg: str):
|
||||
def idle_check(self, terminate=False):
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
assert self._idling
|
||||
self.account.log("imap-direct: calling idle_check")
|
||||
res = self.conn.idle_check()
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
self.account.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
return res
|
||||
|
||||
def idle_wait_for_new_message(self, terminate=False):
|
||||
while 1:
|
||||
for item in self.idle_check():
|
||||
if item[1] in (b'EXISTS', b'RECENT'):
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
return item
|
||||
|
||||
def idle_wait_for_seen(self, terminate=False):
|
||||
""" Return first message with SEEN flag
|
||||
from a running idle-stream REtiurn.
|
||||
"""
|
||||
while 1:
|
||||
for item in self.idle_check():
|
||||
if item[1] == FETCH:
|
||||
if item[2][0] == FLAGS:
|
||||
if SEEN in item[2][1]:
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
return item[0]
|
||||
|
||||
def idle_done(self):
|
||||
""" send idle-done to server if we are currently in idle mode. """
|
||||
if self._idling:
|
||||
res = self.conn.idle_done()
|
||||
self._idling = False
|
||||
return res
|
||||
|
||||
def append(self, folder, msg):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(bytes(msg, encoding='ascii'), folder)
|
||||
self.conn.append(folder, msg)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header('MESSAGE-ID', message_id)))]
|
||||
def get_uid_by_message_id(self, message_id):
|
||||
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap):
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> List[bytes]:
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
return res
|
||||
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
while 1:
|
||||
for item in self.check(timeout=timeout):
|
||||
if b'EXISTS' in item or b'RECENT' in item:
|
||||
return item
|
||||
|
||||
def wait_for_seen(self, timeout=None) -> int:
|
||||
""" Return first message with SEEN flag from a running idle-stream.
|
||||
"""
|
||||
while 1:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb'\Seen' in item:
|
||||
return int(item.split(b' ')[1])
|
||||
|
||||
def done(self):
|
||||
""" send idle-done to server if we are currently in idle mode. """
|
||||
res = self.direct_imap.conn.idle.stop()
|
||||
return res
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import threading
|
||||
import sys
|
||||
import traceback
|
||||
import time
|
||||
import io
|
||||
import re
|
||||
import os
|
||||
from queue import Queue, Empty
|
||||
@@ -32,12 +29,10 @@ class FFIEventLogger:
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account, logid=None, init_time=None) -> None:
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
self.logid = logid or self.account.get_config("displayname")
|
||||
if init_time is None:
|
||||
init_time = time.time()
|
||||
self.init_time = init_time
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
||||
@@ -161,14 +156,14 @@ class FFIEventTracker:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
|
||||
break
|
||||
|
||||
def wait_idle_inbox_ready(self):
|
||||
def wait_all_initial_fetches(self):
|
||||
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
|
||||
so that new messages are not mistaken for old ones:
|
||||
- ac1 and ac2 are created
|
||||
- ac1 sends a message to ac2
|
||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent"""
|
||||
self.get_info_contains("INBOX: Idle entering")
|
||||
self.get_info_contains("Done fetching existing messages")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
""" wait for and return next incoming message. """
|
||||
@@ -230,7 +225,9 @@ class EventThread(threading.Thread):
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL or self._marked_for_shutdown:
|
||||
if event == ffi.NULL:
|
||||
break
|
||||
if self._marked_for_shutdown:
|
||||
break
|
||||
evt = lib.dc_event_get_id(event)
|
||||
data1 = lib.dc_event_get_data1_int(event)
|
||||
@@ -244,23 +241,15 @@ class EventThread(threading.Thread):
|
||||
|
||||
lib.dc_event_unref(event)
|
||||
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
||||
with self.swallow_and_log_exception("ac_process_ffi_event {}".format(ffi_event)):
|
||||
try:
|
||||
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
|
||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||
hook = getattr(self.account._pm.hook, name)
|
||||
info = "call {} kwargs={} failed".format(name, kwargs)
|
||||
with self.swallow_and_log_exception(info):
|
||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
|
||||
hook = getattr(self.account._pm.hook, name)
|
||||
hook(**kwargs)
|
||||
|
||||
@contextmanager
|
||||
def swallow_and_log_exception(self, info):
|
||||
try:
|
||||
yield
|
||||
except Exception as ex:
|
||||
logfile = io.StringIO()
|
||||
traceback.print_exception(*sys.exc_info(), file=logfile)
|
||||
self.account.log("{}\nException {}\nTraceback:\n{}"
|
||||
.format(info, ex, logfile.getvalue()))
|
||||
except Exception:
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
|
||||
@@ -8,42 +8,35 @@ import threading
|
||||
import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
from queue import Queue
|
||||
from typing import List, Callable
|
||||
import tempfile
|
||||
from typing import List, Dict, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from . import Account, const, account_hookimpl, get_core_info
|
||||
from . import Account, const
|
||||
from .capi import lib
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
from _pytest._code import Source
|
||||
from deltachat import direct_imap
|
||||
|
||||
import deltachat
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
group.addoption(
|
||||
parser.addoption(
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
group.addoption(
|
||||
parser.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
group.addoption(
|
||||
parser.addoption(
|
||||
"--strict-tls", action="store_true",
|
||||
help="Never accept invalid TLS certificates for test accounts",
|
||||
)
|
||||
group.addoption(
|
||||
"--extra-info", action="store_true",
|
||||
help="show more info on failures (imap server state, config)"
|
||||
)
|
||||
group.addoption(
|
||||
"--debug-setup", action="store_true",
|
||||
help="show events during configure and start io phases of online accounts"
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -98,12 +91,9 @@ def pytest_configure(config):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
logging = item.config.getoption("--extra-info")
|
||||
if logging:
|
||||
self.enable_logging(item)
|
||||
self.enable_logging(item)
|
||||
yield
|
||||
if logging:
|
||||
self.disable_logging(item)
|
||||
self.disable_logging(item)
|
||||
|
||||
la = LoggingAspect()
|
||||
config.pluginmanager.register(la)
|
||||
@@ -111,12 +101,20 @@ def pytest_configure(config):
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
info = get_core_info()
|
||||
summary = ['Deltachat core={} sqlite={} journal_mode={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
info['journal_mode'],
|
||||
)]
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
try:
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
os.remove(t)
|
||||
summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
info['journal_mode'],
|
||||
)])
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
@@ -128,54 +126,57 @@ def pytest_report_header(config, startdir):
|
||||
return summary
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn) -> None:
|
||||
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: int):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
configlist: List[Dict[str, str]]
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index: int):
|
||||
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 with code {}: '{}'".format(res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def testprocess(request):
|
||||
return TestProcess(pytestconfig=request.config)
|
||||
|
||||
|
||||
class TestProcess:
|
||||
""" A pytest session-scoped instance to help with managing "live" account configurations.
|
||||
"""
|
||||
def __init__(self, pytestconfig):
|
||||
self.pytestconfig = pytestconfig
|
||||
|
||||
def get_liveconfig_producer(self):
|
||||
""" provide live account configs, cached on a per-test-process scope
|
||||
so that test functions can re-use already known live configs.
|
||||
Depending on the --liveconfig option this comes from
|
||||
a HTTP provider or a file with a line specifying each accounts config.
|
||||
"""
|
||||
liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
|
||||
if not liveconfig_opt:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts")
|
||||
|
||||
configlist = []
|
||||
if not liveconfig_opt.startswith("http"):
|
||||
for line in open(liveconfig_opt):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
configlist.append(d)
|
||||
|
||||
yield from iter(configlist)
|
||||
def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
else:
|
||||
MAX_LIVE_CREATED_ACCOUNTS = 10
|
||||
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
|
||||
try:
|
||||
yield configlist[index]
|
||||
except IndexError:
|
||||
res = requests.post(liveconfig_opt)
|
||||
if res.status_code != 200:
|
||||
pytest.fail("newtmpuser count={} code={}: '{}'".format(
|
||||
index, res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
configlist.append(config)
|
||||
yield config
|
||||
pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS))
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -208,320 +209,256 @@ def data(request):
|
||||
return Data()
|
||||
|
||||
|
||||
class ACSetup:
|
||||
""" accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases. From tests, use the higher level
|
||||
public ACFactory methods instead of its private helper class.
|
||||
"""
|
||||
CONFIGURING = "CONFIGURING"
|
||||
CONFIGURED = "CONFIGURED"
|
||||
IDLEREADY = "IDLEREADY"
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
def __init__(self, init_time):
|
||||
self._configured_events = Queue()
|
||||
self._account2state = {}
|
||||
self._imap_cleaned = set()
|
||||
self.init_time = init_time
|
||||
class AccountMaker:
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def start_configure(self, account, reconfigure=False):
|
||||
""" add an account and start its configure process. """
|
||||
class PendingTracker:
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(this, success):
|
||||
self._configured_events.put((account, success))
|
||||
|
||||
account.add_account_plugin(PendingTracker(), name="pending_tracker")
|
||||
self._account2state[account] = self.CONFIGURING
|
||||
account.configure(reconfigure=reconfigure)
|
||||
print("started configure on pending", account)
|
||||
|
||||
def wait_one_configured(self, account):
|
||||
""" wait until this account has successfully configured. """
|
||||
if self._account2state[account] == self.CONFIGURING:
|
||||
while 1:
|
||||
acc = self._pop_config_success()
|
||||
if acc == account:
|
||||
break
|
||||
self.init_direct_imap_and_logging(acc)
|
||||
acc._evtracker.consume_events()
|
||||
|
||||
def bring_online(self):
|
||||
""" Wait for all accounts to become ready to receive messages.
|
||||
|
||||
This will initialize logging, start IO and the direct_imap attribute
|
||||
for each account which either is CONFIGURED already or which is CONFIGURING
|
||||
and successfully completing the configuration process.
|
||||
"""
|
||||
print("wait_all_configured finds accounts=", self._account2state)
|
||||
for acc, state in self._account2state.items():
|
||||
if state == self.CONFIGURED:
|
||||
self._onconfigure_start_io(acc)
|
||||
self._account2state[acc] = self.IDLEREADY
|
||||
|
||||
while self.CONFIGURING in self._account2state.values():
|
||||
acc = self._pop_config_success()
|
||||
self._onconfigure_start_io(acc)
|
||||
self._account2state[acc] = self.IDLEREADY
|
||||
print("finished, account2state", self._account2state)
|
||||
|
||||
def _pop_config_success(self):
|
||||
acc, success = self._configured_events.get()
|
||||
if not success:
|
||||
pytest.fail("configuring online account failed: {}".format(acc))
|
||||
self._account2state[acc] = self.CONFIGURED
|
||||
return acc
|
||||
|
||||
def _onconfigure_start_io(self, acc):
|
||||
acc.start_io()
|
||||
print(acc._logid, "waiting for inbox IDLE to become ready")
|
||||
acc._evtracker.wait_idle_inbox_ready()
|
||||
self.init_direct_imap_and_logging(acc)
|
||||
acc._evtracker.consume_events()
|
||||
acc.log("inbox IDLE ready")
|
||||
|
||||
def init_direct_imap_and_logging(self, acc):
|
||||
""" idempotent function for initializing direct_imap and logging for an account. """
|
||||
self.init_direct_imap(acc)
|
||||
self.init_logging(acc)
|
||||
|
||||
def init_logging(self, acc):
|
||||
logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time)
|
||||
acc.add_account_plugin(logger, name=acc._logid)
|
||||
|
||||
def init_direct_imap(self, acc):
|
||||
""" idempotent function for initializing direct_imap."""
|
||||
from deltachat.direct_imap import DirectImap
|
||||
if not hasattr(acc, "direct_imap"):
|
||||
acc.direct_imap = DirectImap(acc)
|
||||
addr = acc.get_config("addr")
|
||||
if addr not in self._imap_cleaned:
|
||||
imap = acc.direct_imap
|
||||
for folder in imap.list_folders():
|
||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
||||
assert imap.select_folder(folder)
|
||||
imap.delete("1:*", expunge=True)
|
||||
else:
|
||||
imap.conn.folder.delete(folder)
|
||||
acc.log("imap cleaned for addr {}".format(addr))
|
||||
self._imap_cleaned.add(addr)
|
||||
|
||||
|
||||
class ACFactory:
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def __init__(self, request, testprocess, tmpdir, data) -> None:
|
||||
self.init_time = time.time()
|
||||
self.tmpdir = tmpdir
|
||||
self.pytestconfig = request.config
|
||||
self.data = data
|
||||
self._liveconfig_producer = testprocess.get_liveconfig_producer()
|
||||
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self._acsetup = ACSetup(self.init_time)
|
||||
self._preconfigured_keys = ["alice", "bob", "charlie",
|
||||
def __init__(self) -> None:
|
||||
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)
|
||||
request.addfinalizer(self.finalize)
|
||||
self.set_logging_default(False)
|
||||
deltachat.register_global_plugin(direct_imap)
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
|
||||
while self._accounts:
|
||||
acc = self._accounts.pop()
|
||||
if acc is not None:
|
||||
imap = getattr(acc, "direct_imap", None)
|
||||
if imap is not None:
|
||||
imap.shutdown()
|
||||
del acc.direct_imap
|
||||
while self._accounts:
|
||||
acc = self._accounts.pop()
|
||||
acc.shutdown()
|
||||
acc.disable_logging()
|
||||
deltachat.unregister_global_plugin(direct_imap)
|
||||
|
||||
def get_next_liveconfig(self):
|
||||
""" Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer)
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac.addr = ac.get_self_contact().addr
|
||||
ac.set_config("displayname", logid)
|
||||
if not quiet:
|
||||
logger = FFIEventLogger(ac)
|
||||
logger.init_time = self.init_time
|
||||
ac.add_account_plugin(logger)
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# 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)
|
||||
def set_logging_default(self, logging):
|
||||
self._logging = bool(logging)
|
||||
|
||||
assert "addr" in configdict and "mail_pw" in configdict
|
||||
return configdict
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
logid = "ac{}".format(len(self._accounts) + 1)
|
||||
path = self.tmpdir.join(logid)
|
||||
ac = Account(path.strpath, logging=self._logging)
|
||||
ac._logid = logid # later instantiated FFIEventLogger needs this
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
if self.pytestconfig.getoption("--debug-setup"):
|
||||
self._acsetup.init_logging(ac)
|
||||
self._accounts.append(ac)
|
||||
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 set_logging_default(self, logging):
|
||||
self._logging = bool(logging)
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
|
||||
def remove_preconfigured_keys(self):
|
||||
self._preconfigured_keys = []
|
||||
# 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 _preconfigure_key(self, account, addr):
|
||||
# Only set a preconfigured key if we haven't used it yet for another account.
|
||||
try:
|
||||
keyname = self._preconfigured_keys.pop(0)
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
fname_pub = self.data.read_path("key/{name}-public.asc".format(name=keyname))
|
||||
fname_sec = self.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_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"
|
||||
|
||||
def get_pseudo_configured_account(self):
|
||||
# do a pseudo-configured account
|
||||
ac = self.get_unconfigured_account()
|
||||
acname = os.path.basename(ac.db_path)
|
||||
addr = "{}@offline.org".format(acname)
|
||||
ac.update_config(dict(
|
||||
addr=addr, displayname=acname, mail_pw="123",
|
||||
configured_addr=addr, configured_mail_pw="123",
|
||||
configured="1",
|
||||
))
|
||||
self._preconfigure_key(ac, addr)
|
||||
self._acsetup.init_logging(ac)
|
||||
return ac
|
||||
if pytestconfig.getoption("--strict-tls"):
|
||||
# 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)
|
||||
|
||||
def new_online_configuring_account(self, cloned_from=None, **kwargs):
|
||||
if cloned_from is None:
|
||||
configdict = self.get_next_liveconfig()
|
||||
else:
|
||||
# XXX we might want to transfer the key to the new account
|
||||
configdict = dict(
|
||||
addr=cloned_from.get_config("addr"),
|
||||
mail_pw=cloned_from.get_config("mail_pw"),
|
||||
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'])
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, move=move)
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self, move=False, quiet=False):
|
||||
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
|
||||
ac2 = self.get_online_configuring_account(quiet=quiet)
|
||||
self.wait_configure_and_start_io([ac1, ac2])
|
||||
return ac1, ac2
|
||||
|
||||
def get_many_online_accounts(self, num, move=True):
|
||||
accounts = [self.get_online_configuring_account(move=move, quiet=True)
|
||||
for i in range(num)]
|
||||
self.wait_configure_and_start_io(accounts)
|
||||
for acc in accounts:
|
||||
acc.add_account_plugin(FFIEventLogger(acc))
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
|
||||
direct_imap object of an online account. This simulates the user setting
|
||||
up a new device without importing a backup.
|
||||
|
||||
`pre_generated_key` only means that a key from python/tests/data/key is
|
||||
used in order to speed things up.
|
||||
"""
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
if hasattr(account, "direct_imap"):
|
||||
# Attach the existing direct_imap. If we did not do this, a new one would be created and
|
||||
# delete existing messages (see dc_account_extra_configure(configure))
|
||||
ac.direct_imap = account.direct_imap
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def wait_configure_and_start_io(self, accounts=None):
|
||||
if accounts is None:
|
||||
accounts = self._accounts[:]
|
||||
started_accounts = []
|
||||
for acc in accounts:
|
||||
if acc not in started_accounts:
|
||||
self.wait_configure(acc)
|
||||
acc.set_config("bcc_self", "0")
|
||||
if acc.is_configured():
|
||||
acc.start_io()
|
||||
started_accounts.append(acc)
|
||||
print("{}: {} account was started".format(
|
||||
acc.get_config("displayname"), acc.get_config("addr")))
|
||||
for acc in started_accounts:
|
||||
acc._evtracker.wait_all_initial_fetches()
|
||||
|
||||
def wait_configure(self, acc):
|
||||
if hasattr(acc, "_configtracker"):
|
||||
acc._configtracker.wait_finish()
|
||||
acc._evtracker.consume_events()
|
||||
acc.get_device_chat().mark_noticed()
|
||||
del acc._configtracker
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
|
||||
bot_ac, bot_cfg = self.get_online_config()
|
||||
|
||||
# Avoid starting ac so we don't interfere with the bot operating on
|
||||
# the same database.
|
||||
self._accounts.remove(bot_ac)
|
||||
|
||||
args = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
fn,
|
||||
"--email", bot_cfg["addr"],
|
||||
"--password", bot_cfg["mail_pw"],
|
||||
bot_ac.db_path,
|
||||
]
|
||||
if ffi:
|
||||
args.insert(-1, "--show-ffi")
|
||||
print("$", " ".join(args))
|
||||
popen = subprocess.Popen(
|
||||
args=args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
|
||||
bufsize=0, # line buffering
|
||||
close_fds=True, # close all FDs other than 0/1/2
|
||||
universal_newlines=True # give back text
|
||||
)
|
||||
configdict.update(kwargs)
|
||||
ac = self.prepare_account_from_liveconfig(configdict)
|
||||
self._acsetup.start_configure(ac)
|
||||
return ac
|
||||
bot = BotProcess(popen, bot_cfg)
|
||||
self._finalizers.append(bot.kill)
|
||||
return bot
|
||||
|
||||
def prepare_account_from_liveconfig(self, configdict):
|
||||
ac = self.get_unconfigured_account()
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
ac.update_config(configdict)
|
||||
self._preconfigure_key(ac, configdict["addr"])
|
||||
return ac
|
||||
def dump_imap_summary(self, logfile):
|
||||
for ac in self._accounts:
|
||||
ac.dump_account_info(logfile=logfile)
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
try:
|
||||
imap.idle_done()
|
||||
except Exception:
|
||||
pass
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
|
||||
def wait_configured(self, account):
|
||||
""" Wait until the specified account has successfully completed configure. """
|
||||
self._acsetup.wait_one_configured(account)
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def bring_accounts_online(self):
|
||||
print("bringing accounts online")
|
||||
self._acsetup.bring_online()
|
||||
print("all accounts online")
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
for acc2 in accounts[i + 1:]:
|
||||
chat = self.get_accepted_chat(acc, acc2)
|
||||
if sending:
|
||||
chat.send_text("hi")
|
||||
to_wait.append(acc2)
|
||||
acc2.create_chat(acc).send_text("hi back")
|
||||
to_wait.append(acc)
|
||||
for acc in to_wait:
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
def get_online_accounts(self, num):
|
||||
# to reduce number of log events logging starts after accounts can receive
|
||||
accounts = [self.new_online_configuring_account() for i in range(num)]
|
||||
self.bring_accounts_online()
|
||||
return accounts
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
|
||||
bot_cfg = self.get_next_liveconfig()
|
||||
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
|
||||
|
||||
# Forget ac as it will be opened by the bot subprocess
|
||||
# but keep something in the list to not confuse account generation
|
||||
self._accounts[self._accounts.index(bot_ac)] = None
|
||||
|
||||
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, addr=bot_cfg["addr"])
|
||||
self._finalizers.append(bot.kill)
|
||||
return bot
|
||||
|
||||
def dump_imap_summary(self, logfile):
|
||||
for ac in self._accounts:
|
||||
ac.dump_account_info(logfile=logfile)
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
try:
|
||||
imap.idle_done()
|
||||
except Exception:
|
||||
pass
|
||||
imap.dump_imap_structures(self.tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
for acc2 in accounts[i + 1:]:
|
||||
chat = self.get_accepted_chat(acc, acc2)
|
||||
if sending:
|
||||
chat.send_text("hi")
|
||||
to_wait.append(acc2)
|
||||
acc2.create_chat(acc).send_text("hi back")
|
||||
to_wait.append(acc)
|
||||
for acc in to_wait:
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
am = AccountMaker()
|
||||
request.addfinalizer(am.finalize)
|
||||
yield am
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
if testprocess.pytestconfig.getoption("--extra-info"):
|
||||
logfile = io.StringIO()
|
||||
am.dump_imap_summary(logfile=logfile)
|
||||
print(logfile.getvalue())
|
||||
# request.node.add_report_section("call", "imap-server-state", s)
|
||||
logfile = io.StringIO()
|
||||
am.dump_imap_summary(logfile=logfile)
|
||||
print(logfile.getvalue())
|
||||
# request.node.add_report_section("call", "imap-server-state", s)
|
||||
|
||||
|
||||
class BotProcess:
|
||||
stdout_queue: queue.Queue
|
||||
|
||||
def __init__(self, popen, addr) -> None:
|
||||
def __init__(self, popen, bot_cfg) -> None:
|
||||
self.popen = popen
|
||||
self.addr = addr
|
||||
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.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
run with:
|
||||
|
||||
pytest -vv --durations=10 bench_empty.py
|
||||
|
||||
to see timings of test setups.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestEmpty:
|
||||
def test_prepare_setup_measurings(self, acfactory):
|
||||
acfactory.get_online_accounts(5)
|
||||
|
||||
@pytest.mark.parametrize("num", range(0, 5))
|
||||
def test_setup_online_accounts(self, acfactory, num):
|
||||
acfactory.get_online_accounts(num)
|
||||
@@ -17,7 +17,7 @@ def test_db_busy_error(acfactory, tmpdir):
|
||||
print("%3.2f %s" % (time.time() - starttime, string))
|
||||
|
||||
# make a number of accounts
|
||||
accounts = acfactory.get_many_online_accounts(3)
|
||||
accounts = acfactory.get_many_online_accounts(3, quiet=True)
|
||||
log("created %s accounts" % len(accounts))
|
||||
|
||||
# put a bigfile into each account
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
accounts = acfactory.get_online_accounts(5)
|
||||
acfactory.introduce_each_other(accounts)
|
||||
ac1, ac5 = accounts.pop(), accounts.pop()
|
||||
|
||||
lp.sec("ac1: creating group chat with 3 other members")
|
||||
chat = ac1.create_group_chat("title1", contacts=accounts)
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg1 = chat.send_text("hello")
|
||||
assert msg1.is_encrypted()
|
||||
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp > 0
|
||||
|
||||
assert chat.num_contacts() == 3 + 1
|
||||
|
||||
lp.sec("ac2: checking that the chat arrived correctly")
|
||||
ac2 = accounts[0]
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "hello"
|
||||
print("chat is", msg2.chat)
|
||||
assert msg2.chat.num_contacts() == 4
|
||||
|
||||
lp.sec("ac3: checking that 'ac4' is a known contact")
|
||||
ac3 = accounts[1]
|
||||
msg3 = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg3.text == "hello"
|
||||
ac3_contacts = ac3.get_contacts()
|
||||
assert len(ac3_contacts) == 4
|
||||
ac4_contacts = ac3.get_contacts(query=accounts[2].get_config("addr"))
|
||||
assert len(ac4_contacts) == 1
|
||||
|
||||
lp.sec("ac2: removing one contact")
|
||||
to_remove = ac2.create_contact(accounts[-1])
|
||||
msg2.chat.remove_contact(to_remove)
|
||||
|
||||
lp.sec("ac1: receiving system message about contact removal")
|
||||
sysmsg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert to_remove.addr in sysmsg.text
|
||||
assert sysmsg.chat.num_contacts() == 3
|
||||
|
||||
# Receiving message about removed contact does not reset gossip
|
||||
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
|
||||
|
||||
lp.sec("ac1: sending another message to the chat")
|
||||
chat.send_text("hello2")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello2"
|
||||
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
|
||||
|
||||
lp.sec("ac1: adding fifth member to the chat")
|
||||
chat.add_contact(ac5)
|
||||
# Adding contact to chat resets gossiped_timestamp
|
||||
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
|
||||
|
||||
lp.sec("ac2: receiving system message about contact addition")
|
||||
sysmsg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac5.get_config("configured_addr") in sysmsg.text
|
||||
assert sysmsg.chat.num_contacts() == 4
|
||||
|
||||
lp.sec("ac5: waiting for message about addition to the chat")
|
||||
sysmsg = ac5._evtracker.wait_next_incoming_message()
|
||||
msg = sysmsg.chat.send_text("hello!")
|
||||
# Message should be encrypted because keys of other members are gossiped
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
|
||||
"""
|
||||
Test that user recreates group member list when it joins the group again.
|
||||
ac1 creates a group with two other accounts: ac2 and ac3
|
||||
Then it removes ac2, removes ac3 and adds ac2 back.
|
||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||
"""
|
||||
lp.sec("setting up accounts, accepted with each other")
|
||||
accounts = acfactory.get_online_accounts(3)
|
||||
acfactory.introduce_each_other(accounts)
|
||||
ac1, ac2, ac3 = accounts
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
|
||||
assert not chat.is_promoted()
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
assert chat.is_promoted() and msg.is_encrypted()
|
||||
|
||||
assert chat.num_contacts() == 3
|
||||
|
||||
lp.sec("checking that the chat arrived correctly")
|
||||
for ac in accounts[1:]:
|
||||
msg = ac._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
print("chat is", msg.chat)
|
||||
assert msg.chat.num_contacts() == 3
|
||||
|
||||
lp.sec("ac1: removing ac2")
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
lp.sec("ac2: wait for a message about removal from the chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: removing ac3")
|
||||
chat.remove_contact(ac3)
|
||||
|
||||
lp.sec("ac1: adding ac2 back")
|
||||
# Group is promoted, message is sent automatically
|
||||
assert chat.is_promoted()
|
||||
chat.add_contact(ac2)
|
||||
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
assert chat2.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac2: read member added message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
assert msg_out.is_encrypted()
|
||||
|
||||
lp.sec("ac2: read message and check it's verified chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.chat.is_protected()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "world"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac1: create QR code and let ac3 scan it, starting the securejoin")
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
|
||||
lp.sec("ac3: start QR-code based setup contact protocol")
|
||||
ch = ac3.qr_setup_contact(qr)
|
||||
assert ch.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac1: add ac3 to verified group")
|
||||
chat1.add_contact(ac3)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.is_system_message()
|
||||
assert not msg.error
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# Skip system message about added member
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mvbox_move", [False, True])
|
||||
def test_fetch_existing(acfactory, lp, mvbox_move):
|
||||
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
|
||||
This way, we can already offer them some email addresses they can write to.
|
||||
|
||||
Also, the newest existing emails from each folder are fetched during onboarding.
|
||||
|
||||
Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
|
||||
|
||||
def assert_folders_configured(ac):
|
||||
"""There was a bug that scan_folders() set the configured folders to None under some circumstances.
|
||||
So, check that they are still configured:"""
|
||||
assert ac.get_config("configured_sentbox_folder") == "Sent"
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.wait_configured(ac1)
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "1")
|
||||
|
||||
# We need to reconfigure to find the new "Sent" folder.
|
||||
# `scan_folders()`, which runs automatically shortly after `start_io()` is invoked,
|
||||
# would also find the "Sent" folder, but it would be too late:
|
||||
# The sentbox thread, started by `start_io()`, would have seen that there is no
|
||||
# ConfiguredSentboxFolder and do nothing.
|
||||
acfactory._acsetup.start_configure(ac1, reconfigure=True)
|
||||
acfactory.bring_accounts_online()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
assert ac1.direct_imap.select_config_folder("mvbox" if mvbox_move else "inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message text")
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
|
||||
assert idle1.wait_for_seen()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("create a cloned ac1 and fetch contact history during configure")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
ac1_clone.set_config("fetch_existing_msgs", "1")
|
||||
acfactory.wait_configured(ac1_clone)
|
||||
ac1_clone.start_io()
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
lp.sec("check that ac2 contact was fetchted during configure")
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
lp.sec("check that messages changed events arrive for the correct message")
|
||||
msg = ac1_clone._evtracker.wait_next_messages_changed()
|
||||
assert msg.text == "message text"
|
||||
assert_folders_configured(ac1)
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
|
||||
def test_fetch_existing_msgs_group_and_single(acfactory, lp):
|
||||
"""There was a bug concerning fetch-existing-msgs:
|
||||
|
||||
A sent a message to you, adding you to a group. This created a contact request.
|
||||
You wrote a message to A, creating a chat.
|
||||
...but the group stayed blocked.
|
||||
So, after fetch-existing-msgs you have one contact request and one chat with the same person.
|
||||
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2097"""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("receive a message")
|
||||
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
|
||||
# wait until the bcc_self message arrives
|
||||
assert idle1.wait_for_seen()
|
||||
|
||||
lp.sec("Clone online account and let it fetch the existing messages")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
ac1_clone.set_config("fetch_existing_msgs", "1")
|
||||
acfactory.wait_configured(ac1_clone)
|
||||
|
||||
ac1_clone.start_io()
|
||||
ac1_clone._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
chats = ac1_clone.get_chats()
|
||||
assert len(chats) == 4 # two newly created chats + self-chat + device-chat
|
||||
group_chat = [c for c in chats if c.get_name() == "group name"][0]
|
||||
assert group_chat.is_group()
|
||||
private_chat, = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
|
||||
assert not private_chat.is_group()
|
||||
|
||||
group_messages = group_chat.get_messages()
|
||||
assert len(group_messages) == 1
|
||||
assert group_messages[0].text == "incoming, unencrypted group message"
|
||||
private_messages = private_chat.get_messages()
|
||||
# We can't decrypt the message in this chat, so the chat is empty:
|
||||
assert len(private_messages) == 0
|
||||
|
||||
|
||||
def test_undecipherable_group(acfactory, lp):
|
||||
"""Test how group messages that cannot be decrypted are
|
||||
handled.
|
||||
|
||||
Group name is encrypted and plaintext subject is set to "..." in
|
||||
this case, so we should assign the messages to existing chat
|
||||
instead of creating a new one. Since there is no existing group
|
||||
chat, the messages should be assigned to 1-1 chat with the sender
|
||||
of the message.
|
||||
"""
|
||||
|
||||
lp.sec("creating and configuring three accounts")
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2, ac3])
|
||||
|
||||
lp.sec("ac3 reinstalls DC and generates a new key")
|
||||
ac3.stop_io()
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
|
||||
acfactory.wait_configured(ac4)
|
||||
# Create contacts to make sure incoming messages are not treated as contact requests
|
||||
chat41 = ac4.create_chat(ac1)
|
||||
chat42 = ac4.create_chat(ac2)
|
||||
ac4.start_io()
|
||||
ac4._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
|
||||
lp.sec("ac2: checking that the chat arrived correctly")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
|
||||
# ac4 cannot decrypt the message.
|
||||
# Error message should be assigned to the chat with ac1.
|
||||
lp.sec("ac4: checking that message is assigned to the sender chat")
|
||||
error_msg = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_msg.error # There is an error decrypting the message
|
||||
assert error_msg.chat == chat41
|
||||
|
||||
lp.sec("ac2: sending a reply to the chat")
|
||||
msg.chat.send_text("reply")
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert reply.text == "reply"
|
||||
assert reply.is_encrypted(), "Reply is not encrypted"
|
||||
|
||||
lp.sec("ac4: checking that reply is assigned to ac2 chat")
|
||||
error_reply = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_reply.error # There is an error decrypting the message
|
||||
assert error_reply.chat == chat42
|
||||
|
||||
# Test that ac4 replies to error messages don't appear in the
|
||||
# group chat on ac1 and ac2.
|
||||
lp.sec("ac4: replying to ac1 and ac2")
|
||||
|
||||
# Otherwise reply becomes a contact request.
|
||||
chat41.send_text("I can't decrypt your message, ac1!")
|
||||
chat42.send_text("I can't decrypt your message, ac2!")
|
||||
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac1!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac1.create_chat(ac3)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac2!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
|
||||
def test_ephemeral_timer(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("ac1: set ephemeral timer to 60")
|
||||
chat1.set_ephemeral_timer(60)
|
||||
|
||||
lp.sec("ac1: check that ephemeral timer is set for chat")
|
||||
assert chat1.get_ephemeral_timer() == 60
|
||||
chat1_summary = chat1.get_summary()
|
||||
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
|
||||
|
||||
lp.sec("ac2: receive system message about ephemeral timer modification")
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert chat2.get_ephemeral_timer() == 60
|
||||
assert system_message1.is_system_message()
|
||||
|
||||
# Disabled until markers are implemented
|
||||
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
|
||||
|
||||
lp.sec("ac2: send message to ac1")
|
||||
sent_message = chat2.send_text("message")
|
||||
assert sent_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
|
||||
|
||||
# Timer is started immediately for sent messages
|
||||
assert sent_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in sent_message.get_message_info()
|
||||
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
text_message = ac1._evtracker.wait_next_incoming_message()
|
||||
assert text_message.text == "message"
|
||||
assert text_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
|
||||
|
||||
# Timer should not start until message is displayed
|
||||
assert text_message.ephemeral_timestamp is None
|
||||
assert "Expires: " not in text_message.get_message_info()
|
||||
text_message.mark_seen()
|
||||
text_message = ac1.get_message_by_id(text_message.id)
|
||||
assert text_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in text_message.get_message_info()
|
||||
|
||||
lp.sec("ac2: set ephemeral timer to 0")
|
||||
chat2.set_ephemeral_timer(0)
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
|
||||
lp.sec("ac1: receive system message about ephemeral timer modification")
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message2 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert system_message2.ephemeral_timer is None
|
||||
assert "Ephemeral timer: " not in system_message2.get_message_info()
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
def test_multidevice_sync_seen(acfactory, lp):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert ac1_chat.count_fresh_messages() == 1
|
||||
assert ac1_clone_chat.count_fresh_messages() == 1
|
||||
assert ac1_message.is_in_fresh
|
||||
assert ac1_clone_message.is_in_fresh
|
||||
|
||||
lp.sec("ac1 marks message as seen on the first device")
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
|
||||
lp.sec("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
|
||||
lp.sec("Send an ephemeral message from ac2 to ac1")
|
||||
ac2_chat.set_ephemeral_timer(60)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1_clone._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2_chat.send_text("Foobar")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
assert "Expires: " in ac1_message.get_message_info()
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,634 +0,0 @@
|
||||
from __future__ import print_function
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me")),
|
||||
("Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Group left by tmp1@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
])
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
|
||||
out = parse_system_add_remove(msgtext)
|
||||
assert out == res
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
p.write("123")
|
||||
account = Account(p.strpath)
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
# we can't easily test if os_name is used in X-Mailer
|
||||
# outgoing messages without a full Online test
|
||||
# but we at least check Account accepts the arg
|
||||
ac1 = Account(p.strpath, os_name="solarpunk")
|
||||
ac1.get_info()
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, data):
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
d = ac1.get_info()
|
||||
assert d["arch"]
|
||||
assert d["number_of_chats"] == "0"
|
||||
assert d["bcc_self"] == "0"
|
||||
|
||||
def test_is_not_configured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert not ac1.is_configured()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.check_is_configured()
|
||||
|
||||
def test_wrong_config_keys(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(KeyError):
|
||||
ac1.set_config("lqkwje", "value")
|
||||
with pytest.raises(KeyError):
|
||||
ac1.get_config("lqkwje")
|
||||
|
||||
def test_set_config_int_conversion(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.set_config("mvbox_move", False)
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
ac1.set_config("mvbox_move", True)
|
||||
assert ac1.get_config("mvbox_move") == "1"
|
||||
ac1.set_config("mvbox_move", 0)
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
ac1.set_config("mvbox_move", 1)
|
||||
assert ac1.get_config("mvbox_move") == "1"
|
||||
|
||||
def test_update_config(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config(dict(mvbox_move=False))
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_savemime(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
||||
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert not ac1.get_self_contact().addr
|
||||
|
||||
def test_selfcontact_configured(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
me = ac1.get_self_contact()
|
||||
assert me.display_name
|
||||
assert me.addr
|
||||
|
||||
def test_get_config_fails(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(KeyError):
|
||||
ac1.get_config("123123")
|
||||
|
||||
def test_empty_group_bcc_self_enabled(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = ac1.create_group_chat(name="group1")
|
||||
msg = chat.send_text("msg1")
|
||||
assert msg in chat.get_messages()
|
||||
|
||||
def test_empty_group_bcc_self_disabled(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_group_chat(name="group1")
|
||||
msg = chat.send_text("msg1")
|
||||
assert msg in chat.get_messages()
|
||||
|
||||
|
||||
class TestOfflineContact:
|
||||
def test_contact_attr(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some1@example.org", name="some1")
|
||||
str(contact1)
|
||||
repr(contact1)
|
||||
assert contact1 == contact2
|
||||
assert contact1.id
|
||||
assert contact1.addr == "some1@example.org"
|
||||
assert contact1.display_name == "some1"
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_get_blocked(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
ac1.create_contact("some3@example.org", name="some3")
|
||||
assert ac1.get_blocked_contacts() == []
|
||||
contact1.block()
|
||||
assert ac1.get_blocked_contacts() == [contact1]
|
||||
contact2.block()
|
||||
blocked = ac1.get_blocked_contacts()
|
||||
assert len(blocked) == 2 and contact1 in blocked and contact2 in blocked
|
||||
contact2.unblock()
|
||||
assert ac1.get_blocked_contacts() == [contact1]
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contacts = ac1.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contact1 in contacts
|
||||
|
||||
assert not ac1.get_contacts(query="some2")
|
||||
assert ac1.get_contacts(query="some1")
|
||||
assert not ac1.get_contacts(only_verified=True)
|
||||
assert len(ac1.get_contacts(with_self=True)) == 2
|
||||
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert contact1 not in ac1.get_contacts()
|
||||
|
||||
def test_get_contacts_and_delete_fails(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
msg = contact1.create_chat().send_text("one message")
|
||||
assert not ac1.delete_contact(contact1)
|
||||
assert not msg.filemime
|
||||
|
||||
def test_create_chat_flexibility(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
|
||||
assert chat1 == chat2
|
||||
ac3 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.create_chat(ac3)
|
||||
|
||||
def test_contact_rename(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = ac1.create_chat(contact)
|
||||
assert chat.get_name() == "some1"
|
||||
ac1.create_contact("some1@example.com", name="renamed")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
|
||||
assert ev.data1 == chat.id
|
||||
assert chat.get_name() == "renamed"
|
||||
|
||||
|
||||
class TestOfflineChat:
|
||||
@pytest.fixture
|
||||
def ac1(self, acfactory):
|
||||
return acfactory.get_pseudo_configured_account()
|
||||
|
||||
@pytest.fixture
|
||||
def chat1(self, ac1):
|
||||
return ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
|
||||
def test_display(self, chat1):
|
||||
str(chat1)
|
||||
repr(chat1)
|
||||
|
||||
def test_is_group(self, chat1):
|
||||
assert not chat1.is_group()
|
||||
|
||||
def test_chat_by_id(self, chat1):
|
||||
chat2 = chat1.account.get_chat_by_id(chat1.id)
|
||||
assert chat2 == chat1
|
||||
with pytest.raises(ValueError):
|
||||
chat1.account.get_chat_by_id(123123)
|
||||
|
||||
def test_chat_idempotent(self, chat1, ac1):
|
||||
contact1 = chat1.get_contacts()[0]
|
||||
chat2 = contact1.create_chat()
|
||||
assert chat2.id == chat1.id
|
||||
assert chat2.get_name() == chat1.get_name()
|
||||
assert chat1 == chat2
|
||||
assert not (chat1 != chat2)
|
||||
|
||||
for ichat in ac1.get_chats():
|
||||
if ichat.id == chat1.id:
|
||||
break
|
||||
else:
|
||||
pytest.fail("could not find chat")
|
||||
|
||||
def test_group_chat_add_second_account(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
with pytest.raises(ValueError):
|
||||
chat.add_contact(ac2.get_self_contact())
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("addr")
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
def test_group_chat_creation(self, ac1):
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
chat.set_name("title2")
|
||||
assert chat.get_name() == "title2"
|
||||
|
||||
d = chat.get_summary()
|
||||
print(d)
|
||||
assert d["id"] == chat.id
|
||||
assert d["type"] == chat.get_type()
|
||||
assert d["name"] == chat.get_name()
|
||||
assert d["archived"] == chat.is_archived()
|
||||
# assert d["param"] == chat.param
|
||||
assert d["color"] == chat.get_color()
|
||||
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image()
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
||||
assert chat.is_group()
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
def test_removing_blocked_user_from_group(self, ac1, lp):
|
||||
"""
|
||||
Test that blocked contact is not unblocked when removed from a group.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2030
|
||||
"""
|
||||
lp.sec("Create a group chat with a contact")
|
||||
contact = ac1.create_contact("some1@example.org")
|
||||
group = ac1.create_group_chat("title", contacts=[contact])
|
||||
group.send_text("First group message")
|
||||
|
||||
lp.sec("ac1 blocks contact")
|
||||
contact.block()
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac1 removes contact from their group")
|
||||
group.remove_contact(contact)
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac1 adding blocked contact unblocks it")
|
||||
group.add_contact(contact)
|
||||
assert not contact.is_blocked()
|
||||
|
||||
def test_get_set_profile_image_simple(self, ac1, data):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
p = data.get_path("d.png")
|
||||
chat.set_profile_image(p)
|
||||
p2 = chat.get_profile_image()
|
||||
assert open(p, "rb").read() == open(p2, "rb").read()
|
||||
chat.remove_profile_image()
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_mute(self, ac1):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert not chat.is_muted()
|
||||
assert chat.get_mute_duration() == 0
|
||||
chat.mute()
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() == -1
|
||||
chat.unmute()
|
||||
assert not chat.is_muted()
|
||||
chat.mute(50)
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() <= 50
|
||||
with pytest.raises(ValueError):
|
||||
chat.mute(-51)
|
||||
|
||||
# Regression test, this caused Rust panic previously
|
||||
chat.mute(2**63 - 1)
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() == -1
|
||||
|
||||
def test_delete_and_send_fails(self, ac1, chat1):
|
||||
chat1.delete()
|
||||
ac1._evtracker.wait_next_messages_changed()
|
||||
with pytest.raises(ValueError):
|
||||
chat1.send_text("msg1")
|
||||
|
||||
def test_prepare_message_and_send(self, ac1, chat1):
|
||||
msg = chat1.prepare_message(Message.new_empty(chat1.account, "text"))
|
||||
msg.set_text("hello world")
|
||||
assert msg.text == "hello world"
|
||||
assert msg.id > 0
|
||||
chat1.send_prepared(msg)
|
||||
assert "Sent" in msg.get_message_info()
|
||||
str(msg)
|
||||
repr(msg)
|
||||
assert msg == ac1.get_message_by_id(msg.id)
|
||||
|
||||
def test_prepare_file(self, ac1, chat1):
|
||||
blobdir = ac1.get_blobdir()
|
||||
p = os.path.join(blobdir, "somedata.txt")
|
||||
with open(p, "w") as f:
|
||||
f.write("some data")
|
||||
message = chat1.prepare_message_file(p)
|
||||
assert message.id > 0
|
||||
message.set_text("hello world")
|
||||
assert message.is_out_preparing()
|
||||
assert message.text == "hello world"
|
||||
chat1.send_prepared(message)
|
||||
assert "Sent" in message.get_message_info()
|
||||
|
||||
def test_message_eq_contains(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
assert msg in chat1.get_messages()
|
||||
assert not (msg not in chat1.get_messages())
|
||||
str(msg)
|
||||
repr(msg)
|
||||
|
||||
def test_message_send_text(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
assert msg
|
||||
assert msg.is_text()
|
||||
assert not msg.is_audio()
|
||||
assert not msg.is_video()
|
||||
assert not msg.is_gif()
|
||||
assert not msg.is_file()
|
||||
assert not msg.is_image()
|
||||
|
||||
assert not msg.is_in_fresh()
|
||||
assert not msg.is_in_noticed()
|
||||
assert not msg.is_in_seen()
|
||||
assert msg.is_out_pending()
|
||||
assert not msg.is_out_failed()
|
||||
assert not msg.is_out_delivered()
|
||||
assert not msg.is_out_mdn_received()
|
||||
|
||||
def test_message_image(self, chat1, data, lp):
|
||||
with pytest.raises(ValueError):
|
||||
chat1.send_image(path="notexists")
|
||||
fn = data.get_path("d.png")
|
||||
lp.sec("sending image")
|
||||
chat1.account._evtracker.consume_events()
|
||||
msg = chat1.send_image(fn)
|
||||
chat1.account._evtracker.get_matching("DC_EVENT_NEW_BLOB_FILE")
|
||||
assert msg.is_image()
|
||||
assert msg
|
||||
assert msg.id > 0
|
||||
assert os.path.exists(msg.filename)
|
||||
assert msg.filemime == "image/png"
|
||||
|
||||
@pytest.mark.parametrize("typein,typeout", [
|
||||
(None, "application/octet-stream"),
|
||||
("text/plain", "text/plain"),
|
||||
("image/png", "image/png"),
|
||||
])
|
||||
def test_message_file(self, ac1, chat1, data, lp, typein, typeout):
|
||||
lp.sec("sending file")
|
||||
fn = data.get_path("r.txt")
|
||||
msg = chat1.send_file(fn, typein)
|
||||
assert msg
|
||||
assert msg.id > 0
|
||||
assert msg.is_file()
|
||||
assert os.path.exists(msg.filename)
|
||||
assert msg.filename.endswith(msg.basename)
|
||||
assert msg.filemime == typeout
|
||||
msg2 = chat1.send_file(fn, typein)
|
||||
assert msg2 != msg
|
||||
assert msg2.filename != msg.filename
|
||||
|
||||
def test_create_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
email = "hello <hello@example.org>"
|
||||
contact1 = ac1.create_contact(email)
|
||||
assert contact1.addr == "hello@example.org"
|
||||
assert contact1.name == "hello"
|
||||
contact1 = ac1.create_contact(email, name="world")
|
||||
assert contact1.name == "world"
|
||||
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
|
||||
assert contact2.name == "real"
|
||||
|
||||
def test_create_chat_simple(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
msg = chat1.send_text("msg1")
|
||||
ts = msg.time_sent
|
||||
assert msg.time_received is None
|
||||
assert ts.strftime("Y")
|
||||
assert past1s < ts
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
def test_set_config_after_configure_is_forbidden(self, ac1):
|
||||
assert ac1.get_config("mail_pw")
|
||||
assert ac1.is_configured()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_config("addr", "123@example.org")
|
||||
|
||||
def test_import_export_one_contact(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
assert not backupdir.listdir()
|
||||
ac1.stop_io()
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg1 = chat1.prepare_message(msg)
|
||||
msg1.set_text("hello")
|
||||
chat1.set_draft(msg1)
|
||||
msg1.set_text("obsolete")
|
||||
msg2 = chat1.get_draft()
|
||||
assert msg2.text == "hello"
|
||||
chat1.set_draft(None)
|
||||
assert chat1.get_draft() is None
|
||||
|
||||
def test_qr_setup_contact(self, acfactory, lp):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
assert qr.startswith("OPENPGP4FPR:")
|
||||
res = ac2.check_qr(qr)
|
||||
assert res.is_ask_verifycontact()
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
def test_quote(self, chat1):
|
||||
"""Offline quoting test"""
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg.set_text("Multi\nline\nmessage")
|
||||
assert msg.quoted_text is None
|
||||
|
||||
# Prepare message to assign it a Message-Id.
|
||||
# Messages without Message-Id cannot be quoted.
|
||||
msg = chat1.prepare_message(msg)
|
||||
|
||||
reply_msg = Message.new_empty(chat1.account, "text")
|
||||
reply_msg.set_text("reply")
|
||||
reply_msg.quote = msg
|
||||
assert reply_msg.quoted_text == "Multi\nline\nmessage"
|
||||
|
||||
def test_group_chat_many_members_add_remove(self, ac1, lp):
|
||||
lp.sec("ac1: creating group chat with 10 other members")
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
|
||||
# activate local plugin
|
||||
in_list = []
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
# perform add contact many times
|
||||
contacts = []
|
||||
for i in range(10):
|
||||
lp.sec("create contact")
|
||||
contact = ac1.create_contact("some{}@example.org".format(i))
|
||||
contacts.append(contact)
|
||||
lp.sec("add contact")
|
||||
chat.add_contact(contact)
|
||||
|
||||
assert chat.num_contacts() == 11
|
||||
|
||||
# let's make sure the events perform plugin hooks
|
||||
def wait_events(cond):
|
||||
now = time.time()
|
||||
while time.time() < now + 5:
|
||||
if cond():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
pytest.fail("failed to get events")
|
||||
|
||||
wait_events(lambda: len(in_list) == 10)
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
|
||||
in_list[:] = []
|
||||
|
||||
lp.sec("ac1: removing two contacts and checking things are right")
|
||||
chat.remove_contact(contacts[9])
|
||||
chat.remove_contact(contacts[3])
|
||||
assert chat.num_contacts() == 9
|
||||
|
||||
wait_events(lambda: len(in_list) == 2)
|
||||
assert len(in_list) == 2
|
||||
assert in_list[0][0] == "removed"
|
||||
assert in_list[0][1] == chat
|
||||
assert in_list[0][2] == contacts[9]
|
||||
assert in_list[1][0] == "removed"
|
||||
assert in_list[1][1] == chat
|
||||
assert in_list[1][2] == contacts[3]
|
||||
|
||||
def test_audit_log_view_without_daymarker(self, ac1, lp):
|
||||
lp.sec("ac1: test audit log (show only system messages)")
|
||||
chat = ac1.create_group_chat(name="audit log sample data")
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("create test data")
|
||||
chat.add_contact(ac1.create_contact("some-1@example.org"))
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
lp.sec("check message count of all messages")
|
||||
assert len(chat.get_messages()) == 4
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
3010
python/tests/test_account.py
Normal file
3010
python/tests/test_account.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ def wait_msgs_changed(account, msgs_list):
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
@@ -43,7 +43,7 @@ class TestOnlineInCreation:
|
||||
chat.prepare_message_file(src.strpath)
|
||||
|
||||
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
@@ -56,7 +56,7 @@ class TestOnlineInCreation:
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
|
||||
@@ -6,47 +6,9 @@ from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
from deltachat.testplugin import ACSetup
|
||||
# from deltachat.account import EventLogger
|
||||
|
||||
|
||||
class TestACSetup:
|
||||
def test_basic_states(self, acfactory, monkeypatch):
|
||||
pc = ACSetup(init_time=0.0)
|
||||
acc = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(acc, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURING
|
||||
pc._configured_events.put((acc, True))
|
||||
monkeypatch.setattr(pc, "init_direct_imap", lambda *args, **kwargs: None)
|
||||
pc.wait_one_configured(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURED
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
pc.bring_online()
|
||||
assert pc._account2state[acc] == pc.IDLEREADY
|
||||
|
||||
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory):
|
||||
pc = ACSetup(init_time=0.0)
|
||||
monkeypatch.setattr(pc, "init_direct_imap", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(ac1, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(ac1)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(ac2, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(ac2)
|
||||
assert pc._account2state[ac1] == pc.CONFIGURING
|
||||
assert pc._account2state[ac2] == pc.CONFIGURING
|
||||
pc._configured_events.put((ac1, True))
|
||||
pc.wait_one_configured(ac1)
|
||||
assert pc._account2state[ac1] == pc.CONFIGURED
|
||||
assert pc._account2state[ac2] == pc.CONFIGURING
|
||||
pc._configured_events.put((ac2, True))
|
||||
pc.bring_online()
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||
capi.lib.dc_context_unref(ctx)
|
||||
@@ -106,7 +68,8 @@ def test_sig():
|
||||
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = contact1.create_chat()
|
||||
chat.send_text("one messae")
|
||||
@@ -117,7 +80,7 @@ def test_markseen_invalid_message_ids(acfactory):
|
||||
|
||||
|
||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
for i in range(1, 10):
|
||||
msg = ac1.get_message_by_id(i)
|
||||
assert msg.id == 0
|
||||
@@ -140,36 +103,3 @@ def test_get_info_open(tmpdir):
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
assert 'deltachat_core_version' in info
|
||||
assert 'database_dir' in info
|
||||
|
||||
|
||||
def test_logged_hook_failure(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
cap = []
|
||||
ac1.log = cap.append
|
||||
with ac1._event_thread.swallow_and_log_exception("some"):
|
||||
0/0
|
||||
assert cap
|
||||
assert "some" in str(cap)
|
||||
assert "ZeroDivisionError" in str(cap)
|
||||
assert "Traceback" in str(cap)
|
||||
|
||||
|
||||
def test_logged_ac_process_ffi_failure(acfactory):
|
||||
from deltachat import account_hookimpl
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
class FailPlugin:
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(ffi_event):
|
||||
0/0
|
||||
|
||||
cap = Queue()
|
||||
ac1.log = cap.put
|
||||
ac1.add_account_plugin(FailPlugin())
|
||||
# cause any event eg contact added/changed
|
||||
ac1.create_contact("something@example.org")
|
||||
res = cap.get(timeout=10)
|
||||
assert "ac_process_ffi_event" in res
|
||||
assert "ZeroDivisionError" in res
|
||||
assert "Traceback" in res
|
||||
@@ -8,7 +8,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
pytest -n1 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
|
||||
195
src/chat.rs
195
src/chat.rs
@@ -18,7 +18,7 @@ use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
||||
DC_CHAT_ID_TRASH, DC_GCM_ADDDAYMARKER, DC_GCM_INFO_ONLY, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::contact::{addr_cmp, Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::ReceivedMsg;
|
||||
use crate::dc_tools::{
|
||||
@@ -1243,7 +1243,7 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let from = context.get_configured_addr().await?;
|
||||
let new_rfc724_mid = {
|
||||
let grpid = match self.typ {
|
||||
Chattype::Group => Some(self.grpid.as_str()),
|
||||
@@ -2023,7 +2023,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let from = context.get_configured_addr().await?;
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
@@ -2690,8 +2690,8 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
context.sync_qr_code_tokens(Some(chat_id)).await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
let self_addr = context.get_configured_addr().await.unwrap_or_default();
|
||||
if addr_cmp(contact.get_addr(), &self_addr) {
|
||||
// ourself is added using ContactId::SELF, do not add this address explicitly.
|
||||
// if SELF is not in the group, members cannot be added at all.
|
||||
warn!(
|
||||
@@ -3049,7 +3049,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
sql::repeat_vars(msg_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
@@ -3126,52 +3126,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
let mut chat_id = None;
|
||||
let mut msgs: Vec<Message> = Vec::new();
|
||||
for msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, *msg_id).await?;
|
||||
if let Some(chat_id) = chat_id {
|
||||
ensure!(
|
||||
chat_id == msg.chat_id,
|
||||
"messages to resend needs to be in the same chat"
|
||||
);
|
||||
} else {
|
||||
chat_id = Some(msg.chat_id);
|
||||
}
|
||||
ensure!(
|
||||
msg.from_id == ContactId::SELF,
|
||||
"can resend only own messages"
|
||||
);
|
||||
ensure!(!msg.is_info(), "cannot resend info messages");
|
||||
msgs.push(msg)
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
for mut msg in msgs {
|
||||
if msg.get_showpadlock() && !chat.is_protected() {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.update_param(context).await;
|
||||
}
|
||||
match msg.get_state() {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
||||
}
|
||||
_ => bail!("unexpected message state"),
|
||||
}
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
if create_send_msg_job(context, msg.id).await?.is_some() {
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
|
||||
if context.sql.is_open().await {
|
||||
// no database, no chats - this is no error (needed eg. for information)
|
||||
@@ -3644,7 +3598,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_add_contact_to_chat_ex_add_self() {
|
||||
// Adding self to a contact should succeed, even though it's pointless.
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5150,141 +5104,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_resend_own_message() -> Result<()> {
|
||||
// Alice creates group with Bob and sends an initial message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
// Alice adds Claire to group and resends her own initial message
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
resend_msgs(&alice, &[sent1.sender_msg_id]).await?;
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives all messages
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 1);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 2);
|
||||
bob.recv_msg(&sent3).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 2);
|
||||
|
||||
// Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
let claire = TestContext::new().await;
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
claire.recv_msg(&sent2).await;
|
||||
claire.recv_msg(&sent3).await;
|
||||
let msg = claire.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id, 0, None).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_resend_foreign_message_fails() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(resend_msgs(&bob, &[msg.id]).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_resend_opportunistically_encryption() -> Result<()> {
|
||||
// Alice creates group with Bob and sends an initial message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
// Bob now can send an encrypted message
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
msg.chat_id.accept(&bob).await?;
|
||||
let sent2 = bob.send_text(msg.chat_id, "bob->alice").await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats
|
||||
add_contact_to_chat(
|
||||
&bob,
|
||||
msg.chat_id,
|
||||
Contact::create(&bob, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let _sent3 = bob.pop_sent_msg().await;
|
||||
resend_msgs(&bob, &[sent2.sender_msg_id]).await?;
|
||||
let _sent4 = bob.pop_sent_msg().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_resend_info_message_fails() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_can_send_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
132
src/config.rs
132
src/config.rs
@@ -6,7 +6,6 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
|
||||
use crate::events::EventType;
|
||||
@@ -112,7 +111,6 @@ pub enum Config {
|
||||
DeleteDeviceAfter,
|
||||
|
||||
SaveMimeHeaders,
|
||||
/// The primary email address. Also see `SecondaryAddrs`.
|
||||
ConfiguredAddr,
|
||||
ConfiguredMailServer,
|
||||
ConfiguredMailUser,
|
||||
@@ -135,10 +133,6 @@ pub enum Config {
|
||||
ConfiguredProvider,
|
||||
Configured,
|
||||
|
||||
/// All secondary self addresses separated by spaces
|
||||
/// (`addr1@example.org addr2@exapmle.org addr3@example.org`)
|
||||
SecondaryAddrs,
|
||||
|
||||
#[strum(serialize = "sys.version")]
|
||||
SysVersion,
|
||||
|
||||
@@ -232,6 +226,12 @@ impl Context {
|
||||
Ok(self.get_config_int(key).await? != 0)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_configured_addr(&self) -> Result<String> {
|
||||
self.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.context("no address configured")
|
||||
}
|
||||
|
||||
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
|
||||
@@ -333,73 +333,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
// Separate impl block for self address handling
|
||||
impl Context {
|
||||
/// Determine whether the specified addr maps to the/a self addr.
|
||||
/// Returns `false` if no addresses are configured.
|
||||
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
Ok(self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a))
|
||||
|| self
|
||||
.get_secondary_self_addrs()
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
}
|
||||
|
||||
/// Sets `primary_new` as the new primary self address and saves the old
|
||||
/// primary address (if exists) as a secondary address.
|
||||
///
|
||||
/// This should only be used by test code and during configure.
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
// add old primary address (if exists) to secondary addresses
|
||||
let mut secondary_addrs = self.get_all_self_addrs().await?;
|
||||
// never store a primary address also as a secondary
|
||||
secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
|
||||
self.set_config(
|
||||
Config::SecondaryAddrs,
|
||||
Some(secondary_addrs.join(" ").as_str()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.set_config(Config::ConfiguredAddr, Some(primary_new))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all primary and secondary self addresses.
|
||||
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
|
||||
let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();
|
||||
|
||||
Ok(primary_addrs.chain(secondary_addrs).collect())
|
||||
}
|
||||
|
||||
/// Returns all secondary self addresses.
|
||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
let secondary_addrs = self
|
||||
.get_config(Config::SecondaryAddrs)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
Ok(secondary_addrs
|
||||
.split_ascii_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the primary self address.
|
||||
/// Returns an error if no self addr is configured.
|
||||
pub async fn get_primary_self_addr(&self) -> Result<String> {
|
||||
self.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.context("No self addr configured")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all available configuration keys concated together.
|
||||
fn get_config_keys_string() -> String {
|
||||
let keys = Config::iter().fold(String::new(), |mut acc, key| {
|
||||
@@ -484,57 +417,4 @@ mod tests {
|
||||
assert_eq!(t.get_config_bool(c).await?, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_self_addrs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
assert!(alice.is_self_addr("alice@example.org").await?);
|
||||
assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
|
||||
assert!(!alice.is_self_addr("alice@alice.com").await?);
|
||||
|
||||
// Test adding the same primary address
|
||||
alice.set_primary_self_addr("alice@example.org").await?;
|
||||
alice.set_primary_self_addr("Alice@Example.Org").await?;
|
||||
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
|
||||
|
||||
// Test adding a new (primary) self address
|
||||
// The address is trimmed during by `LoginParam::from_database()`,
|
||||
// so `set_primary_self_addr()` doesn't have to trim it.
|
||||
alice.set_primary_self_addr(" Alice@alice.com ").await?;
|
||||
assert!(alice.is_self_addr(" aliCe@example.org").await?);
|
||||
assert!(alice.is_self_addr("alice@alice.com").await?);
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec![" Alice@alice.com ", "Alice@Example.Org"]
|
||||
);
|
||||
|
||||
// Check that the entry is not duplicated
|
||||
alice.set_primary_self_addr("alice@alice.com").await?;
|
||||
alice.set_primary_self_addr("alice@alice.com").await?;
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["alice@alice.com", "Alice@Example.Org"]
|
||||
);
|
||||
|
||||
// Test switching back
|
||||
alice.set_primary_self_addr("alice@example.org").await?;
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["alice@example.org", "alice@alice.com"]
|
||||
);
|
||||
|
||||
// Test setting a new primary self address, the previous self address
|
||||
// should be kept as a secondary self address
|
||||
alice.set_primary_self_addr("alice@alice.xyz").await?;
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
|
||||
);
|
||||
assert!(alice.is_self_addr("alice@example.org").await?);
|
||||
assert!(alice.is_self_addr("alice@alice.com").await?);
|
||||
assert!(alice.is_self_addr("Alice@alice.xyz").await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,12 +453,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(¶m.addr) {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
job::schedule_resync(ctx).await?;
|
||||
}
|
||||
|
||||
// the trailing underscore is correct
|
||||
param.save_as_configured_params(ctx).await?;
|
||||
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
@@ -297,10 +296,7 @@ impl Contact {
|
||||
.await?;
|
||||
if contact_id == ContactId::SELF {
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
contact.addr = context.get_configured_addr().await.unwrap_or_default();
|
||||
contact.status = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await?
|
||||
@@ -402,7 +398,6 @@ impl Contact {
|
||||
if context.is_self_addr(addr_normalized).await? {
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
let id = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
@@ -453,7 +448,10 @@ impl Contact {
|
||||
|
||||
let addr = addr_normalize(addr).to_string();
|
||||
|
||||
if context.is_self_addr(&addr).await? {
|
||||
// during configuration process some chats are added and addr
|
||||
// might not be configured yet
|
||||
let addr_self = context.get_configured_addr().await.unwrap_or_default();
|
||||
if addr_cmp(&addr, &addr_self) {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
@@ -688,7 +686,7 @@ impl Contact {
|
||||
listflags: u32,
|
||||
query: Option<impl AsRef<str>>,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let self_addrs = context.get_all_self_addrs().await?;
|
||||
let self_addr = context.get_configured_addr().await.unwrap_or_default();
|
||||
let mut add_self = false;
|
||||
let mut ret = Vec::new();
|
||||
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
|
||||
@@ -699,25 +697,23 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT c.id FROM contacts c \
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.addr NOT IN ({})
|
||||
AND c.id>? \
|
||||
AND c.origin>=? \
|
||||
WHERE c.addr!=?1 \
|
||||
AND c.id>?2 \
|
||||
AND c.origin>=?3 \
|
||||
AND c.blocked=0 \
|
||||
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
|
||||
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \
|
||||
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
paramsv![
|
||||
self_addr,
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
])),
|
||||
],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
@@ -728,17 +724,13 @@ impl Contact {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(query) = query {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let self_name2 = stock_str::self_msg(context);
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let self_name2 = stock_str::self_msg(context);
|
||||
|
||||
if let Some(query) = query {
|
||||
if self_addr.contains(query.as_ref())
|
||||
|| self_name.contains(query.as_ref())
|
||||
|| self_name2.await.contains(query.as_ref())
|
||||
@@ -754,19 +746,13 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr NOT IN ({})
|
||||
AND id>?
|
||||
AND origin>=?
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr!=?1
|
||||
AND id>?2
|
||||
AND origin>=?3
|
||||
AND blocked=0
|
||||
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo
|
||||
])),
|
||||
paramsv![self_addr, ContactId::LAST_SPECIAL, Origin::IncomingReplyTo],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
@@ -1130,6 +1116,26 @@ impl Contact {
|
||||
Ok(VerifiedStatus::Unverified)
|
||||
}
|
||||
|
||||
pub async fn addr_equals_contact(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
if addr.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
if !contact.addr.is_empty() {
|
||||
let normalized_addr = addr_normalize(addr);
|
||||
if contact.addr == normalized_addr {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
return Ok(0);
|
||||
@@ -1420,6 +1426,17 @@ fn cat_fingerprint(
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// determine whether the specified addr maps to the/a self addr
|
||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
if let Some(self_addr) = self.get_config(Config::ConfiguredAddr).await? {
|
||||
Ok(addr_cmp(&self_addr, addr))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2).to_lowercase();
|
||||
@@ -1515,8 +1532,6 @@ mod tests {
|
||||
async fn test_get_contacts() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
assert!(context.get_all_self_addrs().await?.is_empty());
|
||||
|
||||
// Bob is not in the contacts yet.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
@@ -189,29 +189,30 @@ impl Context {
|
||||
|
||||
/// Starts the IO scheduler.
|
||||
pub async fn start_io(&self) {
|
||||
info!(self, "starting IO");
|
||||
if self.inner.is_io_running().await {
|
||||
info!(self, "IO is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(false) = self.is_configured().await {
|
||||
warn!(self, "can not start io on a context that is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
info!(self, "starting IO");
|
||||
if let Err(err) = self.inner.scheduler.write().await.start(self.clone()).await {
|
||||
error!(self, "Failed to start IO: {}", err)
|
||||
{
|
||||
let l = &mut *self.inner.scheduler.write().await;
|
||||
if let Err(err) = l.start(self.clone()).await {
|
||||
error!(self, "Failed to start IO: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops the IO scheduler.
|
||||
pub async fn stop_io(&self) {
|
||||
// Sending an event wakes up event pollers (get_next_event)
|
||||
// so the caller of stop_io() can arrange for proper termination.
|
||||
// For this, the caller needs to instruct the event poller
|
||||
// to terminate on receiving the next event and then call stop_io()
|
||||
// which will emit the below event(s)
|
||||
info!(self, "stopping IO");
|
||||
|
||||
if let Err(err) = self.inner.stop_io().await {
|
||||
warn!(self, "failed to stop IO: {}", err);
|
||||
}
|
||||
self.inner.stop_io().await;
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
@@ -335,7 +336,6 @@ impl Context {
|
||||
let unset = "0";
|
||||
let l = LoginParam::load_candidate_params(self).await?;
|
||||
let l2 = LoginParam::load_configured_params(self).await?;
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await? as usize;
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize;
|
||||
@@ -420,7 +420,6 @@ impl Context {
|
||||
res.insert("socks5_enabled", socks5_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2.to_string());
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
"fetch_existing_msgs",
|
||||
self.get_config_int(Config::FetchExistingMsgs)
|
||||
@@ -643,9 +642,21 @@ impl Context {
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
async fn stop_io(&self) -> Result<()> {
|
||||
self.scheduler.write().await.stop().await?;
|
||||
Ok(())
|
||||
async fn is_io_running(&self) -> bool {
|
||||
self.scheduler.read().await.is_running()
|
||||
}
|
||||
|
||||
async fn stop_io(&self) {
|
||||
if self.is_io_running().await {
|
||||
let token = {
|
||||
let lock = &*self.scheduler.read().await;
|
||||
lock.pre_stop().await
|
||||
};
|
||||
{
|
||||
let lock = &mut *self.scheduler.write().await;
|
||||
lock.stop(token).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@ use mailparse::{parse_mail, SingleInfo};
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::contact;
|
||||
use crate::contact::{
|
||||
may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
|
||||
addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_create_id, dc_extract_grpid_from_rfc724_mid, dc_smeared_time};
|
||||
@@ -1102,11 +1103,11 @@ INSERT INTO msgs
|
||||
|
||||
// If you change which information is skipped if the message is trashed,
|
||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||
let trash = chat_id.is_trash() || (location_kml_is && msg.is_empty());
|
||||
let trash = chat_id.is_trash();
|
||||
|
||||
stmt.execute(paramsv![
|
||||
rfc724_mid,
|
||||
if trash { DC_CHAT_ID_TRASH } else { chat_id },
|
||||
chat_id,
|
||||
if trash { ContactId::UNDEFINED } else { from_id },
|
||||
if trash { ContactId::UNDEFINED } else { to_id },
|
||||
sort_timestamp,
|
||||
@@ -1414,17 +1415,7 @@ async fn create_or_lookup_group(
|
||||
ProtectionStatus::Unprotected
|
||||
};
|
||||
|
||||
async fn self_explicitly_added(
|
||||
context: &Context,
|
||||
mime_parser: &&mut MimeMessage,
|
||||
) -> Result<bool> {
|
||||
let ret = match mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
Some(member_addr) => context.is_self_addr(member_addr).await?,
|
||||
None => false,
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
if chat_id.is_none()
|
||||
&& !mime_parser.is_mailinglist_message()
|
||||
&& !grpid.is_empty()
|
||||
@@ -1433,7 +1424,7 @@ async fn create_or_lookup_group(
|
||||
&& mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
|
||||
// re-create explicitly left groups only if ourself is re-added
|
||||
&& (!chat::is_group_explicitly_left(context, &grpid).await?
|
||||
|| self_explicitly_added(context, &mime_parser).await?)
|
||||
|| mime_parser.get_header(HeaderDef::ChatGroupMemberAdded).map_or(false, |member_addr| addr_cmp(&self_addr, member_addr)))
|
||||
{
|
||||
// Group does not exist but should be created.
|
||||
if !allow_creation {
|
||||
@@ -1465,7 +1456,7 @@ async fn create_or_lookup_group(
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
info!(context, "adding to={:?} to chat id={}", to_id, new_chat_id);
|
||||
if to_id != ContactId::SELF
|
||||
if !Contact::addr_equals_contact(context, &self_addr, to_id).await?
|
||||
&& !chat::is_contact_in_chat(context, new_chat_id, to_id).await?
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, to_id).await?;
|
||||
@@ -1520,6 +1511,7 @@ async fn apply_group_changes(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
let mut recreate_member_list = false;
|
||||
let mut send_event_chat_modified = false;
|
||||
|
||||
@@ -1639,14 +1631,14 @@ async fn apply_group_changes(
|
||||
}
|
||||
}
|
||||
if !from_id.is_special()
|
||||
&& from_id != ContactId::SELF
|
||||
&& !Contact::addr_equals_contact(context, &self_addr, from_id).await?
|
||||
&& !chat::is_contact_in_chat(context, chat_id, from_id).await?
|
||||
&& removed_id != Some(from_id)
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, from_id).await?;
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
if to_id != ContactId::SELF
|
||||
if !Contact::addr_equals_contact(context, &self_addr, to_id).await?
|
||||
&& !chat::is_contact_in_chat(context, chat_id, to_id).await?
|
||||
&& removed_id != Some(to_id)
|
||||
{
|
||||
@@ -1924,6 +1916,9 @@ async fn create_adhoc_group(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Create a new ad-hoc group.
|
||||
let grpid = create_adhoc_grp_id(context, member_ids).await?;
|
||||
|
||||
// use subject as initial chat name
|
||||
let grpname = mime_parser
|
||||
.get_subject()
|
||||
@@ -1932,7 +1927,7 @@ async fn create_adhoc_group(
|
||||
let new_chat_id: ChatId = ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
"", // Ad hoc groups have no ID.
|
||||
&grpid,
|
||||
&grpname,
|
||||
create_blocked,
|
||||
ProtectionStatus::Unprotected,
|
||||
@@ -1948,6 +1943,56 @@ async fn create_adhoc_group(
|
||||
Ok(Some(new_chat_id))
|
||||
}
|
||||
|
||||
/// Creates ad-hoc group ID.
|
||||
///
|
||||
/// Algorithm:
|
||||
/// - sort normalized, lowercased, e-mail addresses alphabetically
|
||||
/// - put all e-mail addresses into a single string, separate the address by a single comma
|
||||
/// - sha-256 this string (without possibly terminating null-characters)
|
||||
/// - encode the first 64 bits of the sha-256 output as lowercase hex (results in 16 characters from the set [0-9a-f])
|
||||
///
|
||||
/// This ensures that different Delta Chat clients generate the same group ID unless some of them
|
||||
/// are hidden in BCC. This group ID is sent by DC in the messages sent to this chat,
|
||||
/// so having the same ID prevents group split.
|
||||
async fn create_adhoc_grp_id(context: &Context, member_ids: &[ContactId]) -> Result<String> {
|
||||
let member_cs = context.get_configured_addr().await?.to_lowercase();
|
||||
let query = format!(
|
||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=?",
|
||||
sql::repeat_vars(member_ids.len())?
|
||||
);
|
||||
let mut params = Vec::new();
|
||||
params.extend_from_slice(member_ids);
|
||||
params.push(ContactId::SELF);
|
||||
|
||||
let members = context
|
||||
.sql
|
||||
.query_map(
|
||||
query,
|
||||
rusqlite::params_from_iter(params),
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
let mut addrs = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
addrs.sort();
|
||||
let mut acc = member_cs.clone();
|
||||
for addr in &addrs {
|
||||
acc += ",";
|
||||
acc += &addr.to_lowercase();
|
||||
}
|
||||
Ok(acc)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(hex_hash(&members))
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn hex_hash(s: &str) -> String {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha256::digest(bytes);
|
||||
hex::encode(&result[..8])
|
||||
}
|
||||
|
||||
async fn check_verified_properties(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
@@ -2011,7 +2056,7 @@ async fn check_verified_properties(
|
||||
format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||
sql::repeat_vars(to_ids.len())
|
||||
sql::repeat_vars(to_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(to_ids),
|
||||
|row| {
|
||||
@@ -2218,6 +2263,14 @@ mod tests {
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_hex_hash() {
|
||||
let data = "hello world";
|
||||
|
||||
let res = hex_hash(data);
|
||||
assert_eq!(res, "b94d27b9934d3e08");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_grpid_simple() {
|
||||
let context = TestContext::new().await;
|
||||
@@ -4527,30 +4580,6 @@ Second thread."#;
|
||||
let bob_second_reply = bob.get_last_msg().await;
|
||||
assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id);
|
||||
|
||||
// Alice adds Fiona to both ad hoc groups.
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let (alice_fiona_contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Fiona",
|
||||
"fiona@example.net",
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
|
||||
chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_first_invite = alice.pop_sent_msg().await;
|
||||
fiona.recv_msg(&alice_first_invite).await;
|
||||
let fiona_first_invite = fiona.get_last_msg().await;
|
||||
|
||||
chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_second_invite = alice.pop_sent_msg().await;
|
||||
fiona.recv_msg(&alice_second_invite).await;
|
||||
let fiona_second_invite = fiona.get_last_msg().await;
|
||||
|
||||
// Fiona was added to two separate chats and should see two separate chats, even though they
|
||||
// don't have different group IDs to distinguish them.
|
||||
assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ impl EncryptHelper {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let addr = context.get_configured_addr().await?;
|
||||
let public_key = SignedPublicKey::load_self(context).await?;
|
||||
|
||||
Ok(EncryptHelper {
|
||||
@@ -381,7 +381,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
||||
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
||||
// TODO, remove this once deltachat::key::Key no longer exists.
|
||||
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
SignedPublicKey::load_self(context).await?;
|
||||
Ok(self_addr)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id IN ({})",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
sql::repeat_vars(msg_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::ToSql)
|
||||
|
||||
48
src/imap.rs
48
src/imap.rs
@@ -912,7 +912,7 @@ impl Imap {
|
||||
.execute(
|
||||
format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
sql::repeat_vars(row_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
@@ -949,7 +949,7 @@ impl Imap {
|
||||
.execute(
|
||||
format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
sql::repeat_vars(row_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
@@ -991,7 +991,7 @@ impl Imap {
|
||||
.execute(
|
||||
format!(
|
||||
"UPDATE imap SET target='' WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
sql::repeat_vars(row_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
@@ -1103,7 +1103,7 @@ impl Imap {
|
||||
.execute(
|
||||
format!(
|
||||
"DELETE FROM imap_markseen WHERE id IN ({})",
|
||||
sql::repeat_vars(rowid_set.len())
|
||||
sql::repeat_vars(rowid_set.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(rowid_set),
|
||||
)
|
||||
@@ -1206,9 +1206,10 @@ impl Imap {
|
||||
.session
|
||||
.as_mut()
|
||||
.context("IMAP No Connection established")?;
|
||||
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
let search_command = format!("FROM \"{}\"", self_addr);
|
||||
let uids = session
|
||||
.uid_search(get_imap_self_sent_search_command(context).await?)
|
||||
.uid_search(search_command)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
@@ -2169,18 +2170,6 @@ async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Compute the imap search expression for all self-sent mails (for all self addresses)
|
||||
pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
|
||||
// See https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4 for syntax of SEARCH and OR
|
||||
let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
|
||||
|
||||
for item in context.get_secondary_self_addrs().await? {
|
||||
search_command = format!("OR ({}) (FROM \"{}\")", search_command, item);
|
||||
}
|
||||
|
||||
Ok(search_command)
|
||||
}
|
||||
|
||||
/// Deprecated, use get_uid_next() and get_uidvalidity()
|
||||
pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result<(u32, u32)> {
|
||||
let key = format!("imap.mailbox.{}", folder);
|
||||
@@ -2549,27 +2538,4 @@ mod tests {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_imap_search_command() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"FROM "alice@example.org""#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ async fn set_self_key(
|
||||
}
|
||||
};
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
let addr = EmailAddress::new(&self_addr)?;
|
||||
let keypair = pgp::KeyPair {
|
||||
addr,
|
||||
|
||||
13
src/job.rs
13
src/job.rs
@@ -14,6 +14,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::Imap;
|
||||
use crate::location;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::param::{Param, Params};
|
||||
@@ -89,6 +90,8 @@ pub enum Action {
|
||||
ResyncFolders = 300,
|
||||
|
||||
// Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999
|
||||
MaybeSendLocations = 5005, // low priority ...
|
||||
MaybeSendLocationsEnded = 5007,
|
||||
SendMdn = 5010,
|
||||
}
|
||||
|
||||
@@ -102,6 +105,8 @@ impl From<Action> for Thread {
|
||||
UpdateRecentQuota => Thread::Imap,
|
||||
DownloadMsg => Thread::Imap,
|
||||
|
||||
MaybeSendLocations => Thread::Smtp,
|
||||
MaybeSendLocationsEnded => Thread::Smtp,
|
||||
SendMdn => Thread::Smtp,
|
||||
}
|
||||
}
|
||||
@@ -394,7 +399,7 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
}
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
sql::repeat_vars(job_ids.len())
|
||||
sql::repeat_vars(job_ids.len())?
|
||||
);
|
||||
context
|
||||
.sql
|
||||
@@ -566,6 +571,10 @@ async fn perform_job_action(
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::SendMdn => job.send_mdn(context, connection.smtp()).await,
|
||||
Action::MaybeSendLocations => location::job_maybe_send_locations(context, job).await,
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
|
||||
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
|
||||
@@ -638,7 +647,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
info!(context, "interrupt: imap");
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
}
|
||||
Action::SendMdn => {
|
||||
Action::MaybeSendLocations | Action::MaybeSendLocationsEnded | Action::SendMdn => {
|
||||
info!(context, "interrupt: smtp");
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
|
||||
@@ -91,17 +91,16 @@ impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=?
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
paramsv![],
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
@@ -199,7 +198,7 @@ impl DcSecretKey for SignedSecretKey {
|
||||
}
|
||||
|
||||
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let addr = context.get_configured_addr().await?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
|
||||
320
src/location.rs
320
src/location.rs
@@ -1,20 +1,19 @@
|
||||
//! Location handling.
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use async_std::channel::Receiver;
|
||||
use async_std::future::timeout;
|
||||
use anyhow::{ensure, Result};
|
||||
use bitflags::bitflags;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{duration_to_str, time};
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::job::{self, Job};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Params;
|
||||
use crate::stock_str;
|
||||
|
||||
/// Location record
|
||||
@@ -227,11 +226,32 @@ pub async fn send_locations_to_chat(
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
context.interrupt_location().await;
|
||||
schedule_maybe_send_locations(context, false).await?;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
job::Action::MaybeSendLocationsEnded,
|
||||
chat_id.to_u32(),
|
||||
Params::new(),
|
||||
seconds + 1,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) -> Result<()> {
|
||||
if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await? {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(job::Action::MaybeSendLocations, 0, Params::new(), 60),
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether `chat_id` or any chat is sending locations.
|
||||
///
|
||||
/// If `chat_id` is `Some` only that chat is checked, otherwise returns `true` if any chat
|
||||
@@ -298,13 +318,13 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
).await {
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
info!(context, "stored location for chat {}", chat_id);
|
||||
continue_streaming = true;
|
||||
}
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
};
|
||||
schedule_maybe_send_locations(context, false).await.ok();
|
||||
}
|
||||
|
||||
continue_streaming
|
||||
@@ -403,7 +423,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
|
||||
@@ -587,140 +607,147 @@ pub(crate) async fn save(
|
||||
Ok(newest_location_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
|
||||
loop {
|
||||
let next_event = match maybe_send_locations(context).await {
|
||||
Err(err) => {
|
||||
warn!(context, "maybe_send_locations failed: {}", err);
|
||||
Some(60) // Retry one minute later.
|
||||
}
|
||||
Ok(next_event) => next_event,
|
||||
};
|
||||
|
||||
let duration = if let Some(next_event) = next_event {
|
||||
Duration::from_secs(next_event)
|
||||
} else {
|
||||
Duration::from_secs(86400)
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Location loop is waiting for {} or interrupt",
|
||||
duration_to_str(duration)
|
||||
);
|
||||
timeout(duration, interrupt_receiver.recv()).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns number of seconds until the next time location streaming for some chat ends
|
||||
/// automatically.
|
||||
async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
let mut next_event: Option<u64> = None;
|
||||
|
||||
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
|
||||
let now = time();
|
||||
let mut continue_streaming = false;
|
||||
info!(
|
||||
context,
|
||||
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
|
||||
);
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, locations_send_begin, locations_send_until, locations_last_sent
|
||||
FROM chats
|
||||
WHERE locations_send_until>0",
|
||||
[],
|
||||
"SELECT id, locations_send_begin, locations_last_sent \
|
||||
FROM chats \
|
||||
WHERE locations_send_until>?;",
|
||||
paramsv![now],
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let locations_send_begin: i64 = row.get(1)?;
|
||||
let locations_send_until: i64 = row.get(2)?;
|
||||
let locations_last_sent: i64 = row.get(3)?;
|
||||
Ok((
|
||||
chat_id,
|
||||
locations_send_begin,
|
||||
locations_send_until,
|
||||
locations_last_sent,
|
||||
))
|
||||
let locations_last_sent: i64 = row.get(2)?;
|
||||
continue_streaming = true;
|
||||
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||
}
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to query location streaming chats")?;
|
||||
.await;
|
||||
|
||||
for (chat_id, locations_send_begin, locations_send_until, locations_last_sent) in rows {
|
||||
if locations_send_begin > 0 && locations_send_until > now {
|
||||
let can_send = now > locations_last_sent + 60;
|
||||
let has_locations = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) \
|
||||
if let Ok(rows) = rows {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
{
|
||||
let conn = job_try!(context.sql.get_conn().await);
|
||||
|
||||
let mut stmt_locations = job_try!(conn.prepare_cached(
|
||||
"SELECT id \
|
||||
FROM locations \
|
||||
WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND timestamp>? \
|
||||
AND independent=0",
|
||||
paramsv![ContactId::SELF, locations_send_begin, locations_last_sent,],
|
||||
)
|
||||
.await?;
|
||||
AND independent=0 \
|
||||
ORDER BY timestamp;",
|
||||
));
|
||||
|
||||
next_event = next_event
|
||||
.into_iter()
|
||||
.chain(u64::try_from(locations_send_until - now).into_iter())
|
||||
.min();
|
||||
|
||||
if has_locations {
|
||||
if can_send {
|
||||
// Send location-only message.
|
||||
// Pending locations are attached automatically to every message,
|
||||
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
|
||||
if !stmt_locations
|
||||
.exists(paramsv![
|
||||
ContactId::SELF,
|
||||
*locations_send_begin,
|
||||
*locations_last_sent,
|
||||
])
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// if there is no new location, there's nothing to send.
|
||||
// however, maybe we want to bypass this test eg. 15 minutes
|
||||
} else {
|
||||
// pending locations are attached automatically to every message,
|
||||
// so also to this empty text message.
|
||||
info!(
|
||||
context,
|
||||
"Chat {} has pending locations, sending them.", chat_id
|
||||
);
|
||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||
//
|
||||
// for optimisation and to avoid flooding the sending queue,
|
||||
// we could sending these messages only if we're really online.
|
||||
// the easiest way to determine this, is to check for an empty message queue.
|
||||
// (might not be 100%, however, as positions are sent combined later
|
||||
// and dc_set_location() is typically called periodically, this is ok)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.hidden = true;
|
||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
} else {
|
||||
// Wait until pending locations can be sent.
|
||||
info!(
|
||||
context,
|
||||
"Chat {} has pending locations, but they can't be sent yet.", chat_id
|
||||
);
|
||||
next_event = next_event
|
||||
.into_iter()
|
||||
.chain(u64::try_from(locations_last_sent + 61 - now).into_iter())
|
||||
.min();
|
||||
msgs.push((*chat_id, msg));
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Chat {} has location streaming enabled, but no pending locations.", chat_id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Location streaming was either explicitly disabled (locations_send_begin = 0) or
|
||||
// locations_send_until is in the past.
|
||||
info!(
|
||||
context,
|
||||
"Disabling location streaming for chat {}.", chat_id
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=0, locations_send_until=0 \
|
||||
WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await
|
||||
.context("failed to disable location streaming")?;
|
||||
}
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
for (chat_id, mut msg) in msgs.into_iter() {
|
||||
// TODO: better error handling
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(next_event)
|
||||
if continue_streaming {
|
||||
job_try!(schedule_maybe_send_locations(context, true).await);
|
||||
}
|
||||
job::Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
pub(crate) async fn job_maybe_send_locations_ended(
|
||||
context: &Context,
|
||||
job: &mut Job,
|
||||
) -> job::Status {
|
||||
// this function is called when location-streaming _might_ have ended for a chat.
|
||||
// the function checks, if location-streaming is really ended;
|
||||
// if so, a device-message is added if not yet done.
|
||||
|
||||
let chat_id = ChatId::new(job.foreign_id);
|
||||
|
||||
let (send_begin, send_until) = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
let now = time();
|
||||
if !(send_begin != 0 && now <= send_until) {
|
||||
// still streaming -
|
||||
// may happen as several calls to dc_send_locations_to_chat()
|
||||
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
|
||||
if !(send_begin == 0 && send_until == 0) {
|
||||
// not streaming, device-message already sent
|
||||
job_try!(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=0, locations_send_until=0 \
|
||||
WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
job_try!(chat::add_info_msg(context, chat_id, &stock_str, now).await);
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
job::Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -728,7 +755,6 @@ mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
@@ -789,68 +815,4 @@ mod tests {
|
||||
assert!(!is_marker(" "));
|
||||
assert!(!is_marker("\t"));
|
||||
}
|
||||
|
||||
/// Tests that location.kml is hidden.
|
||||
#[async_std::test]
|
||||
async fn receive_location_kml() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
br#"Subject: Hello
|
||||
Message-ID: hello@example.net
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.net>
|
||||
Date: Mon, 20 Dec 2021 00:00:00 +0000
|
||||
Chat-Version: 1.0
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Text message."#,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert_eq!(received_msg.text.unwrap(), "Text message.");
|
||||
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
br#"Subject: locations
|
||||
MIME-Version: 1.0
|
||||
To: <alice@example.org>
|
||||
From: <bob@example.net>
|
||||
Date: Tue, 21 Dec 2021 00:00:00 +0000
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <foobar@example.net>
|
||||
Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
|
||||
|
||||
|
||||
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
|
||||
|
||||
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
|
||||
Content-Type: application/vnd.google-earth.kml+xml
|
||||
Content-Disposition: attachment; filename="location.kml"
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document addr="bob@example.net">
|
||||
<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
|
||||
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Received location message is not visible, last message stays the same.
|
||||
let received_msg2 = alice.get_last_msg().await;
|
||||
assert_eq!(received_msg2.id, received_msg.id);
|
||||
|
||||
let locations = get_range(&alice, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,8 @@ impl LoginParam {
|
||||
let prefix = "configured_";
|
||||
let sql = &context.sql;
|
||||
|
||||
context.set_primary_self_addr(&self.addr).await?;
|
||||
let key = format!("{}addr", prefix);
|
||||
sql.set_raw_config(key, Some(&self.addr)).await?;
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
sql.set_raw_config(key, Some(&self.imap.server)).await?;
|
||||
|
||||
@@ -1296,7 +1296,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE m.id IN ({}) AND m.chat_id>9",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
sql::repeat_vars(msg_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(&msg_ids),
|
||||
|row| {
|
||||
|
||||
@@ -133,7 +133,7 @@ impl<'a> MimeFactory<'a> {
|
||||
) -> Result<MimeFactory<'a>> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let from_addr = context.get_configured_addr().await?;
|
||||
let config_displayname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
@@ -233,7 +233,7 @@ impl<'a> MimeFactory<'a> {
|
||||
ensure!(!msg.chat_id.is_special(), "Invalid chat id");
|
||||
|
||||
let contact = Contact::load_from_db(context, msg.from_id).await?;
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let from_addr = context.get_configured_addr().await?;
|
||||
let from_displayname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
@@ -271,8 +271,7 @@ impl<'a> MimeFactory<'a> {
|
||||
&self,
|
||||
context: &Context,
|
||||
) -> Result<Vec<(Option<Peerstate>, &str)>> {
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
let mut res = Vec::new();
|
||||
for (_, addr) in self
|
||||
.recipients
|
||||
@@ -850,12 +849,9 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group {
|
||||
// Send group ID unless it is an ad hoc group that has no ID.
|
||||
if !chat.grpid.is_empty() {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||
}
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||
|
||||
let encoded = encode_words(&chat.name);
|
||||
headers
|
||||
|
||||
@@ -479,7 +479,7 @@ mod tests {
|
||||
let mut keyring = Keyring::new();
|
||||
keyring.add(KEYS.alice_public.clone());
|
||||
keyring.add(KEYS.bob_public.clone());
|
||||
futures_lite::future::block_on(pk_encrypt(
|
||||
async_std::task::block_on(pk_encrypt(
|
||||
CLEARTEXT,
|
||||
keyring,
|
||||
Some(KEYS.alice_secret.clone()),
|
||||
@@ -492,7 +492,7 @@ mod tests {
|
||||
let mut keyring = Keyring::new();
|
||||
keyring.add(KEYS.alice_public.clone());
|
||||
keyring.add(KEYS.bob_public.clone());
|
||||
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
|
||||
async_std::task::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
|
||||
});
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -44,7 +44,7 @@ static P_AKTIVIX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "newyear.aktivix.org",
|
||||
port: 587,
|
||||
port: 25,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
@@ -851,6 +851,10 @@ static P_NAUTA_CU: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::E2eeEnabled,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MediaQuality,
|
||||
value: "1",
|
||||
@@ -979,7 +983,7 @@ static P_QQ: Lazy<Provider> = Lazy::new(|| {
|
||||
overview_page: "https://providers.delta.chat/qq",
|
||||
server: vec![
|
||||
Server { protocol: Imap, socket: Ssl, hostname: "imap.qq.com", port: 993, username_pattern: Emaillocalpart },
|
||||
Server { protocol: Smtp, socket: Ssl, hostname: "smtp.qq.com", port: 465, username_pattern: Email },
|
||||
Server { protocol: Smtp, socket: Starttls, hostname: "smtp.qq.com", port: 465, username_pattern: Email },
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
@@ -1398,9 +1402,7 @@ static P_YGGMAIL: Lazy<Provider> = Lazy::new(|| {
|
||||
Server { protocol: Imap, socket: Plain, hostname: "localhost", port: 1143, username_pattern: Email },
|
||||
Server { protocol: Smtp, socket: Plain, hostname: "localhost", port: 1025, username_pattern: Email },
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault { key: Config::MvboxMove, value: "0" },
|
||||
]),
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
@@ -1851,4 +1853,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
});
|
||||
|
||||
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 5, 3));
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));
|
||||
|
||||
@@ -65,12 +65,13 @@ async fn generate_verification_qr(context: &Context) -> Result<String> {
|
||||
}
|
||||
|
||||
fn inner_generate_secure_join_qr_code(
|
||||
qrcode_description: &str,
|
||||
raw_qrcode_description: &str,
|
||||
qrcode_content: &str,
|
||||
color: &str,
|
||||
avatar: Option<Vec<u8>>,
|
||||
avatar_letter: char,
|
||||
) -> Result<String> {
|
||||
let qrcode_description = &escaper::encode_minimal(raw_qrcode_description);
|
||||
// config
|
||||
let width = 515.0;
|
||||
let height = 630.0;
|
||||
@@ -261,21 +262,3 @@ fn inner_generate_secure_join_qr_code(
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_svg_escaping() {
|
||||
let svg = inner_generate_secure_join_qr_code(
|
||||
"descr123 \" < > &",
|
||||
"qr-code-content",
|
||||
"#000000",
|
||||
None,
|
||||
'X',
|
||||
)
|
||||
.unwrap();
|
||||
assert!(svg.contains("descr123 " < > &"))
|
||||
}
|
||||
}
|
||||
|
||||
144
src/scheduler.rs
144
src/scheduler.rs
@@ -12,7 +12,6 @@ use crate::dc_tools::time;
|
||||
use crate::ephemeral::{self, delete_expired_imap_messages};
|
||||
use crate::imap::Imap;
|
||||
use crate::job::{self, Thread};
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::smtp::{send_smtp_messages, Smtp};
|
||||
use crate::sql;
|
||||
@@ -21,6 +20,8 @@ use self::connectivity::ConnectivityStore;
|
||||
|
||||
pub(crate) mod connectivity;
|
||||
|
||||
pub(crate) struct StopToken;
|
||||
|
||||
/// Job and connection scheduler.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -37,8 +38,6 @@ pub(crate) enum Scheduler {
|
||||
smtp_handle: Option<task::JoinHandle<()>>,
|
||||
ephemeral_handle: Option<task::JoinHandle<()>>,
|
||||
ephemeral_interrupt_send: Sender<()>,
|
||||
location_handle: Option<task::JoinHandle<()>>,
|
||||
location_interrupt_send: Sender<()>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -68,10 +67,6 @@ impl Context {
|
||||
pub(crate) async fn interrupt_ephemeral_task(&self) {
|
||||
self.scheduler.read().await.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_location(&self) {
|
||||
self.scheduler.read().await.interrupt_location().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConnectionHandlers) {
|
||||
@@ -81,15 +76,16 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
let ImapConnectionHandlers {
|
||||
mut connection,
|
||||
stop_receiver,
|
||||
shutdown_sender,
|
||||
} = inbox_handlers;
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let fut = async move {
|
||||
started
|
||||
.send(())
|
||||
.await
|
||||
.expect("inbox loop, missing started receiver");
|
||||
let ctx = ctx1;
|
||||
if let Err(err) = started.send(()).await {
|
||||
warn!(ctx, "inbox loop, missing started receiver: {}", err);
|
||||
return;
|
||||
};
|
||||
|
||||
let mut info = InterruptInfo::default();
|
||||
loop {
|
||||
@@ -135,6 +131,10 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
})
|
||||
.race(fut)
|
||||
.await;
|
||||
shutdown_sender
|
||||
.send(())
|
||||
.await
|
||||
.expect("inbox loop, missing shutdown receiver");
|
||||
}
|
||||
|
||||
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
|
||||
@@ -259,16 +259,17 @@ async fn simple_imap_loop(
|
||||
let ImapConnectionHandlers {
|
||||
mut connection,
|
||||
stop_receiver,
|
||||
shutdown_sender,
|
||||
} = inbox_handlers;
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
|
||||
let fut = async move {
|
||||
started
|
||||
.send(())
|
||||
.await
|
||||
.expect("simple imap loop, missing started receive");
|
||||
let ctx = ctx1;
|
||||
if let Err(err) = started.send(()).await {
|
||||
warn!(&ctx, "simple imap loop, missing started receiver: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
fetch_idle(&ctx, &mut connection, folder).await;
|
||||
@@ -282,6 +283,10 @@ async fn simple_imap_loop(
|
||||
})
|
||||
.race(fut)
|
||||
.await;
|
||||
shutdown_sender
|
||||
.send(())
|
||||
.await
|
||||
.expect("simple imap loop, missing shutdown receiver");
|
||||
}
|
||||
|
||||
async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnectionHandlers) {
|
||||
@@ -291,16 +296,17 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
let SmtpConnectionHandlers {
|
||||
mut connection,
|
||||
stop_receiver,
|
||||
shutdown_sender,
|
||||
idle_interrupt_receiver,
|
||||
} = smtp_handlers;
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let fut = async move {
|
||||
started
|
||||
.send(())
|
||||
.await
|
||||
.expect("smtp loop, missing started receiver");
|
||||
let ctx = ctx1;
|
||||
if let Err(err) = started.send(()).await {
|
||||
warn!(&ctx, "smtp loop, missing started receiver: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut timeout = None;
|
||||
let mut interrupt_info = Default::default();
|
||||
@@ -372,15 +378,15 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
})
|
||||
.race(fut)
|
||||
.await;
|
||||
shutdown_sender
|
||||
.send(())
|
||||
.await
|
||||
.expect("smtp loop, missing shutdown receiver");
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
/// Start the scheduler, returns error if it is already running.
|
||||
/// Start the scheduler, panics if it is already running.
|
||||
pub async fn start(&mut self, ctx: Context) -> Result<()> {
|
||||
if self.is_running() {
|
||||
bail!("scheduler is already started");
|
||||
}
|
||||
|
||||
let (mvbox, mvbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
let (sentbox, sentbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
let (smtp, smtp_handlers) = SmtpConnectionState::new();
|
||||
@@ -393,7 +399,6 @@ impl Scheduler {
|
||||
let mut sentbox_handle = None;
|
||||
let (smtp_start_send, smtp_start_recv) = channel::bounded(1);
|
||||
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
|
||||
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
|
||||
|
||||
let inbox_handle = {
|
||||
let ctx = ctx.clone();
|
||||
@@ -417,7 +422,7 @@ impl Scheduler {
|
||||
mvbox_start_send
|
||||
.send(())
|
||||
.await
|
||||
.context("mvbox start send, missing receiver")?;
|
||||
.expect("mvbox start send, missing receiver");
|
||||
mvbox_handlers
|
||||
.connection
|
||||
.connectivity
|
||||
@@ -440,7 +445,7 @@ impl Scheduler {
|
||||
sentbox_start_send
|
||||
.send(())
|
||||
.await
|
||||
.context("sentbox start send, missing receiver")?;
|
||||
.expect("sentbox start send, missing receiver");
|
||||
sentbox_handlers
|
||||
.connection
|
||||
.connectivity
|
||||
@@ -462,13 +467,6 @@ impl Scheduler {
|
||||
}))
|
||||
};
|
||||
|
||||
let location_handle = {
|
||||
let ctx = ctx.clone();
|
||||
Some(task::spawn(async move {
|
||||
location::location_loop(&ctx, location_interrupt_recv).await;
|
||||
}))
|
||||
};
|
||||
|
||||
*self = Scheduler::Running {
|
||||
inbox,
|
||||
mvbox,
|
||||
@@ -480,8 +478,6 @@ impl Scheduler {
|
||||
smtp_handle,
|
||||
ephemeral_handle,
|
||||
ephemeral_interrupt_send,
|
||||
location_handle,
|
||||
location_interrupt_send,
|
||||
};
|
||||
|
||||
// wait for all loops to be started
|
||||
@@ -557,21 +553,11 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
async fn interrupt_location(&self) {
|
||||
if let Scheduler::Running {
|
||||
ref location_interrupt_send,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
location_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Halt the scheduler.
|
||||
pub(crate) async fn stop(&mut self) -> Result<()> {
|
||||
/// Halts the scheduler, must be called first, and then `stop`.
|
||||
pub(crate) async fn pre_stop(&self) -> StopToken {
|
||||
match self {
|
||||
Scheduler::Stopped => {
|
||||
bail!("scheduler is already stopped");
|
||||
panic!("WARN: already stopped");
|
||||
}
|
||||
Scheduler::Running {
|
||||
inbox,
|
||||
@@ -582,23 +568,40 @@ impl Scheduler {
|
||||
sentbox_handle,
|
||||
smtp,
|
||||
smtp_handle,
|
||||
ephemeral_handle,
|
||||
location_handle,
|
||||
..
|
||||
} => {
|
||||
if inbox_handle.is_some() {
|
||||
inbox.stop().await?;
|
||||
inbox.stop().await;
|
||||
}
|
||||
if mvbox_handle.is_some() {
|
||||
mvbox.stop().await?;
|
||||
mvbox.stop().await;
|
||||
}
|
||||
if sentbox_handle.is_some() {
|
||||
sentbox.stop().await?;
|
||||
sentbox.stop().await;
|
||||
}
|
||||
if smtp_handle.is_some() {
|
||||
smtp.stop().await?;
|
||||
smtp.stop().await;
|
||||
}
|
||||
|
||||
StopToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Halt the scheduler, must only be called after pre_stop.
|
||||
pub(crate) async fn stop(&mut self, _t: StopToken) {
|
||||
match self {
|
||||
Scheduler::Stopped => {
|
||||
panic!("WARN: already stopped");
|
||||
}
|
||||
Scheduler::Running {
|
||||
inbox_handle,
|
||||
mvbox_handle,
|
||||
sentbox_handle,
|
||||
smtp_handle,
|
||||
ephemeral_handle,
|
||||
..
|
||||
} => {
|
||||
if let Some(handle) = inbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
@@ -614,12 +617,8 @@ impl Scheduler {
|
||||
if let Some(handle) = ephemeral_handle.take() {
|
||||
handle.cancel().await;
|
||||
}
|
||||
if let Some(handle) = location_handle.take() {
|
||||
handle.cancel().await;
|
||||
}
|
||||
|
||||
*self = Scheduler::Stopped;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,6 +632,8 @@ impl Scheduler {
|
||||
/// Connection state logic shared between imap and smtp connections.
|
||||
#[derive(Debug)]
|
||||
struct ConnectionState {
|
||||
/// Channel to notify that shutdown has completed.
|
||||
shutdown_receiver: Receiver<()>,
|
||||
/// Channel to interrupt the whole connection.
|
||||
stop_sender: Sender<()>,
|
||||
/// Channel to interrupt idle.
|
||||
@@ -643,13 +644,14 @@ struct ConnectionState {
|
||||
|
||||
impl ConnectionState {
|
||||
/// Shutdown this connection completely.
|
||||
async fn stop(&self) -> Result<()> {
|
||||
async fn stop(&self) {
|
||||
// Trigger shutdown of the run loop.
|
||||
self.stop_sender
|
||||
.send(())
|
||||
.await
|
||||
.context("failed to stop, missing receiver")?;
|
||||
Ok(())
|
||||
.expect("stop, missing receiver");
|
||||
// Wait for a notification that the run loop has been shutdown.
|
||||
self.shutdown_receiver.recv().await.ok();
|
||||
}
|
||||
|
||||
async fn interrupt(&self, info: InterruptInfo) {
|
||||
@@ -666,15 +668,18 @@ pub(crate) struct SmtpConnectionState {
|
||||
impl SmtpConnectionState {
|
||||
fn new() -> (Self, SmtpConnectionHandlers) {
|
||||
let (stop_sender, stop_receiver) = channel::bounded(1);
|
||||
let (shutdown_sender, shutdown_receiver) = channel::bounded(1);
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = SmtpConnectionHandlers {
|
||||
connection: Smtp::new(),
|
||||
stop_receiver,
|
||||
shutdown_sender,
|
||||
idle_interrupt_receiver,
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
shutdown_receiver,
|
||||
stop_sender,
|
||||
idle_interrupt_sender,
|
||||
connectivity: handlers.connection.connectivity.clone(),
|
||||
@@ -691,15 +696,15 @@ impl SmtpConnectionState {
|
||||
}
|
||||
|
||||
/// Shutdown this connection completely.
|
||||
async fn stop(&self) -> Result<()> {
|
||||
self.state.stop().await?;
|
||||
Ok(())
|
||||
async fn stop(&self) {
|
||||
self.state.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
struct SmtpConnectionHandlers {
|
||||
connection: Smtp,
|
||||
stop_receiver: Receiver<()>,
|
||||
shutdown_sender: Sender<()>,
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
}
|
||||
|
||||
@@ -712,14 +717,17 @@ impl ImapConnectionState {
|
||||
/// Construct a new connection.
|
||||
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
let (stop_sender, stop_receiver) = channel::bounded(1);
|
||||
let (shutdown_sender, shutdown_receiver) = channel::bounded(1);
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = ImapConnectionHandlers {
|
||||
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
|
||||
stop_receiver,
|
||||
shutdown_sender,
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
shutdown_receiver,
|
||||
stop_sender,
|
||||
idle_interrupt_sender,
|
||||
connectivity: handlers.connection.connectivity.clone(),
|
||||
@@ -736,9 +744,8 @@ impl ImapConnectionState {
|
||||
}
|
||||
|
||||
/// Shutdown this connection completely.
|
||||
async fn stop(&self) -> Result<()> {
|
||||
self.state.stop().await?;
|
||||
Ok(())
|
||||
async fn stop(&self) {
|
||||
self.state.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,6 +753,7 @@ impl ImapConnectionState {
|
||||
struct ImapConnectionHandlers {
|
||||
connection: Imap,
|
||||
stop_receiver: Receiver<()>,
|
||||
shutdown_sender: Sender<()>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
||||
@@ -449,7 +449,7 @@ impl Context {
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain = dc_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
|
||||
let domain = dc_tools::EmailAddress::new(&self.get_configured_addr().await?)?.domain;
|
||||
ret += &format!(
|
||||
"<h3>{}</h3><ul>",
|
||||
stock_str::storage_on_domain(self, domain).await
|
||||
|
||||
@@ -66,7 +66,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
||||
.is_none();
|
||||
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, group).await;
|
||||
let auth = token::lookup_or_new(context, Namespace::Auth, group).await;
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr = context.get_configured_addr().await?;
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
@@ -87,12 +87,6 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
||||
let qr = if let Some(group) = group {
|
||||
// parameters used: a=g=x=i=s=
|
||||
let chat = Chat::load_from_db(context, group).await?;
|
||||
if chat.grpid.is_empty() {
|
||||
bail!(
|
||||
"can't generate securejoin QR code for ad-hoc group {}",
|
||||
group
|
||||
);
|
||||
}
|
||||
let group_name = chat.get_name();
|
||||
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
||||
if sync_token {
|
||||
@@ -716,7 +710,6 @@ mod tests {
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
@@ -1315,25 +1308,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_adhoc_group_no_qr() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
let mime = br#"Subject: First thread
|
||||
Message-ID: first@example.org
|
||||
To: Alice <alice@example.org>, Bob <bob@example.net>
|
||||
From: Claire <claire@example.org>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First thread."#;
|
||||
|
||||
dc_receive_imf(&alice, mime, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
|
||||
assert!(dc_get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Try number {} to send message {} over SMTP", retries, msg_id
|
||||
"Retry number {} to send message {} over SMTP", retries, msg_id
|
||||
);
|
||||
|
||||
let recipients_list = recipients
|
||||
|
||||
18
src/sql.rs
18
src/sql.rs
@@ -34,17 +34,6 @@ macro_rules! paramsv {
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! params_iterv {
|
||||
($($param:expr),+ $(,)?) => {
|
||||
vec![$(&$param as &dyn $crate::ToSql),+]
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn params_iter(iter: &[impl crate::ToSql]) -> impl Iterator<Item = &dyn crate::ToSql> {
|
||||
iter.iter().map(|item| item as &dyn crate::ToSql)
|
||||
}
|
||||
|
||||
mod migrations;
|
||||
|
||||
/// A wrapper around the underlying Sqlite3 object.
|
||||
@@ -843,10 +832,13 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
///
|
||||
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
|
||||
/// parameter lists.
|
||||
pub fn repeat_vars(count: usize) -> String {
|
||||
pub fn repeat_vars(count: usize) -> Result<String> {
|
||||
if count == 0 {
|
||||
bail!("Must have at least one repeat variable");
|
||||
}
|
||||
let mut s = "?,".repeat(count);
|
||||
s.pop(); // Remove trailing comma
|
||||
s
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -395,7 +395,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
|
||||
|
||||
if dbversion < 71 {
|
||||
info!(context, "[migration] v71");
|
||||
if let Ok(addr) = context.get_primary_self_addr().await {
|
||||
if let Ok(addr) = context.get_configured_addr().await {
|
||||
if let Ok(domain) = addr.parse::<EmailAddress>().map(|email| email.domain) {
|
||||
context
|
||||
.set_config(
|
||||
|
||||
@@ -88,13 +88,6 @@ impl TestContextBuilder {
|
||||
self.with_key_pair(bob_keypair())
|
||||
}
|
||||
|
||||
/// Configures as fiona@example.net with fixed secret key.
|
||||
///
|
||||
/// This is a shortcut for `.with_key_pair(bob_keypair()).
|
||||
pub fn configure_fiona(self) -> Self {
|
||||
self.with_key_pair(fiona_keypair())
|
||||
}
|
||||
|
||||
/// Configures the new [`TestContext`] with the provided [`KeyPair`].
|
||||
///
|
||||
/// This will extract the email address from the key and configure the context with the
|
||||
@@ -190,13 +183,6 @@ impl TestContext {
|
||||
Self::builder().configure_bob().build().await
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which configures fiona@example.net with a fixed key.
|
||||
pub async fn new_fiona() -> Self {
|
||||
Self::builder().configure_fiona().build().await
|
||||
}
|
||||
|
||||
/// Internal constructor.
|
||||
///
|
||||
/// `name` is used to identify this context in e.g. log output. This is useful mostly
|
||||
@@ -421,7 +407,7 @@ impl TestContext {
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
let addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let addr = other.ctx.get_configured_addr().await.unwrap();
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
// origin when creating this contact.
|
||||
let (contact_id, modified) =
|
||||
@@ -705,24 +691,6 @@ pub fn bob_keypair() -> KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for fiona@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn fiona_keypair() -> key::KeyPair {
|
||||
let addr = EmailAddress::new("fiona@example.net").unwrap();
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/fiona-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility to help wait for and retrieve events.
|
||||
///
|
||||
/// This buffers the events in order they are emitted. This allows consuming events in
|
||||
|
||||
Reference in New Issue
Block a user