Compare commits

..

26 Commits

Author SHA1 Message Date
link2xt
a9dbf05d8d ci: update to Python 3.13 2024-06-04 16:56:16 +00:00
link2xt
90c30879b1 refactor(imap): make select_folder() accept non-optional folder
If no folder should be selected,
`maybe_close_folder()` can be called directly.
2024-06-04 13:31:40 +00:00
link2xt
0ca1318118 fix: retry sending MDNs on temporary error
Postfix returns 421 4.4.2 Error: timeout exceeded
when overloaded by CI. If MDN is not retried in this case,
`test_qr_readreceipt` fails.
2024-06-04 12:54:47 +00:00
link2xt
0be639b244 chore(release): prepare for 1.140.0 2024-06-04 12:01:55 +00:00
Sebastian Klähn
48b4cfc247 feat: add config option to enable iroh (#5607)
Co-authored-by: link2xt <link2xt@testrun.org>
2024-06-03 22:59:29 +00:00
link2xt
a4037b8278 refactor: put duplicate code into lookup_chat_or_create_adhoc_group 2024-06-03 20:46:30 +00:00
link2xt
30405056e3 fix: Do not fail to send images with wrong extensions
Try to guess the image format based on its content first.

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2024-06-03 14:56:10 +00:00
link2xt
0fbab7147a fix: prefer Chat-Group-ID over references for new groups 2024-06-03 14:10:32 +00:00
link2xt
de57ef5ac7 refactor: factor create_adhoc_group() call out of create_group() 2024-06-03 14:10:32 +00:00
link2xt
f48a047fe0 test: refactor test_alias_* into 8 separate tests 2024-06-03 14:10:32 +00:00
link2xt
8ba08432c5 docs: fix a typo in test_partial_group_consistency() 2024-06-03 14:10:32 +00:00
link2xt
bf34bd3a62 docs: create_group() does not find chats, only creates them 2024-06-03 14:10:32 +00:00
B. Petersen
21845ca5ea docs: add vCard as supported standard 2024-06-02 15:33:56 +02:00
iequidoo
768ef772bb feat: Add a db index for reactions by msg_id (#5507)
This should speed up `get_msg_reactions()` filtering reactions by `msg_id`, but also queries in
other places involving both `msg_id` and `contact_id`.
2024-06-01 11:43:15 -03:00
link2xt
69842c18f7 test: fix logging of TestContext created using TestContext::new_alice()
Before this fix LogSink was dropped immediately,
resulting in no logs printed for contexts created using
TextContext::new_alice(), but printed for contexts created
using TextContext::new().
2024-05-30 21:14:41 +00:00
link2xt
42a7cd3eea fix: allow creation of groups by outgoing messages without recipients
`!to_ids().is_empty()` check is needed in cases of 1:1 chat creation
because otherwise `to_id` is undefined,
but in case of outgoing group message without recipients
observed on a second device creating a group should be allowed.
2024-05-30 18:01:32 +00:00
link2xt
b7e5b906d1 build: unpin OpenSSL version and update to OpenSSL 3.3.0
Previously OpenSSL was pinned due to problems
with Zig toolchain used back then
and then not unpinned even after switching from Zig to Nix
due to <https://github.com/alexcrichton/openssl-src-rs/issues/235>.
The problem should be fixed now
and we can try to unpin OpenSSL again.
2024-05-30 16:56:10 +00:00
link2xt
ad271fac80 ci: remove cargo-nextest bug workaround
The problem should be fixed
since nextest 0.9.72.
2024-05-30 13:46:29 +00:00
iequidoo
70ad323c9a fix: AEAP: Remove old peerstate verified_key instead of removing the whole peerstate (#5535)
When doing an AEAP transition, we mustn't just delete the old peerstate as this would break
encryption to it. This is critical for non-verified groups -- if we can't encrypt to the old
address, we can't securely remove it from the group (to add the new one instead).
2024-05-30 10:38:39 -03:00
iequidoo
27bf4c37a7 feat: Map *.wav to Viewtype::Audio (#5633)
It seems there are no problems with playing WAV on all modern platforms, so such files should be
displayed in the "AUDIO", not "FILES" tab in the UIs.
2024-05-30 10:33:10 -03:00
iequidoo
1cc31c1038 feat: Remove limit on number of email recipients for chatmail clients (#5598) 2024-05-27 16:59:01 -03:00
iequidoo
adb0dd43a7 fix: Set Param::Bot for messages on the sender side as well (#5615) 2024-05-27 16:29:41 -03:00
link2xt
d29538beb0 chore(release): prepare for 1.139.6 2024-05-25 07:05:10 +00:00
link2xt
b99e4649a4 chore(cargo-deny): remove unused entry from deny.toml 2024-05-25 06:50:16 +00:00
link2xt
68daa3550e build: upgrade iroh to 0.17.0 2024-05-25 06:36:34 +00:00
link2xt
9d65282710 build(nix): add iroh-base output hash 2024-05-25 05:59:00 +00:00
37 changed files with 748 additions and 357 deletions

View File

@@ -125,9 +125,6 @@ jobs:
- name: Tests
env:
RUST_BACKTRACE: 1
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: cargo nextest run --workspace
- name: Doc-Tests
@@ -212,11 +209,11 @@ jobs:
fail-fast: false
matrix:
include:
# Currently used Rust version.
# Currently used Python version.
- os: ubuntu-latest
python: 3.12
python: 3.13
- os: macos-latest
python: 3.12
python: 3.13
# PyPy tests
- os: ubuntu-latest
@@ -246,6 +243,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install tox
run: pip install tox
@@ -266,11 +264,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.12
python: 3.13
- os: macos-latest
python: 3.12
python: 3.13
- os: windows-latest
python: 3.12
python: 3.13
# PyPy tests
- os: ubuntu-latest
@@ -292,6 +290,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install tox
run: pip install tox

View File

@@ -1,5 +1,74 @@
# Changelog
## [1.140.0] - 2024-06-04
### Features / Changes
- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)).
- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)).
- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)).
- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)).
### Fixes
- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)).
- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)).
- Allow creation of groups by outgoing messages without recipients.
- Prefer `Chat-Group-ID` over references for new groups.
- Do not fail to send images with wrong extensions.
### Build system
- Unpin OpenSSL version and update to OpenSSL 3.3.0.
### CI
- Remove cargo-nextest bug workaround.
### Documentation
- Add vCard as supported standard.
- Create_group() does not find chats, only creates them.
- Fix a typo in test_partial_group_consistency().
### Refactor
- Factor create_adhoc_group() call out of create_group().
- Put duplicate code into `lookup_chat_or_create_adhoc_group`.
### Tests
- Fix logging of TestContext created using TestContext::new_alice().
- Refactor `test_alias_*` into 8 separate tests.
## [1.139.6] - 2024-05-25
### Build system
- Update `iroh` to the git version.
- nix: Add iroh-base output hash.
- Upgrade iroh to 0.17.0.
### Fixes
- @deltachat/stdio-rpc-server: Do not set RUST_LOG to "info" by default.
- Acquire write lock on iroh_channels before checking for subscribe_loop.
### Miscellaneous Tasks
- Fix python lint.
- cargo-deny: Remove unused entry from deny.toml.
### Refactor
- Log IMAP connection type on connection failure.
### Tests
- Viewtype::File attachments are sent unchanged and preserve extensions.
- deltachat-rpc-client: Add realtime channel tests.
- deltachat-rpc-client: Regression test for double gossip subscription.
## [1.139.5] - 2024-05-23
### API-Changes
@@ -4300,3 +4369,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3
[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4
[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5
[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6
[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0

71
Cargo.lock generated
View File

@@ -1324,7 +1324,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.139.5"
version = "1.140.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1367,7 +1367,6 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"openssl-src",
"parking_lot",
"percent-encoding",
"pgp",
@@ -1419,7 +1418,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.139.5"
version = "1.140.0"
dependencies = [
"anyhow",
"async-channel 2.2.1",
@@ -1444,7 +1443,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.139.5"
version = "1.140.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1459,7 +1458,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.139.5"
version = "1.140.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1488,7 +1487,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.139.5"
version = "1.140.0"
dependencies = [
"anyhow",
"deltachat",
@@ -3237,8 +3236,9 @@ dependencies = [
[[package]]
name = "iroh-base"
version = "0.16.0"
source = "git+https://github.com/n0-computer/iroh?rev=c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e#c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1be0b442ed44d20905cf77c673169906c883e05c829e3fb303b131e925139fc"
dependencies = [
"aead",
"anyhow",
@@ -3277,8 +3277,9 @@ dependencies = [
[[package]]
name = "iroh-gossip"
version = "0.16.0"
source = "git+https://github.com/n0-computer/iroh?rev=c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e#c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "857aa77a0b29283edf99224bd12cd739684c5873e639b85adb38b8d1d777162d"
dependencies = [
"anyhow",
"bytes",
@@ -3302,8 +3303,9 @@ dependencies = [
[[package]]
name = "iroh-metrics"
version = "0.16.0"
source = "git+https://github.com/n0-computer/iroh?rev=c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e#c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd54b9cf342b2618efc8d3ff6cdcd083fa5a2cf6cc78bb473bd32e228eabb40e"
dependencies = [
"anyhow",
"erased_set",
@@ -3322,8 +3324,9 @@ dependencies = [
[[package]]
name = "iroh-net"
version = "0.16.0"
source = "git+https://github.com/n0-computer/iroh?rev=c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e#c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a744000e6c5704479eeb4751eb23b9b8e7e56a0fc484beb8831694fc93e378f"
dependencies = [
"aead",
"anyhow",
@@ -3368,7 +3371,7 @@ dependencies = [
"postcard",
"rand 0.8.5",
"rand_core 0.6.4",
"rcgen 0.11.3",
"rcgen 0.12.1",
"reqwest 0.12.4",
"ring 0.17.8",
"rtnetlink",
@@ -3398,9 +3401,9 @@ dependencies = [
[[package]]
name = "iroh-quinn"
version = "0.10.4"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b934380145fd5d53a583d01ae9500f4807efe6b0f0fe115c7be4afa2b35db99f"
checksum = "906875956feb75d3d41d708ddaffeb11fdb10cd05f23efbcb17600037e411779"
dependencies = [
"bytes",
"iroh-quinn-proto",
@@ -3415,13 +3418,13 @@ dependencies = [
[[package]]
name = "iroh-quinn-proto"
version = "0.10.7"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f2656b322c7f6cf3eb95e632d1c0f2fa546841915b0270da581f918c70c4be"
checksum = "c6bf92478805e67f2320459285496e1137edf5171411001a0d4d85f9bbafb792"
dependencies = [
"bytes",
"rand 0.8.5",
"ring 0.16.20",
"ring 0.17.8",
"rustc-hash",
"rustls 0.21.11",
"rustls-native-certs",
@@ -3433,9 +3436,9 @@ dependencies = [
[[package]]
name = "iroh-quinn-udp"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6679979a7271c24f9dae9622c0b4a543881508aa3a7396f55dfbaaa56f01c063"
checksum = "edc7915b3a31f08ee0bc02f73f4d61a5d5be146a1081ef7f70622a11627fd314"
dependencies = [
"bytes",
"libc",
@@ -4089,9 +4092,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.63"
version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@@ -4121,18 +4124,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.1.6+3.1.4"
version = "300.3.0+3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.99"
version = "0.9.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
dependencies = [
"cc",
"libc",
@@ -5056,18 +5059,6 @@ dependencies = [
"yasna",
]
[[package]]
name = "rcgen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6"
dependencies = [
"pem 3.0.4",
"ring 0.16.20",
"time 0.3.34",
"yasna",
]
[[package]]
name = "rcgen"
version = "0.12.1"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.139.5"
version = "1.140.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -61,8 +61,8 @@ hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
iroh-net = { git = "https://github.com/n0-computer/iroh", rev="c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e" }
iroh-gossip = { git = "https://github.com/n0-computer/iroh", rev="c8690a2c6eb5753c4ec6b7e44db72abf09df3c6e", features = ["net"] }
iroh-net = "0.17.0"
iroh-gossip = { version = "0.17.0", features = ["net"] }
quinn = "0.10.0"
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
@@ -104,14 +104,6 @@ toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
[dev-dependencies]
ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
@@ -179,4 +171,4 @@ vendored = [
"async-native-tls/vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.139.5"
version = "1.140.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -523,6 +523,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
* 1 = WebXDC realtime API is enabled.
*
* If you want to retrieve a value, use dc_get_config().
*

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.139.5"
version = "1.140.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.139.5"
"version": "1.140.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.139.5"
version = "1.140.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.139.5"
version = "1.140.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -17,6 +17,8 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]

View File

@@ -177,8 +177,7 @@ class Rpc:
account_id = event["contextId"]
queue = self.get_queue(account_id)
event = event["event"]
# logging.debug("account_id=%d got an event %s", account_id, event)
print("account_id=%d got an event %s" % (account_id, event))
logging.debug("account_id=%d got an event %s", account_id, event)
queue.put(event)
except Exception:
# Log an exception if the event loop dies.

View File

@@ -29,6 +29,8 @@ def log(msg):
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
assert ac1.get_config("webxdc_realtime_enabled") == "1"
assert ac2.get_config("webxdc_realtime_enabled") == "1"
ac1_ac2_chat = ac1.create_chat(ac2)
ac2.create_chat(ac1)
@@ -75,6 +77,8 @@ def wait_receive_realtime_data(msg_data_list):
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1.create_chat(ac2)
ac2.create_chat(ac1)
@@ -115,6 +119,8 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
@@ -125,6 +131,8 @@ def test_realtime_simultaneously(acfactory, path_to_webxdc):
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
@@ -141,6 +149,8 @@ def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
def test_no_duplicate_messages(acfactory, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.139.5"
version = "1.140.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.139.5"
"version": "1.140.0"
}

View File

@@ -139,5 +139,4 @@ license-files = [
github = [
"async-email",
"deltachat",
"n0-computer",
]

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.139.5"
"version": "1.140.0"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.139.5"
version = "1.140.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"

View File

@@ -1 +1 @@
2024-05-23
2024-06-04

View File

@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -3,7 +3,7 @@
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use std::io::{Cursor, Seek};
use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
@@ -12,6 +12,7 @@ use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::codecs::jpeg::JpegEncoder;
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
@@ -426,9 +427,25 @@ impl<'a> BlobObject<'a> {
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
let (nr_bytes, exif) = self.metadata()?;
let mut file = std::fs::File::open(self.to_abs_path())?;
let (nr_bytes, exif) = image_metadata(&file)?;
*no_exif_ref = exif.is_none();
let mut img = image::open(&blob_abs).context("image decode failure")?;
// It's strange that BufReader modifies a file position while it takes a non-mut
// reference. Ok, just rewind it.
file.rewind()?;
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
let imgreader = match imgreader {
Ok(ir) => ir,
_ => {
file.rewind()?;
ImageReader::with_format(
std::io::BufReader::new(&file),
ImageFormat::from_path(&blob_abs)?,
)
}
};
let fmt = imgreader.format().context("No format??")?;
let mut img = imgreader.decode().context("image decode failure")?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
@@ -457,10 +474,9 @@ impl<'a> BlobObject<'a> {
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75;
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
Ok(ImageFormat::Jpeg) => {
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
ImageFormat::Jpeg => {
add_white_bg = false;
ImageOutputFormat::Jpeg {
quality: jpeg_quality,
@@ -497,7 +513,7 @@ impl<'a> BlobObject<'a> {
img_wh = max(img.width(), img.height());
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
// crate when recoding, so don't scale them down.
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
img_wh = img_wh * 2 / 3;
}
}
@@ -538,7 +554,7 @@ impl<'a> BlobObject<'a> {
if do_scale || exif.is_some() {
// The file format is JPEG/PNG now, we may have to change the file extension
if !matches!(fmt, Ok(ImageFormat::Jpeg))
if !matches!(fmt, ImageFormat::Jpeg)
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
{
blob_abs = blob_abs.with_extension("jpg");
@@ -575,15 +591,14 @@ impl<'a> BlobObject<'a> {
}
}
}
}
/// Returns image file size and Exif.
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
let file = std::fs::File::open(self.to_abs_path())?;
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(&file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
/// Returns image file size and Exif.
pub fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
@@ -1306,8 +1321,7 @@ mod tests {
.context("failed to write file")?;
check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
let (_, exif) = blob.metadata()?;
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
@@ -1342,8 +1356,7 @@ mod tests {
assert_eq!(&bytes1, bytes);
}
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
let (_, exif) = blob.metadata()?;
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
assert!(exif.is_none());
let img = check_image_size(file_saved, compressed_width, compressed_height);
@@ -1379,8 +1392,7 @@ mod tests {
.get_blobdir()
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
bob_msg.save_file(&bob, &file_saved).await?;
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
let (file_size, _) = blob.metadata()?;
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
assert_eq!(file_size, bytes.len() as u64);
check_image_size(file_saved, width, height);
Ok(())

View File

@@ -1932,6 +1932,10 @@ impl Chat {
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
let is_bot = context.get_config_bool(Config::Bot).await?;
msg.param
.set_optional(Param::Bot, Some("1").filter(|_| is_bot));
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
// Set "References:" to identify the "thread" of the conversation.
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
@@ -2991,11 +2995,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.subject.clone_from(&rendered_msg.subject);
msg.update_subject(context).await?;
let chunk_size = context
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
for recipients_chunk in recipients.chunks(chunk_size) {
@@ -7528,4 +7528,27 @@ mod tests {
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
Ok(())
}
/// Tests sending JPEG image with .png extension.
///
/// This is a regression test, previously sending failed
/// because image was passed to PNG decoder
/// and it failed to decode image.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_jpeg_with_png_ext() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
let file = alice.get_blobdir().join("screenshot.png");
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let alice_chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let _msg = bob.recv_msg(&sent_msg).await;
Ok(())
}
}

View File

@@ -363,8 +363,8 @@ pub enum Config {
/// MsgId of webxdc map integration.
WebxdcIntegration,
/// Iroh secret key.
IrohSecretKey,
/// Enable webxdc realtime features.
WebxdcRealtimeEnabled,
}
impl Config {

View File

@@ -498,6 +498,23 @@ impl Context {
self.get_config_bool(Config::IsChatmail).await
}
/// Returns maximum number of recipients the provider allows to send a single email to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
let is_chatmail = self.is_chatmail().await?;
let val = self
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or_else(
|| match is_chatmail {
true => usize::MAX,
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
},
usize::from,
);
Ok(val)
}
/// Does a background fetch
/// pauses the scheduler and does one imap fetch, then unpauses and returns
pub async fn background_fetch(&self) -> Result<()> {
@@ -950,6 +967,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"webxdc_realtime_enabled",
self.get_config_bool(Config::WebxdcRealtimeEnabled)
.await?
.to_string(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));
@@ -1656,7 +1679,6 @@ mod tests {
"socks5_password",
"key_id",
"webxdc_integration",
"iroh_secret_key",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();

View File

@@ -184,7 +184,7 @@ impl Session {
bail!("Attempt to fetch UID 0");
}
self.select_folder(context, Some(folder)).await?;
self.select_folder(context, folder).await?;
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);

View File

@@ -838,7 +838,7 @@ impl Session {
// Collect pairs of UID and Message-ID.
let mut msgs = BTreeMap::new();
self.select_folder(context, Some(folder)).await?;
self.select_folder(context, folder).await?;
let mut list = self
.uid_fetch("1:*", RFC724MID_UID)
@@ -1039,7 +1039,7 @@ impl Session {
// MOVE/DELETE operations. This does not result in multiple SELECT commands
// being sent because `select_folder()` does nothing if the folder is already
// selected.
self.select_folder(context, Some(folder)).await?;
self.select_folder(context, folder).await?;
// Empty target folder name means messages should be deleted.
if target.is_empty() {
@@ -1087,7 +1087,7 @@ impl Session {
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
self.select_folder(context, Some(&folder))
self.select_folder(context, &folder)
.await
.context("failed to select folder")?;
@@ -1131,7 +1131,7 @@ impl Session {
return Ok(());
}
self.select_folder(context, Some(folder))
self.select_folder(context, folder)
.await
.context("failed to select folder")?;
@@ -1563,7 +1563,7 @@ impl Session {
) -> Result<Option<&'a str>> {
// Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below.
self.select_folder(context, None).await?;
self.maybe_close_folder(context).await?;
for folder in folders {
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);

View File

@@ -29,7 +29,7 @@ impl Session {
) -> Result<Self> {
use futures::future::FutureExt;
self.select_folder(context, Some(folder)).await?;
self.select_folder(context, folder).await?;
if self.server_sent_unsolicited_exists(context)? {
return Ok(self);

View File

@@ -55,15 +55,13 @@ impl ImapSession {
pub(crate) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
folder: &str,
) -> Result<NewlySelected> {
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(folder) = folder {
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
}
@@ -71,34 +69,30 @@ impl ImapSession {
self.maybe_close_folder(context).await?;
// select new folder
if let Some(folder) = folder {
let res = if self.can_condstore() {
self.select_condstore(folder).await
} else {
self.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => Err(Error::Other(err.to_string())),
}
let res = if self.can_condstore() {
self.select_condstore(folder).await
} else {
Ok(NewlySelected::No)
self.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => Err(Error::Other(err.to_string())),
}
}
@@ -108,7 +102,7 @@ impl ImapSession {
context: &Context,
folder: &str,
) -> anyhow::Result<NewlySelected> {
match self.select_folder(context, Some(folder)).await {
match self.select_folder(context, folder).await {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
@@ -118,7 +112,7 @@ impl ImapSession {
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
// Need to recheck, could have been created in parallel.
}
let select_res = self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {folder}"));
let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}"));
if select_res.is_err() {
create_res?;
}

View File

@@ -1433,7 +1433,7 @@ pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)>
"txt" => (Viewtype::File, "text/plain"),
"vcard" => (Viewtype::Vcard, "text/vcard"),
"vcf" => (Viewtype::Vcard, "text/vcard"),
"wav" => (Viewtype::File, "audio/wav"),
"wav" => (Viewtype::Audio, "audio/wav"),
"weba" => (Viewtype::File, "audio/webm"),
"webm" => (Viewtype::Video, "video/webm"),
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0

View File

@@ -25,7 +25,6 @@
use anyhow::{anyhow, Context as _, Result};
use email::Header;
use futures_lite::StreamExt;
use iroh_gossip::net::{Gossip, JoinTopicFut, GOSSIP_ALPN};
use iroh_gossip::proto::{Event as IrohEvent, TopicId};
use iroh_net::relay::{RelayMap, RelayUrl};
@@ -38,6 +37,7 @@ use tokio::task::JoinHandle;
use url::Url;
use crate::chat::send_msg;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::message::{Message, MsgId, Viewtype};
@@ -158,6 +158,10 @@ impl Iroh {
self.gossip.broadcast(topic, data.into()).await?;
if env::var("REALTIME_DEBUG").is_ok() {
info!(ctx, "Sent realtime data");
}
Ok(())
}
@@ -243,15 +247,6 @@ impl Context {
// Shuts down on deltachat shutdown
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
let endp = endpoint.clone();
let gsp = gossip.clone();
tokio::spawn(async move {
let mut stream = endp.local_endpoints();
while let Some(endpoints) = stream.next().await {
gsp.update_endpoints(&endpoints)?;
}
anyhow::Ok(())
});
Ok(Iroh {
endpoint,
gossip,
@@ -344,6 +339,10 @@ pub async fn send_webxdc_realtime_advertisement(
ctx: &Context,
msg_id: MsgId,
) -> Result<Option<JoinTopicFut>> {
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
return Ok(None);
}
let iroh = ctx.get_or_try_init_peer_channel().await?;
let conn = iroh.join_and_subscribe_gossip(ctx, msg_id).await?;
@@ -353,14 +352,16 @@ pub async fn send_webxdc_realtime_advertisement(
msg.param.set_cmd(SystemMessage::IrohNodeAddr);
msg.in_reply_to = Some(webxdc.rfc724_mid.clone());
send_msg(ctx, webxdc.chat_id, &mut msg).await?;
if env::var("REALTIME_DEBUG").is_ok() {
info!(ctx, "IROH_REALTIME: Sent realtime advertisement");
}
info!(ctx, "IROH_REALTIME: Sent realtime advertisement");
Ok(conn)
}
/// Send realtime data to the gossip swarm.
/// Send realtime data to other peers using iroh.
pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u8>) -> Result<()> {
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
return Ok(());
}
let iroh = ctx.get_or_try_init_peer_channel().await?;
iroh.send_webxdc_realtime_data(ctx, msg_id, data).await?;
Ok(())
@@ -368,10 +369,13 @@ pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u
/// Leave the gossip of the webxdc with given [MsgId].
pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
return Ok(());
}
let iroh = ctx.get_or_try_init_peer_channel().await?;
iroh.leave_realtime(get_iroh_topic_for_msg(ctx, msg_id).await?)
.await?;
info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
Ok(())
@@ -439,12 +443,11 @@ async fn subscribe_loop(
let event = stream.recv().await?;
match event {
IrohEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
IrohEvent::Received(event) => {
if env::var("REALTIME_DEBUG").is_ok() {
info!(context, "IROH_REALTIME: Received realtime data");
}
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: event
@@ -475,6 +478,17 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -606,6 +620,21 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
assert!(alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
.unwrap());
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -741,6 +770,17 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -803,4 +843,29 @@ mod tests {
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_peer_channels_disabled() {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
// creates iroh endpoint as side effect
send_webxdc_realtime_advertisement(alice, MsgId::new(1))
.await
.unwrap();
assert!(alice.ctx.iroh.get().is_none());
// creates iroh endpoint as side effect
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
.await
.unwrap();
assert!(alice.ctx.iroh.get().is_none());
// creates iroh endpoint as side effect
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
assert!(alice.ctx.iroh.get().is_none())
}
}

View File

@@ -538,10 +538,18 @@ impl Peerstate {
if let Some(old_addr) = old_addr {
// We are doing an AEAP transition to the new address and the SQL INSERT below will
// save the existing peerstate as belonging to this new address. We now need to
// delete the peerstate that belongs to the current address in case if the contact
// later wants to move back to the current address. Otherwise the old entry will be
// just found and updated instead of doing AEAP.
t.execute("DELETE FROM acpeerstates WHERE addr=?", (old_addr,))?;
// "unverify" the peerstate that belongs to the current address in case if the
// contact later wants to move back to the current address. Otherwise the old entry
// will be just found and updated instead of doing AEAP. We can't just delete the
// existing peerstate as this would break encryption to it. This is critical for
// non-verified groups -- if we can't encrypt to the old address, we can't securely
// remove it from the group (to add the new one instead).
t.execute(
"UPDATE acpeerstates \
SET verified_key=NULL, verified_key_fingerprint='', verifier='' \
WHERE addr=?",
(old_addr,),
)?;
}
t.execute(
"INSERT INTO acpeerstates (

View File

@@ -782,29 +782,6 @@ async fn add_parts(
info!(context, "Message is an MDN (TRASH).",);
}
// Try to assign to a chat based on Chat-Group-ID.
if chat_id.is_none() {
if let Some(grpid) = mime_parser.get_chat_group_id() {
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
}
}
}
if chat_id.is_none() {
// try to assign to a chat based on In-Reply-To/References:
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, &parent, to_ids, from_id).await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
// signals whether the current user is a bot
let is_bot = context.get_config_bool(Config::Bot).await?;
@@ -834,17 +811,44 @@ async fn add_parts(
create_blocked_default
};
if chat_id.is_none() && (allow_creation || test_normal_chat.is_some()) {
// try to create a group
// Try to assign to a chat based on Chat-Group-ID.
if chat_id.is_none() {
if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, &grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation || test_normal_chat.is_some() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
create_blocked,
from_id,
to_ids,
&verified_encryption,
&grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
}
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
if chat_id.is_none() {
if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
context,
mime_parser,
is_partial_download.is_some(),
create_blocked,
from_id,
&parent,
to_ids,
&verified_encryption,
from_id,
allow_creation || test_normal_chat.is_some(),
create_blocked,
is_partial_download.is_some(),
)
.await?
{
@@ -1046,27 +1050,32 @@ async fn add_parts(
// Try to assign to a chat based on Chat-Group-ID.
if chat_id.is_none() {
if let Some(grpid) = mime_parser.get_chat_group_id() {
if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, grpid).await?
chat::get_chat_id_by_grpid(context, &grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
Blocked::Not,
from_id,
to_ids,
&verified_encryption,
&grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
}
if chat_id.is_none() {
// try to assign to a chat based on In-Reply-To/References:
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, &parent, to_ids, from_id).await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if mime_parser.decrypting_failed && !fetching_existing_messages {
if chat_id.is_none() {
chat_id = Some(DC_CHAT_ID_TRASH);
@@ -1098,23 +1107,25 @@ async fn add_parts(
}
}
if !to_ids.is_empty() {
if chat_id.is_none() && allow_creation {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
Blocked::Not,
from_id,
to_ids,
&verified_encryption,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
if chat_id.is_none() {
if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
context,
mime_parser,
&parent,
to_ids,
from_id,
allow_creation,
Blocked::Not,
is_partial_download.is_some(),
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if !to_ids.is_empty() {
if chat_id.is_none() && allow_creation {
let to_contact = Contact::get_by_id(context, to_id).await?;
if let Some(list_id) = to_contact.param.get(Param::ListId) {
@@ -1779,6 +1790,44 @@ async fn lookup_chat_by_reply(
Ok(Some((parent_chat.id, parent_chat.blocked)))
}
#[allow(clippy::too_many_arguments)]
async fn lookup_chat_or_create_adhoc_group(
context: &Context,
mime_parser: &MimeMessage,
parent: &Option<Message>,
to_ids: &[ContactId],
from_id: ContactId,
allow_creation: bool,
create_blocked: Blocked,
is_partial_download: bool,
) -> Result<Option<(ChatId, Blocked)>> {
if let Some((new_chat_id, new_chat_id_blocked)) =
// Try to assign to a chat based on In-Reply-To/References.
lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await?
{
Ok(Some((new_chat_id, new_chat_id_blocked)))
} else if allow_creation {
// Try to create an ad hoc group.
if let Some(new_chat_id) = create_adhoc_group(
context,
mime_parser,
create_blocked,
from_id,
to_ids,
is_partial_download,
)
.await
.context("Could not create ad hoc group")?
{
Ok(Some((new_chat_id, create_blocked)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
/// If it returns false, it shall be assigned to the parent chat.
async fn is_probably_private_reply(
@@ -1820,7 +1869,8 @@ async fn is_probably_private_reply(
/// chat with this ID. If there is no group-id and there are more
/// than two members, a new ad hoc group is created.
///
/// On success the function returns the found/created (chat_id, chat_blocked) tuple.
/// On success the function returns the created (chat_id, chat_blocked) tuple.
#[allow(clippy::too_many_arguments)]
async fn create_group(
context: &Context,
mime_parser: &mut MimeMessage,
@@ -1829,34 +1879,8 @@ async fn create_group(
from_id: ContactId,
to_ids: &[ContactId],
verified_encryption: &VerifiedEncryption,
grpid: &str,
) -> Result<Option<(ChatId, Blocked)>> {
let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) else {
if is_partial_download {
// Partial download may be an encrypted message with protected Subject header.
//
// We do not want to create a group with "..." or "Encrypted message" as a subject.
info!(
context,
"Ad-hoc group cannot be created from partial download."
);
return Ok(None);
}
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
if !member_ids.contains(&(from_id)) {
member_ids.push(from_id);
}
if !member_ids.contains(&(ContactId::SELF)) {
member_ids.push(ContactId::SELF);
}
let res = create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
.await
.context("could not create ad hoc group")?
.map(|chat_id| (chat_id, create_blocked));
return Ok(res);
};
let mut chat_id = None;
let mut chat_id_blocked = Default::default();
@@ -1899,7 +1923,7 @@ async fn create_group(
// otherwise, a pending "quit" message may pop up
&& 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?
&& (!chat::is_group_explicitly_left(context, grpid).await?
|| self_explicitly_added(context, &mime_parser).await?)
{
// Group does not exist but should be created.
@@ -1912,7 +1936,7 @@ async fn create_group(
let new_chat_id = ChatId::create_multiuser_record(
context,
Chattype::Group,
&grpid,
grpid,
grpname,
create_blocked,
create_protected,
@@ -2470,8 +2494,29 @@ async fn create_adhoc_group(
context: &Context,
mime_parser: &MimeMessage,
create_blocked: Blocked,
member_ids: &[ContactId],
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
) -> Result<Option<ChatId>> {
if is_partial_download {
// Partial download may be an encrypted message with protected Subject header.
//
// We do not want to create a group with "..." or "Encrypted message" as a subject.
info!(
context,
"Ad-hoc group cannot be created from partial download."
);
return Ok(None);
}
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
if !member_ids.contains(&(from_id)) {
member_ids.push(from_id);
}
if !member_ids.contains(&(ContactId::SELF)) {
member_ids.push(ContactId::SELF);
}
if mime_parser.is_mailinglist_message() {
return Ok(None);
}
@@ -2517,7 +2562,7 @@ async fn create_adhoc_group(
context,
"Created ad-hoc group id={new_chat_id}, name={grpname:?}."
);
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
chat::add_to_chat_contacts_table(context, new_chat_id, &member_ids).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);

View File

@@ -1820,26 +1820,31 @@ async fn test_save_mime_headers_on() -> anyhow::Result<()> {
Ok(())
}
async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestContext, TestContext) {
async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
// Claire, a customer, sends a support request
// to the alias address <support@example.org> from a classic MUA.
// to the alias address <support@example.org>.
// If `chat_request` is true, Claire is using Delta Chat,
// otherwise Claire sends the request from a classic MUA.
// The alias expands to the supporters Alice and Bob.
// Check that Alice receives the message in a group chat.
let claire_request = if group_request {
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
To: support@example.org, ceo@example.org\n\
From: claire@example.org\n\
Subject: i have a question\n\
Message-ID: <non-dc-1@example.org>\n\
{}\
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
Content-Type: text/plain\n\
\n\
hi support! what is the current version?",
To: support@example.org, ceo@example.org\n\
From: claire@example.org\n\
Subject: i have a question\n\
Message-ID: <non-dc-1@example.org>\n\
{}\
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
Content-Type: text/plain\n\
\n\
hi support! what is the current version?",
if chat_request {
"Chat-Group-ID: 8ud29aridt29arid\n\
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n"
"Chat-Version: 1.0\n\
Chat-Group-ID: 8ud29aridt29arid\n\
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n"
} else {
""
}
@@ -1847,15 +1852,15 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
} else {
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
To: support@example.org\n\
From: claire@example.org\n\
Subject: i have a question\n\
Message-ID: <non-dc-1@example.org>\n\
{}\
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
Content-Type: text/plain\n\
\n\
hi support! what is the current version?",
To: support@example.org\n\
From: claire@example.org\n\
Subject: i have a question\n\
Message-ID: <non-dc-1@example.org>\n\
{}\
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
Content-Type: text/plain\n\
\n\
hi support! what is the current version?",
if chat_request {
"Chat-Version: 1.0\n"
} else {
@@ -1864,11 +1869,11 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
)
};
let alice = TestContext::new_alice().await;
receive_imf(&alice, claire_request.as_bytes(), false)
.await
.unwrap();
// Check that Alice receives the message in a group chat.
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_subject(), "i have a question");
assert!(msg.get_text().contains("hi support!"));
@@ -1882,7 +1887,7 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
}
assert_eq!(msg.get_override_sender_name(), None);
let claire = TestContext::new().await;
let claire = tcm.unconfigured().await;
claire.configure_addr("claire@example.org").await;
receive_imf(&claire, claire_request.as_bytes(), false)
.await
@@ -1906,15 +1911,48 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
assert_eq!(get_chat_msgs(&claire, chat.id).await.unwrap().len(), 1);
assert_eq!(msg.get_override_sender_name(), None);
(claire, alice)
}
async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool) {
let (claire, alice) = create_test_alias(chat_request, group_request).await;
let reply = if from_dc {
// Bob, the other supporter, answers with Delta Chat.
format!(
"To: support@example.org, claire@example.org\n\
From: bob@example.net\n\
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
References: <bobreply@localhost>\n\
In-Reply-To: <non-dc-1@example.org>\n\
Message-ID: <bobreply@localhost>\n\
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
Chat-Version: 1.0\n\
{}\
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\
Chat-Disposition-Notification-To: bob@example.net\n\
Content-Type: text/plain\n\
\n\
hi claire, the version is 1.0, cheers bob",
if group_request && chat_request {
"Chat-Group-ID: 8ud29aridt29arid\n"
} else {
// Ad-hoc group has no ID.
""
}
)
} else {
// Bob, the other supporter, answers with a classic MUA.
"To: support@example.org, claire@example.org\n\
From: bob@example.net\n\
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
References: <non-dc-1@example.org>\n\
In-Reply-To: <non-dc-1@example.org>\n\
Message-ID: <non-dc-2@example.net>\n\
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
Content-Type: text/plain\n\
\n\
hi claire, the version is 1.0, cheers bob"
.to_string()
};
// Check that Alice gets the message in the same chat.
let request = alice.get_last_msg().await;
receive_imf(&alice, reply, false).await.unwrap();
receive_imf(&alice, reply.as_bytes(), false).await.unwrap();
let answer = alice.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().contains("the version is 1.0"));
@@ -1937,7 +1975,7 @@ async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool
// Check that Claire also gets the message in the same chat.
let request = claire.get_last_msg().await;
receive_imf(&claire, reply, false).await.unwrap();
receive_imf(&claire, reply.as_bytes(), false).await.unwrap();
let answer = claire.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().contains("the version is 1.0"));
@@ -1949,47 +1987,36 @@ async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_nondc() {
// Bob, the other supporter, answers with a classic MUA.
let bob_answer = b"To: support@example.org, claire@example.org\n\
From: bob@example.net\n\
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
References: <non-dc-1@example.org>\n\
In-Reply-To: <non-dc-1@example.org>\n\
Message-ID: <non-dc-2@example.net>\n\
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
Content-Type: text/plain\n\
\n\
hi claire, the version is 1.0, cheers bob";
check_alias_reply(bob_answer, true, true).await;
check_alias_reply(bob_answer, false, true).await;
check_alias_reply(bob_answer, true, false).await;
check_alias_reply(bob_answer, false, false).await;
async fn test_alias_support_answer_from_nondc_nonchat_nongroup() {
check_alias_reply(false, false, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_answer_from_dc() {
// Bob, the other supporter, answers with Delta Chat.
let bob_answer = b"To: support@example.org, claire@example.org\n\
From: bob@example.net\n\
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
References: <Gr.af9e810c9b592927.gNm8dVdkZsH@example.net>\n\
In-Reply-To: <non-dc-1@example.org>\n\
Message-ID: <Gr.af9e810c9b592927.gNm8dVdkZsH@example.net>\n\
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
Chat-Version: 1.0\n\
Chat-Group-ID: af9e810c9b592927\n\
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\
Chat-Disposition-Notification-To: bob@example.net\n\
Content-Type: text/plain\n\
\n\
hi claire, the version is 1.0, cheers bob";
check_alias_reply(bob_answer, true, true).await;
check_alias_reply(bob_answer, false, true).await;
check_alias_reply(bob_answer, true, false).await;
check_alias_reply(bob_answer, false, false).await;
async fn test_alias_support_answer_from_nondc_nonchat_group() {
check_alias_reply(false, false, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_nondc_chat_nongroup() {
check_alias_reply(false, true, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_nondc_chat_group() {
check_alias_reply(false, true, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_dc_nonchat_nongroup() {
check_alias_reply(true, false, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_dc_nonchat_group() {
check_alias_reply(true, false, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_dc_chat_nongroup() {
check_alias_reply(true, true, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alias_support_answer_from_dc_chat_group() {
check_alias_reply(true, true, true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3213,6 +3240,29 @@ async fn test_auto_accept_group_for_bots() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_as_bot() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::Bot, Some("1")).await.unwrap();
let bob = &tcm.bob().await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_id = Contact::create(alice, "", &bob_addr).await?;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let alice_chat_id = ChatId::lookup_by_contact(alice, alice_bob_id)
.await?
.unwrap();
let msg = alice.get_last_msg_in(alice_chat_id).await;
assert!(msg.is_bot());
let msg = bob.get_last_msg_in(bob_chat_id).await;
assert!(msg.is_bot());
chat::forward_msgs(bob, &[msg.id], bob_chat_id).await?;
let msg = bob.get_last_msg_in(bob_chat_id).await;
assert!(msg.is_forwarded());
assert!(!msg.is_bot());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4380,7 +4430,7 @@ Chat-Group-Member-Added: charlie@example.com",
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 3);
// Bob fully reives the partial message.
// Bob fully receives the partial message.
let msg_id = receive_imf_from_inbox(
&bob,
"first@example.org",
@@ -4570,3 +4620,83 @@ async fn test_receive_vcard() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_no_recipients() -> Result<()> {
let t = &TestContext::new_alice().await;
let raw = b"From: alice@example.org\n\
Subject: Group\n\
Chat-Version: 1.0\n\
Chat-Group-Name: Group\n\
Chat-Group-ID: GePFDkwEj2K\n\
Message-ID: <foobar@localhost>\n\
\n\
Hello!";
let received = receive_imf(t, raw, false).await?.unwrap();
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
let chat = Chat::load_from_db(t, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Group);
Ok(())
}
/// Tests that creating a group
/// is preferred over assigning message to existing
/// chat based on `In-Reply-To` and `References`.
///
/// Referenced message itself may be incorrectly assigned,
/// but `Chat-Group-ID` uniquely identifies the chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prefer_chat_group_id_over_references() -> Result<()> {
let t = &TestContext::new_alice().await;
// Alice receives 1:1 message from Bob.
let raw = b"From: bob@example.net\n\
To: alice@example.org\n\
Subject: Hi\n\
Message-ID: <oneoneone@localhost>\n\
\n\
Hello!";
receive_imf(t, raw, false).await?.unwrap();
// Alice receives a group message from Bob.
// This references 1:1 message,
// but should create a group.
let raw = b"From: bob@example.net\n\
To: alice@example.org\n\
Subject: Group\n\
Chat-Version: 1.0\n\
Chat-Group-Name: Group 1\n\
Chat-Group-ID: GePFDkwEj2K\n\
Message-ID: <incoming@localhost>\n\
References: <oneoneone@localhost>\n\
In-Reply-To: <oneoneone@localhost>\n\
\n\
Group 1";
let received1 = receive_imf(t, raw, false).await?.unwrap();
let msg1 = Message::load_from_db(t, *received1.msg_ids.last().unwrap()).await?;
let chat1 = Chat::load_from_db(t, msg1.chat_id).await?;
assert_eq!(chat1.typ, Chattype::Group);
// Alice receives outgoing group message.
// This references 1:1 message,
// but should create another group.
let raw = b"From: alice@example.org\n\
To: bob@example.net
Subject: Group\n\
Chat-Version: 1.0\n\
Chat-Group-Name: Group 2\n\
Chat-Group-ID: Abcdexyzfoo\n\
Message-ID: <outgoing@localhost>\n\
References: <oneoneone@localhost>\n\
In-Reply-To: <oneoneone@localhost>\n\
\n\
Group 2";
let received2 = receive_imf(t, raw, false).await?.unwrap();
let msg2 = Message::load_from_db(t, *received2.msg_ids.last().unwrap()).await?;
let chat2 = Chat::load_from_db(t, msg2.chat_id).await?;
assert_eq!(chat2.typ, Chattype::Group);
assert_ne!(chat1.id, chat2.id);
Ok(())
}

View File

@@ -830,20 +830,26 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
.await
.context("failed to update MDN retries count")?;
let res = send_mdn_msg_id(context, msg_id, contact_id, smtp).await;
if let Err(ref err) = res {
// If there is an error, for example there is no message corresponding to the msg_id in the
// database, do not try to send this MDN again.
warn!(
context,
"Error sending MDN for {msg_id}, removing it: {err:#}."
);
context
.sql
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
.await?;
match send_mdn_msg_id(context, msg_id, contact_id, smtp).await {
Err(err) => {
// If there is an error, for example there is no message corresponding to the msg_id in the
// database, do not try to send this MDN again.
warn!(
context,
"Error sending MDN for {msg_id}, removing it: {err:#}."
);
context
.sql
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
.await?;
Err(err)
}
Ok(false) => {
bail!("Temporary error while sending an MDN");
}
Ok(true) => {
// Successfully sent MDN.
Ok(true)
}
}
// If there's a temporary error, pretend there are no more MDNs to send. It's unlikely that
// other MDNs could be sent successfully in case of connectivity problems.
res
}

View File

@@ -5,7 +5,7 @@ use deltachat_contact_tools::EmailAddress;
use rusqlite::OptionalExtension;
use crate::config::Config;
use crate::constants::{self, ShowEmails};
use crate::constants::ShowEmails;
use crate::context::Context;
use crate::imap;
use crate::message::MsgId;
@@ -839,11 +839,7 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
if dbversion < 108 {
let version = 108;
let chunk_size = context
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
let chunk_size = context.get_max_smtp_rcpt_to().await?;
sql.transaction(move |trans| {
Sql::set_db_version_trans(trans, version)?;
let id_max =
@@ -936,6 +932,11 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
.await?;
}
if dbversion < 114 {
sql.execute_migration("CREATE INDEX reactions_index1 ON reactions (msg_id)", 114)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -193,7 +193,18 @@ impl TestContextManager {
#[derive(Debug, Clone, Default)]
pub struct TestContextBuilder {
key_pair: Option<KeyPair>,
log_sink: LogSink,
/// Log sink if set.
///
/// If log sink is not set,
/// a new one will be created and stored
/// inside the test context when it is built.
/// If log sink is provided by the caller,
/// it will be subscribed to the test context,
/// but not stored inside of it,
/// so the caller should store the LogSink elsewhere to
/// prevent it from being dropped immediately.
log_sink: Option<LogSink>,
}
impl TestContextBuilder {
@@ -234,7 +245,7 @@ impl TestContextBuilder {
/// sequence as they occurred rather than all messages from each context in a single
/// block.
pub fn with_log_sink(mut self, sink: LogSink) -> Self {
self.log_sink = sink;
self.log_sink = Some(sink);
self
}
@@ -242,7 +253,7 @@ impl TestContextBuilder {
pub async fn build(self) -> TestContext {
let name = self.key_pair.as_ref().map(|key| key.addr.local.clone());
let test_context = TestContext::new_internal(name, Some(self.log_sink.clone())).await;
let test_context = TestContext::new_internal(name, self.log_sink).await;
if let Some(key_pair) = self.key_pair {
test_context

View File

@@ -327,6 +327,12 @@ async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob:
last_info_msg.is_none(),
"{last_info_msg:?} shouldn't be there (or it's an unrelated info msg)"
);
let sent = bob.send_text(*group, "hi").await;
let msg = Message::load_from_db(bob, sent.sender_msg_id)
.await
.unwrap();
assert_eq!(msg.get_showpadlock(), true);
}
}

View File

@@ -25,6 +25,7 @@ Messenger functions | [Chat-over-Email](https://github.com/deltacha
Detect mailing list | List-Id ([RFC 2919][]) and Precedence ([RFC 3834][])
User and chat colors | [XEP-0392][]: Consistent Color Generation
Send and receive system messages | Multipart/Report Media Type ([RFC 6522][])
Send and receive contact files | vCard ([RFC 6350][])
Return receipts | Message Disposition Notification (MDN, [RFC 8098][], [RFC 3503][]) using the Chat-Disposition-Notification-To header
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
@@ -53,6 +54,7 @@ Locations | KML ([Open Geospatial Consortium](http://www.
[RFC 5321]: https://tools.ietf.org/html/rfc5321
[RFC 5322]: https://tools.ietf.org/html/rfc5322
[RFC 6154]: https://tools.ietf.org/html/rfc6154
[RFC 6350]: https://tools.ietf.org/html/rfc6350
[RFC 6522]: https://tools.ietf.org/html/rfc6522
[RFC 6749]: https://tools.ietf.org/html/rfc6749
[RFC 7162]: https://tools.ietf.org/html/rfc7162