Compare commits

...

52 Commits

Author SHA1 Message Date
link2xt
491d6abe49 docs(deltachat-rpc-client): document Account.import_vcard() 2025-03-10 23:54:31 +00:00
link2xt
8e9c79061f docs(deltachat-rpc-client): document Account.check_qr() 2025-03-10 23:54:31 +00:00
link2xt
94f57e786d api(deltechat-rpc-client): add Account.wait_for_msgs_noticed_event() 2025-03-10 23:54:31 +00:00
link2xt
db1a7f6084 api(deltachat-rpc-client): add Account.get_device_chat() 2025-03-10 23:54:31 +00:00
link2xt
25df14707e api(deltachat-rpc-client): add Account.device_contact 2025-03-10 23:54:31 +00:00
link2xt
26672900d5 fix: update async-imap to 0.10.23 to fix division by zero 2025-03-10 23:18:16 +00:00
link2xt
82573dc78c api(deltachat-rpc-client): make it possible to clone accounts 2025-03-10 22:43:23 +00:00
link2xt
35d4eb5168 chore(release): prepare for 1.156.3 2025-03-09 20:46:22 +00:00
link2xt
b6d4d10025 chore: update iroh to 0.33 2025-03-09 18:21:24 +00:00
link2xt
53fa9ebf11 build: fixup nix flake after removing git dependency 2025-03-09 18:03:34 +00:00
link2xt
287829d385 build: use mailbuilder from crates.io
This gets rid of the last git dependency.
2025-03-09 17:53:59 +00:00
iequidoo
58b7efe006 refactor: recode_to_size(): Rename strict_limits to is_avatar 2025-03-09 14:49:35 +00:00
link2xt
d2e1e57890 chore: make cargo-deny happy 2025-03-08 01:45:13 +00:00
iequidoo
6a29cca349 fix: Ignore outer Chat-User-Avatar header in Autocrypt-encrypted messages
This is already done for Chat-Group-Avatar.
2025-03-07 22:03:43 -03:00
iequidoo
c51f7a4249 fix: Move Chat-Group-Avatar to hidden headers
This is already done for Chat-User-Avatar. No changes needed on the receiver side, it already parses
Chat-Group-Avatar from hidden headers.
2025-03-07 22:03:43 -03:00
iequidoo
71dfcaa81c docs: Nonstandard headers needing DKIM protection should be hidden 2025-03-07 22:03:43 -03:00
bjoern
8e25639126 feat: delete messages on IMAP when deleting chat (#6613)
this PR deletes all known messages belonging to a chat when the chat is
deleted.

this may not be an exhaustive list as a client might not know all
message-ids (eg. when using different times for "delete from device").
in this case, other devices may know more IDs. otherwise, the chatmail
server will eventually clean up at some point. for non-chatmail, this is
up to the user then.

the deletion sql commands were inspired by
[`delete_msgs_ex`](https://github.com/chatmail/core/blob/main/src/message.rs#L1743)
(in fact, [a first
try](https://github.com/chatmail/core/compare/r10s/clear-chat-on-delete)
was adapting that part, however, that seems less performant as lots of
sql commands are needed)

successor of #5007
2025-03-06 22:43:21 +01:00
link2xt
c4e6823396 api!: remove key_gen_type config
This removes the ability to generate RSA keys.
2025-03-06 21:41:41 +00:00
link2xt
8e5f4a2d53 api(jsonrpc): add API to make and import vCards 2025-03-06 21:12:18 +00:00
link2xt
dd6e3973d2 api(jsonrpc): add import_vcard_contents() method 2025-03-06 21:12:18 +00:00
link2xt
33b9a582f3 test: transfer vCards in TestContext.create_chat()
SecureJoin and importing a vCard are the primary
ways we want to support for creating contacts.
Typing in an email address and relying on Autocrypt
results in sending the first message unencrypted
and we want to clearly separate unencrypted and encrypted
chats in the future.

To make the tests more stable, we set up test contacts
with vCards as this always immediately
results in creating a single encrypted chat
and this is not going to change.
2025-03-06 21:12:18 +00:00
link2xt
0913b6707b api!: remove save_mime_headers config option and dc_get_mime_headers()
This was only used in tests.
`msgs.mime_headers` coulmn remains
as it is used for HTML messages.
2025-03-06 21:12:18 +00:00
link2xt
476224b980 test: replace create_chat() with get_chat() in test_setup_contact_ex() and test_secure_join()
We do not want to create the chat,
but only check some properties of it even when it is hidden.
Creating a chat makes it appear in the chatlist.
2025-03-06 21:12:18 +00:00
dependabot[bot]
b699ac1aca chore(cargo): bump serde_json from 1.0.138 to 1.0.139
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.138 to 1.0.139.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.138...v1.0.139)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 17:15:01 -03:00
B. Petersen
97d8bd89bf test for ChatDeleted event 2025-03-06 20:30:17 +01:00
B. Petersen
9a915b2a95 feat: add chat-deleted event 2025-03-06 20:30:17 +01:00
B. Petersen
d6209e08e6 allow doubled avatar resolution
up to now, avatars were restricted to 20k, 256x256 pixel.
resulting in often poor avatar quality.
(the limitation comes from times
where unencrypted outlook inner headers were a thing;
this has changed meanwhile)

to increase quality while keeping a reasonable small size,
for "balanced quality",
we increase the allowed width to 512x512 and the allowed size to 60k.

for "worse quality", things stay unchanged at 128x128 pixel and 20k.
2025-03-04 22:23:17 +01:00
dependabot[bot]
b9acd603a5 Merge pull request #6606 from deltachat/dependabot/cargo/log-0.4.26 2025-03-03 17:14:37 +00:00
dependabot[bot]
df61905455 chore(cargo): bump log from 0.4.25 to 0.4.26
Bumps [log](https://github.com/rust-lang/log) from 0.4.25 to 0.4.26.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.25...0.4.26)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 16:02:23 +00:00
dependabot[bot]
71582304f3 chore(cargo): bump tracing-subscriber from 0.3.18 to 0.3.19
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.18 to 0.3.19.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.18...tracing-subscriber-0.3.19)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 16:00:51 +00:00
dependabot[bot]
c6b6967fec Merge pull request #6602 from deltachat/dependabot/cargo/chrono-0.4.40 2025-03-03 16:00:32 +00:00
dependabot[bot]
9b4e49e979 Merge pull request #6592 from deltachat/dependabot/cargo/blake3-1.6.1 2025-03-03 16:00:00 +00:00
dependabot[bot]
e3dac9abbb Merge pull request #6604 from deltachat/dependabot/cargo/schemars-0.8.22 2025-03-03 15:58:56 +00:00
dependabot[bot]
1bc97385b9 Merge pull request #6603 from deltachat/dependabot/cargo/libc-0.2.170 2025-03-03 15:58:08 +00:00
dependabot[bot]
ffb903092a Merge pull request #6596 from deltachat/dependabot/cargo/tokio-rustls-0.26.2 2025-03-03 15:57:42 +00:00
link2xt
490171650a chore(release): prepare for 1.156.2 2025-03-02 20:49:30 +00:00
iequidoo
586aae690c feat: Sync chats deletion across devices
Currently broadcast lists creation is synced across devices. Groups creation, in some means, too --
on a group promotion by a first message. Otoh, messages deletion is synced now as well. So, for
feature-completeness, sync chats deletion too.
2025-03-02 20:38:04 +00:00
bjoern
ba0a7f1f0b feat: show sender name in 'Saved Messages' summary (#6607)
since <https://github.com/deltachat/deltachat-core-rust/pull/5606>, 'Saved Messages' contain the sender name; therefore, show
the name in summaries as for groups
2025-03-02 18:59:00 +00:00
dependabot[bot]
a50b43598f chore(cargo): bump serde from 1.0.217 to 1.0.218
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.217 to 1.0.218.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.217...v1.0.218)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-02 18:31:27 +00:00
dependabot[bot]
02a18420e5 chore(cargo): bump libc from 0.2.169 to 0.2.170
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.169 to 0.2.170.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.170/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.169...0.2.170)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-02 18:25:17 +00:00
dependabot[bot]
ef6c5870bb chore(cargo): bump anyhow from 1.0.95 to 1.0.96
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.95 to 1.0.96.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.95...1.0.96)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-02 18:24:20 +00:00
link2xt
efbc4780f2 fix: upgrade native-tls from 0.2.13 to 0.2.14
This fixes the problem introduced by upgrading native-tls from 0.2.11 to 0.2.13 that prevented using OpenSSL on Android.
2025-03-02 18:16:15 +00:00
dependabot[bot]
5c49706dfd chore(cargo): bump schemars from 0.8.21 to 0.8.22
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.21 to 0.8.22.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.21...v0.8.22)

---
updated-dependencies:
- dependency-name: schemars
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 21:19:46 +00:00
dependabot[bot]
36e6b2306b chore(cargo): bump chrono from 0.4.39 to 0.4.40
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.39 to 0.4.40.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.39...v0.4.40)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 21:19:13 +00:00
dependabot[bot]
4942303c19 chore(cargo): bump tokio-rustls from 0.26.1 to 0.26.2
Bumps [tokio-rustls](https://github.com/rustls/tokio-rustls) from 0.26.1 to 0.26.2.
- [Release notes](https://github.com/rustls/tokio-rustls/releases)
- [Commits](https://github.com/rustls/tokio-rustls/commits)

---
updated-dependencies:
- dependency-name: tokio-rustls
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 21:17:02 +00:00
dependabot[bot]
2168e39156 chore(cargo): bump blake3 from 1.5.5 to 1.6.1
Bumps [blake3](https://github.com/BLAKE3-team/BLAKE3) from 1.5.5 to 1.6.1.
- [Release notes](https://github.com/BLAKE3-team/BLAKE3/releases)
- [Commits](https://github.com/BLAKE3-team/BLAKE3/compare/1.5.5...1.6.1)

---
updated-dependencies:
- dependency-name: blake3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 21:15:45 +00:00
B. Petersen
2ee83bf786 docs: add DC_QR_BACKUP_TOO_NEW documentation 2025-02-28 22:21:57 +01:00
link2xt
43a40b9349 chore(release): prepare for 1.156.1 2025-02-28 01:23:21 +00:00
link2xt
43a3e40bc7 fix: update mailparse to 0.16.1 to fix panic when parsing a message 2025-02-28 01:12:01 +00:00
link2xt
33f96d4010 build: update fuzzing setup
Say in the README that cargo-bolero@0.8.0
should be installed as newer versions
do not work currently.

Update `mailparse` to 0.16.0
because this is what Delta Chat currently uses.

I also added fuzzer outputs to .gitignore
2025-02-27 21:05:33 +00:00
iequidoo
b5e9a5ebb6 fix: Add Chat-Group-Name-Timestamp header and use it to update group names (#6412)
Add "Chat-Group-Name-Timestamp" message header and use the last-write-wins logic when updating group
names (similar to group member timestamps). Note that if the "Chat-Group-Name-Changed" header is
absent though, we don't add a system message (`MsgGrpNameChangedBy`) because we don't want to blame
anyone.
2025-02-27 15:45:09 -03:00
link2xt
44f72e7f85 fix: log tokio::fs::metadata errors 2025-02-27 16:56:55 +00:00
58 changed files with 2643 additions and 2231 deletions

4
.gitignore vendored
View File

@@ -1,7 +1,9 @@
/target
target/
**/*.rs.bk
/build
/dist
/fuzz/fuzz_targets/corpus/
/fuzz/fuzz_targets/crashes/
# ignore vi temporaries
*~

View File

@@ -1,5 +1,76 @@
# Changelog
## [1.156.3] - 2025-03-09
### API-Changes
- jsonrpc: Add import_vcard_contents() method.
- jsonrpc: Add API to make and import vCards.
- [**breaking**] Remove save_mime_headers config option and dc_get_mime_headers().
- [**breaking**] Remove key_gen_type config.
### Features / Changes
- Add chat-deleted event.
- Delete messages on IMAP when deleting chat ([#6613](https://github.com/deltachat/deltachat-core-rust/pull/6613)).
- Allow doubled avatar resolution
### Fixes
- Move Chat-Group-Avatar to hidden headers.
- Ignore outer Chat-User-Avatar header in Autocrypt-encrypted messages.
### Build system
- Use mailbuilder from crates.io.
- Update iroh to 0.33.
### Documentation
- Nonstandard headers needing DKIM protection should be hidden.
### Refactor
- Recode_to_size(): Rename strict_limits to is_avatar.
### Tests
- Test for ChatDeleted event.
- Replace create_chat() with get_chat() in test_setup_contact_ex() and test_secure_join().
- Transfer vCards in TestContext.create_chat().
## [1.156.2] - 2025-03-02
### Fixes
- Upgrade native-tls from 0.2.13 to 0.2.14. This fixes "Accept invalid certificates" failing on Android with "OpenSSL error". The bug was there since 1.156.0 due to upgrade of native-tls from 0.2.11 to 0.2.13.
### Features / Changes
- Show sender name in 'Saved Messages' summary ([#6607](https://github.com/deltachat/deltachat-core-rust/pull/6607)).
- Sync chats deletion across devices.
### Documentation
- Add DC_QR_BACKUP_TOO_NEW documentation.
### Miscellaneous Tasks
- cargo: Bump anyhow from 1.0.95 to 1.0.96.
- cargo: Bump serde from 1.0.217 to 1.0.218.
## [1.156.1] - 2025-02-28
### Fixes
- Update mailparse to 0.16.1 to fix panic when parsing a message.
- Add Chat-Group-Name-Timestamp header and use it to update group names ([#6412](https://github.com/deltachat/deltachat-core-rust/pull/6412)).
- Log tokio::fs::metadata errors.
### Build system
- Update fuzzing setup.
## [1.156.0] - 2025-02-26
### API-Changes
@@ -5875,3 +5946,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.155.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.4..v1.155.5
[1.155.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.5..v1.155.6
[1.156.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.6..v1.156.0
[1.156.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.156.0..v1.156.1
[1.156.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.156.1..v1.156.2
[1.156.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.156.2..v1.156.3

485
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.156.0"
version = "1.156.3"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.81"
@@ -41,7 +41,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.10.3", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -57,18 +57,18 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "=0.25.0-alpha.4"
hickory-resolver = "=0.25.0-alpha.5"
http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.10"
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.32", default-features = false, features = ["net"] }
iroh = { version = "0.32", default-features = false }
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
iroh = { version = "0.33", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", branch = "main", default-features = false }
mailparse = "0.16"
mail-builder = { version = "0.4.2", default-features = false }
mailparse = "0.16.1"
mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
@@ -101,7 +101,7 @@ tagger = "4.3.4"
textwrap = "0.16.1"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.1", default-features = false }
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
@@ -110,7 +110,7 @@ toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
blake3 = "1.5.5"
blake3 = "1.6.1"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
@@ -174,7 +174,7 @@ harness = false
anyhow = "1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.39", default-features = false }
chrono = { version = "0.4.40", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }

View File

@@ -139,7 +139,7 @@ $ cargo test -- --ignored
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
```sh
$ cargo install cargo-bolero
$ cargo install cargo-bolero@0.8.0
```
Run fuzzing tests with

View File

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

View File

@@ -440,17 +440,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts (default).
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Curve25519 keypair
* DC_KEY_GEN_RSA4096 (3)=
* generate RSA 4096 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
* >=1=seconds, after which messages are deleted automatically from the device.
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
@@ -1960,23 +1949,6 @@ char* dc_get_msg_html (dc_context_t* context, uint32_t ms
void dc_download_full_msg (dc_context_t* context, int msg_id);
/**
* Get the raw mime-headers of the given message.
* Raw headers are saved for incoming messages
* only if `dc_set_config(context, "save_mime_headers", "1")`
* was called before.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID, must be the ID of an incoming message.
* @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage.
* Returns NULL if there are no headers saved for the given message,
* e.g. because of save_mime_headers is not set
* or the message is not incoming.
*/
char* dc_get_mime_headers (dc_context_t* context, uint32_t msg_id);
/**
* Delete messages. The messages are deleted on all devices and
* on the IMAP server.
@@ -2540,11 +2512,14 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to create an account on the given domain,
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* - DC_QR_BACKUP:
* - DC_QR_BACKUP2:
* ask the user if they want to set up a new device.
* If so, pass the qr-code to dc_receive_backup().
*
* - DC_QR_BACKUP_TOO_NEW:
* show a hint to the user that this backup comes from a newer Delta Chat version
* and this device needs an update
*
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
@@ -6311,6 +6286,18 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
/**
* Chat was deleted.
* This event is emitted in response to dc_delete_chat()
* called on this or another device.
* The event is a good place to remove notifications or homescreen shortcuts.
*
* @param data1 (int) chat_id
* @param data2 (int) 0
*/
#define DC_EVENT_CHAT_DELETED 2023
/**
* Contact(s) created, renamed, verified, blocked or deleted.
*
@@ -6551,15 +6538,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_MEDIA_QUALITY_WORSE 1
/*
* Values for dc_get|set_config("key_gen_type")
*/
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
#define DC_KEY_GEN_RSA4096 3
/**
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
*

View File

@@ -544,6 +544,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgDeleted { .. } => 2016,
EventType::ChatModified(_) => 2020,
EventType::ChatEphemeralTimerModified { .. } => 2021,
EventType::ChatDeleted { .. } => 2023,
EventType::ContactsChanged(_) => 2030,
EventType::LocationChanged(_) => 2035,
EventType::ConfigureProgress { .. } => 2041,
@@ -610,7 +611,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::MsgRead { chat_id, .. }
| EventType::MsgDeleted { chat_id, .. }
| EventType::ChatModified(chat_id)
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
| EventType::ChatEphemeralTimerModified { chat_id, .. }
| EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
let id = id.unwrap_or_default();
id.to_u32() as libc::c_int
@@ -676,6 +678,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsItemChanged
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::ChatDeleted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
@@ -767,6 +770,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::WebxdcInstanceDeleted { .. }
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::ChatDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
@@ -1949,28 +1953,6 @@ pub unsafe extern "C" fn dc_get_msg_html(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_mime_headers(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_mime_headers()");
return ptr::null_mut(); // NULL explicitly defined as "no mime headers"
}
let ctx = &*context;
block_on(async move {
let mime = message::get_mime_headers(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get mime headers");
if mime.is_empty() {
return ptr::null_mut();
}
mime.strdup()
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_delete_msgs(
context: *mut dc_context_t,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.156.0"
version = "1.156.3"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -17,7 +17,7 @@ anyhow = { workspace = true }
deltachat = { workspace = true }
deltachat-contact-tools = { workspace = true }
num-traits = { workspace = true }
schemars = "0.8.21"
schemars = "0.8.22"
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
log = { workspace = true }

View File

@@ -1518,6 +1518,18 @@ impl CommandApi {
.collect())
}
/// Imports contacts from a vCard.
///
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
async fn import_vcard_contents(&self, account_id: u32, vcard: String) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(deltachat::contact::import_vcard(&ctx, &vcard)
.await?
.into_iter()
.map(|c| c.to_u32())
.collect())
}
/// Returns a vCard containing contacts with the given ids.
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
let ctx = self.get_context(account_id).await?;

View File

@@ -243,6 +243,12 @@ pub enum EventType {
timer: u32,
},
/// Chat deleted.
ChatDeleted {
/// Chat ID.
chat_id: u32,
},
/// Contact(s) created, renamed, blocked or deleted.
#[serde(rename_all = "camelCase")]
ContactsChanged {
@@ -499,6 +505,9 @@ impl From<CoreEventType> for EventType {
timer: timer.to_u32(),
}
}
CoreEventType::ChatDeleted { chat_id } => ChatDeleted {
chat_id: chat_id.to_u32(),
},
CoreEventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()),
},

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.156.0"
version = "1.156.3"
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.156.0"
version = "1.156.3"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
@@ -38,6 +40,16 @@ class Account:
"""Remove the account."""
self._rpc.remove_account(self.id)
def clone(self) -> "Account":
"""Clone given account."""
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
self.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
new_account = self.manager.add_account()
new_account.import_backup(files[0])
return new_account
def start_io(self) -> None:
"""Start the account I/O."""
self._rpc.start_io(self.id)
@@ -83,6 +95,10 @@ class Account:
return self.get_config("selfavatar")
def check_qr(self, qr):
"""Parse QR code contents.
This function takes the raw text scanned
and checks what can be done with it."""
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
@@ -118,11 +134,28 @@ class Account:
obj = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
def make_vcard(self, contacts: list[Contact]) -> str:
"""Create vCard with the given contacts."""
assert all(contact.account == self for contact in contacts)
contact_ids = [contact.id for contact in contacts]
return self._rpc.make_vcard(self.id, contact_ids)
def import_vcard(self, vcard: str) -> list[Contact]:
"""Import vCard.
Return created or modified contacts in the order they appear in vCard."""
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
return [Contact(self, contact_id) for contact_id in contact_ids]
def create_chat(self, account: "Account") -> Chat:
addr = account.get_config("addr")
contact = self.create_contact(addr)
vcard = account.self_contact.make_vcard()
[contact] = self.import_vcard(vcard)
return contact.create_chat()
def get_device_chat(self) -> Chat:
"""Return device chat."""
return self.device_contact.create_chat()
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
@@ -183,6 +216,11 @@ class Account:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
@property
def device_contact(self) -> Chat:
"""This account's device contact."""
return Contact(self, SpecialContactId.DEVICE)
def get_chatlist(
self,
query: Optional[str] = None,
@@ -308,6 +346,13 @@ class Account:
if event.kind == EventType.MSGS_CHANGED:
return event
def wait_for_msgs_noticed_event(self):
"""Wait for messages noticed event and return it."""
while True:
event = self.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
return event
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.

View File

@@ -66,4 +66,4 @@ class Contact:
)
def make_vcard(self) -> str:
return self._rpc.make_vcard(self.account.id, [self.id])
return self.account.make_vcard([self])

View File

@@ -157,11 +157,7 @@ def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Acc
bob.wait_for_incoming_msg_event()
alice_second_device: Account = acfactory.get_unconfigured_account()
alice._rpc.provide_backup.future(alice.id)
backup_code = alice._rpc.get_backup_qr(alice.id)
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
alice_second_device = alice.clone()
alice_second_device.start_io()
alice.clear_all_events()
alice_second_device.clear_all_events()

View File

@@ -60,15 +60,12 @@ def test_qr_setup_contact_svg(acfactory) -> None:
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect, tmp_path):
def test_qr_securejoin(acfactory, protect):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
# to test observing securejoin protocol.
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
alice2 = alice.clone()
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group", protect=protect)

View File

@@ -287,12 +287,9 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
def test_reaction_seen_on_another_dev(acfactory, tmp_path) -> None:
def test_reaction_seen_on_another_dev(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
alice2 = alice.clone()
alice2.start_io()
bob_addr = bob.get_config("addr")
@@ -661,7 +658,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
assert snapshot.chat == bob_chat_alice
def test_markseen_contact_request(acfactory, tmp_path):
def test_markseen_contact_request(acfactory):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
@@ -669,10 +666,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
bob.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
bob2 = acfactory.get_unconfigured_account()
bob2.import_backup(files[0])
bob2 = bob.clone()
bob2.start_io()
alice_chat_bob = alice.create_chat(bob)

View File

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

View File

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

View File

@@ -10,8 +10,11 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# DNSSEC validation that we don't use anyway.
"RUSTSEC-2025-0006",
# Unmaintained backoff
"RUSTSEC-2025-0012",
# Unmaintained paste
"RUSTSEC-2024-0436",
]
[bans]
@@ -27,12 +30,16 @@ skip = [
{ name = "core-foundation", version = "0.9.4" },
{ name = "event-listener", version = "2.5.3" },
{ name = "generator", version = "0.7.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "http", version = "0.2.12" },
{ name = "loom", version = "0.5.6" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nix", version = "0.26.4" },
{ name = "nix", version = "0.27.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
@@ -45,6 +52,7 @@ skip = [
{ name = "tokio-tungstenite", version = "0.21.0" },
{ name = "tungstenite", version = "0.21.0" },
{ name = "unicode-width", version = "0.1.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
{ name = "windows_aarch64_msvc" },
@@ -61,6 +69,7 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "zerocopy", version = "0.7.32" },
]
@@ -75,7 +84,6 @@ allow = [
"ISC",
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
@@ -87,9 +95,3 @@ expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[sources.allow-org]
# Organisations which we allow git sources from.
github = [
"stalwartlabs",
]

View File

@@ -87,9 +87,6 @@
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"mail-builder-0.4.1" = "sha256-1hnsU76ProcX7iXT2UBjHnHbJ/ROT3077sLi3+yAV58=";
};
};
mkRustPackage = packageName:
naersk'.buildPackage {

3303
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ edition = "2021"
bolero = "0.8"
[dependencies]
mailparse = "0.13"
mailparse = "0.16"
deltachat = { path = ".." }
format-flowed = { path = "../format-flowed" }

View File

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

View File

@@ -285,23 +285,6 @@ class Message:
"""Force the message to be sent in plain text."""
lib.dc_msg_force_plaintext(self._dc_msg)
def get_mime_headers(self):
"""return mime-header object for an incoming message.
This only returns a non-None object if ``save_mime_headers``
config option was set and the message is incoming.
:returns: email-mime message object (with headers only, no body).
"""
import email
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
if isinstance(s, bytes):
return email.message_from_bytes(s)
return email.message_from_string(s)
@property
def error(self) -> Optional[str]:
"""Error message."""

View File

@@ -31,37 +31,6 @@ def test_basic_imap_api(acfactory, tmp_path):
imap2.shutdown()
@pytest.mark.ignored()
def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys.
acfactory.remove_preconfigured_keys()
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1._evtracker.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2._evtracker.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
def test_configure_canceled(acfactory):
ac1 = acfactory.new_online_configuring_account()
ac1.stop_ongoing()
@@ -1010,7 +979,6 @@ def test_gossip_encryption_preference(acfactory, lp):
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.set_config("save_mime_headers", "1")
lp.sec("ac1: create chat with ac2")
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -1400,26 +1368,6 @@ def test_quote_attachment(tmp_path, acfactory, lp):
assert open(received_reply.filename).read() == "data to send"
def test_saved_mime_on_received_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat")
ac2.set_config("save_mime_headers", "1")
chat = ac1.create_chat(ac2)
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
ac1._evtracker.wait_msg_delivered(msg_out)
assert msg_out.get_mime_headers() is None
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
in_id = ev.data2
mime = ac2.get_message_by_id(in_id).get_mime_headers()
assert mime.get_all("From")
assert mime.get_all("Received")
def test_send_mark_seen_clean_incoming_events(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)

View File

@@ -105,10 +105,6 @@ class TestOfflineAccountBasic:
ac1.update_config({"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()

View File

@@ -1 +1 @@
2025-02-26
2025-03-09

View File

@@ -252,25 +252,28 @@ impl<'a> BlobObject<'a> {
}
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
let img_wh =
let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
MediaQuality::Balanced => (
constants::BALANCED_AVATAR_SIZE,
constants::BALANCED_AVATAR_BYTES,
),
MediaQuality::Worse => {
(constants::WORSE_AVATAR_SIZE, constants::WORSE_AVATAR_BYTES)
}
};
let maybe_sticker = &mut false;
let strict_limits = true;
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
let is_avatar = true;
self.recode_to_size(
context,
None, // The name of an avatar doesn't matter
maybe_sticker,
img_wh,
20_000,
strict_limits,
max_bytes,
is_avatar,
)?;
Ok(())
@@ -299,21 +302,17 @@ impl<'a> BlobObject<'a> {
),
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
let strict_limits = false;
let new_name = self.recode_to_size(
context,
name,
maybe_sticker,
img_wh,
max_bytes,
strict_limits,
)?;
let is_avatar = false;
let new_name =
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
Ok(new_name)
}
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
/// proceed with the result.
/// Recodes the image so that it fits into limits on width/height and byte size.
///
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
/// with the result without rechecking.
///
/// This modifies the blob object in-place.
///
@@ -328,10 +327,10 @@ impl<'a> BlobObject<'a> {
maybe_sticker: &mut bool,
mut img_wh: u32,
max_bytes: usize,
strict_limits: bool,
is_avatar: bool,
) -> Result<String> {
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
let mut add_white_bg = is_avatar;
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let mut name = name.unwrap_or_else(|| self.name.clone());
@@ -402,7 +401,7 @@ impl<'a> BlobObject<'a> {
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
// images.
let do_scale = exceeds_max_bytes
|| strict_limits
|| is_avatar
&& (exceeds_wh
|| exif.is_some() && {
if mem::take(&mut add_white_bg) {
@@ -439,7 +438,7 @@ impl<'a> BlobObject<'a> {
ofmt.clone(),
max_bytes,
&mut encoded,
)? && strict_limits
)? && is_avatar
{
if img_wh < 20 {
return Err(format_err!(
@@ -489,7 +488,7 @@ impl<'a> BlobObject<'a> {
match res {
Ok(_) => res,
Err(err) => {
if !strict_limits && no_exif {
if !is_avatar && no_exif {
warn!(
context,
"Cannot recode image, using original data: {err:#}.",

View File

@@ -174,7 +174,7 @@ async fn test_selfavatar_outside_blobdir() {
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
let avatar_path = Path::new(&avatar_blob);
assert!(
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
avatar_blob.ends_with("009161310a6afc319163e4bcabd23b9.jpg"),
"The avatar filename should be its hash, put instead it's {avatar_blob}"
);
let scaled_avatar_size = file_size(avatar_path).await;
@@ -226,7 +226,7 @@ async fn test_selfavatar_in_blobdir() {
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert!(
avatar_cfg.ends_with("fa7418e646301203538041f60d03190.png"),
avatar_cfg.ends_with("ec054c444a5755adf2b0aaea40209f2.png"),
"Avatar file name {avatar_cfg} should end with its hash"
);

View File

@@ -434,7 +434,7 @@ impl ChatId {
.ok();
}
if delete {
self.delete(context).await?;
self.delete_ex(context, Nosync).await?;
}
Ok(())
}
@@ -773,6 +773,10 @@ impl ChatId {
/// Deletes a chat.
pub async fn delete(self, context: &Context) -> Result<()> {
self.delete_ex(context, Sync).await
}
pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
ensure!(
!self.is_special(),
"bad chat_id, can not be a special chat: {}",
@@ -780,10 +784,23 @@ impl ChatId {
);
let chat = Chat::load_from_db(context, self).await?;
let delete_msgs_target = context.get_delete_msgs_target().await?;
let sync_id = match sync {
Nosync => None,
Sync => chat.get_sync_id(context).await?,
};
context
.sql
.transaction(|transaction| {
transaction.execute(
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
(delete_msgs_target, self,),
)?;
transaction.execute(
"DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
)?;
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
@@ -795,13 +812,15 @@ impl ChatId {
})
.await?;
context.emit_event(EventType::ChatDeleted { chat_id: self });
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;
context.scheduler.interrupt_inbox().await;
if let Some(id) = sync_id {
self::sync(context, id, SyncAction::Delete)
.await
.log_err(context)
.ok();
}
if chat.is_self_talk() {
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
@@ -809,6 +828,11 @@ impl ChatId {
}
chatlist_events::emit_chatlist_changed(context);
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;
context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -3039,6 +3063,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.state = MessageState::OutDelivered;
return Ok(Vec::new());
}
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
msg.chat_id
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
.await?;
}
let rendered_msg = match mimefactory.render(context).await {
Ok(res) => Ok(res),
@@ -4870,6 +4899,7 @@ pub(crate) enum SyncAction {
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
Delete,
}
impl Context {
@@ -4929,6 +4959,7 @@ impl Context {
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
}
}

View File

@@ -1883,10 +1883,6 @@ async fn test_sticker(
msg.set_file_and_deduplicate(&alice, &file, Some(filename), None)?;
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let mime = sent_msg.payload();
if res_viewtype == Viewtype::Sticker {
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
}
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, bob_chat.id);
@@ -3030,6 +3026,48 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_delete_chat() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
let a1b_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
a0b_chat_id.accept(alice0).await?;
sync(alice0, alice1).await;
a0b_chat_id.delete(alice0).await?;
sync(alice0, alice1).await;
alice1.assert_no_chat(a1b_chat_id).await;
alice1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
.await;
let bob_grp_chat_id = bob
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
.await;
let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await;
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
a0_grp_chat_id.accept(alice0).await?;
sync(alice0, alice1).await;
a0_grp_chat_id.delete(alice0).await?;
sync(alice0, alice1).await;
alice1.assert_no_chat(a1_grp_chat_id).await;
alice0
.evtracker
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_adhoc_grp() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
@@ -3212,6 +3250,10 @@ async fn test_sync_broadcast() -> Result<()> {
assert!(get_past_chat_contacts(alice1, a1_broadcast_id)
.await?
.is_empty());
a0_broadcast_id.delete(alice0).await?;
sync(alice0, alice1).await;
alice1.assert_no_chat(a1_broadcast_id).await;
Ok(())
}

View File

@@ -407,16 +407,17 @@ impl Chatlist {
let lastcontact = if let Some(lastmsg) = &lastmsg {
if lastmsg.from_id == ContactId::SELF {
None
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::Broadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
Some(lastcontact)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
Some(lastcontact)
}
Chattype::Single => None,
}
None
}
} else {
None
@@ -479,6 +480,7 @@ pub async fn get_last_message_for_chat(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
@@ -486,6 +488,7 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
@@ -787,6 +790,31 @@ mod tests {
assert!(summary_res.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_for_saved_messages() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat_alice = alice.create_chat(&bob).await;
send_text_msg(&alice, chat_alice.id, "hi".into()).await?;
let sent1 = alice.pop_sent_msg().await;
save_msgs(&alice, &[sent1.sender_msg_id]).await?;
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
let summary = chatlist.get_summary(&alice, 0, None).await?;
assert_eq!(summary.prefix.unwrap().to_string(), "Me");
assert_eq!(summary.text, "hi");
let msg = bob.recv_msg(&sent1).await;
save_msgs(&bob, &[msg.id]).await?;
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
assert_eq!(summary.prefix.unwrap().to_string(), "alice@example.org");
assert_eq!(summary.text, "hi");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;

View File

@@ -193,10 +193,6 @@ pub enum Config {
#[strum(props(default = "1"))]
FetchedExistingMsgs,
/// Type of the OpenPGP key to generate.
#[strum(props(default = "0"))]
KeyGenType,
/// Timer in seconds after which the message is deleted from the
/// server.
///
@@ -220,9 +216,6 @@ pub enum Config {
/// `ProviderOptions::delete_to_trash`.
DeleteToTrash,
/// Save raw MIME messages with headers in the database if true.
SaveMimeHeaders,
/// The primary email address. Also see `SecondaryAddrs`.
ConfiguredAddr,
@@ -716,7 +709,6 @@ impl Context {
| Config::OnlyFetchMvbox
| Config::FetchExistingMsgs
| Config::DeleteToTrash
| Config::SaveMimeHeaders
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw

View File

@@ -58,25 +58,6 @@ pub enum MediaQuality {
Worse = 1,
}
/// Type of the key to generate.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum KeyGenType {
#[default]
Default = 0,
/// 2048-bit RSA.
Rsa2048 = 1,
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
Ed25519 = 2,
/// 4096-bit RSA.
Rsa4096 = 3,
}
/// Video chat URL type.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
@@ -204,9 +185,11 @@ pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
// max. width/height of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
// max. width/height and bytes of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
// max. width/height of images scaled down because of being too huge
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
@@ -253,16 +236,6 @@ mod tests {
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
}
#[test]
fn test_keygentype_values() {
// values may be written to disk and must not change
assert_eq!(KeyGenType::Default, KeyGenType::default());
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
}
#[test]
fn test_showemails_values() {
// values may be written to disk and must not change

View File

@@ -1207,16 +1207,16 @@ async fn test_reset_encryption() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
@@ -1235,6 +1235,7 @@ async fn test_reset_verified_encryption() -> Result<()> {
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
// Check that the contact is still verified after resetting encryption.
@@ -1250,7 +1251,8 @@ async fn test_reset_verified_encryption() -> Result<()> {
"bob@example.net sent a message from another device."
);
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())

View File

@@ -919,12 +919,6 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert(
"save_mime_headers",
self.get_config_bool(Config::SaveMimeHeaders)
.await?
.to_string(),
);
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
@@ -944,10 +938,6 @@ impl Context {
res.insert("configured_trash_folder", configured_trash_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert(
"key_gen_type",
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("sync_msgs", sync_msgs.to_string());
res.insert("disable_idle", disable_idle.to_string());

View File

@@ -228,8 +228,8 @@ Sent with my Delta Chat Messenger: https://delta.chat";
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
let chat_alice = alice.create_email_chat(&bob).await.id;
let chat_bob = bob.create_email_chat(&alice).await.id;
// Alice sends unencrypted message to Bob
let mut msg = Message::new(Viewtype::Text);

View File

@@ -219,6 +219,12 @@ pub enum EventType {
timer: EphemeralTimer,
},
/// Chat was deleted.
ChatDeleted {
/// Chat ID.
chat_id: ChatId,
},
/// Contact(s) created, renamed, blocked, deleted or changed their "recently seen" status.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.

View File

@@ -57,6 +57,7 @@ pub enum HeaderDef {
ChatGroupId,
ChatGroupName,
ChatGroupNameChanged,
ChatGroupNameTimestamp,
ChatVerified,
ChatGroupAvatar,
ChatUserAvatar,

View File

@@ -7,7 +7,6 @@ use std::io::Cursor;
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::EmailAddress;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
@@ -15,8 +14,6 @@ use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use rand::thread_rng;
use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::log::LogExt;
use crate::pgp::KeyPair;
@@ -282,11 +279,9 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
Some(key_pair) => Ok(key_pair),
None => {
let start = tools::Time::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
info!(context, "Generating keypair.");
let keypair = Handle::current()
.spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
.spawn_blocking(move || crate::pgp::create_keypair(addr))
.await??;
store_self_keypair(context, &keypair).await?;
@@ -466,6 +461,7 @@ mod tests {
use once_cell::sync::Lazy;
use super::*;
use crate::config::Config;
use crate::test_utils::{alice_keypair, TestContext};
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);

View File

@@ -1596,14 +1596,12 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &
}
/// Get the raw mime-headers of the given message.
/// Raw headers are saved for incoming messages
/// only if `set_config(context, "save_mime_headers", "1")`
/// was called before.
/// Raw headers are saved for large messages
/// that need a "Show full message..."
/// to see HTML part.
///
/// Returns an empty vector if there are no headers saved for the given message,
/// e.g. because of save_mime_headers is not set
/// or the message is not incoming.
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
/// Returns an empty vector if there are no headers saved for the given message.
pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
let (headers, compressed) = context
.sql
.query_row(

View File

@@ -835,11 +835,13 @@ impl MimeFactory {
// placed here.
let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new();
// Headers that MUST NOT go into IMF header section.
//
// These are large headers which may hit the header section size limit on the server, such as
// Chat-User-Avatar with a base64-encoded image inside. Also there are headers duplicated here
// that servers mess up with in the IMF header section, like Message-ID.
// Headers that MUST NOT (only) go into IMF header section:
// - Large headers which may hit the header section size limit on the server, such as
// Chat-User-Avatar with a base64-encoded image inside.
// - Headers duplicated here that servers mess up with in the IMF header section, like
// Message-ID.
// - Nonstandard headers that should be DKIM-protected because e.g. OpenDKIM only signs
// known headers.
//
// The header should be hidden from MTA
// by moving it either into protected part
@@ -868,6 +870,7 @@ impl MimeFactory {
unprotected_headers.push(header.clone());
hidden_headers.push(header.clone());
} else if header_name == "chat-user-avatar"
|| header_name == "chat-group-avatar"
|| header_name == "chat-delete"
|| header_name == "chat-edit"
{
@@ -1190,6 +1193,12 @@ impl MimeFactory {
"Chat-Group-Name",
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
));
if let Some(ts) = chat.param.get_i64(Param::GroupNameTimestamp) {
headers.push((
"Chat-Group-Name-Timestamp",
mail_builder::headers::text::Text::new(ts.to_string()).into(),
));
}
match command {
SystemMessage::MemberRemovedFromGroup => {

View File

@@ -5,7 +5,7 @@ use std::str;
use super::*;
use crate::chat::{
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId,
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId,
ProtectionStatus,
};
use crate::chatlist::Chatlist;
@@ -622,6 +622,43 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_avatar_unencrypted() -> anyhow::Result<()> {
let t = &TestContext::new_alice().await;
let group_id = chat::create_group_chat(t, chat::ProtectionStatus::Unprotected, "Group")
.await
.unwrap();
let bob = Contact::create(t, "", "bob@example.org").await?;
chat::add_contact_to_chat(t, group_id, bob).await?;
let file = t.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
chat::set_chat_profile_image(t, group_id, file.to_str().unwrap()).await?;
// Send message to bob: that should get multipart/mixed because of the avatar moved to inner header.
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(group_id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
let outer = payload.next().unwrap();
let inner = payload.next().unwrap();
let body = payload.next().unwrap();
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
assert_eq!(outer.match_indices("Subject:").count(), 1);
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
assert_eq!(outer.match_indices("Chat-Group-Avatar:").count(), 0);
assert_eq!(inner.match_indices("text/plain").count(), 1);
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-Group-Avatar:").count(), 1);
assert_eq!(body.match_indices("this is the text!").count(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_unencrypted_signed() {
// create chat with bob, set selfavatar

View File

@@ -446,6 +446,7 @@ impl MimeMessage {
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupNameTimestamp,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
@@ -453,6 +454,7 @@ impl MimeMessage {
HeaderDef::ChatGroupPastMembers,
HeaderDef::ChatDelete,
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
headers.remove(h.get_headername());
}

View File

@@ -312,6 +312,19 @@ fn test_mailparse_content_type() {
);
}
/// Test to reproduce
/// <https://github.com/staktrace/mailparse/issues/130>.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mailparse_0_16_0_panic() {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/mailparse-0.16.0-panic.eml");
// There should be an error, but no panic.
assert!(MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_first_addr() {
let context = TestContext::new().await;
@@ -1798,26 +1811,39 @@ async fn test_take_last_header() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt() -> Result<()> {
async fn test_protect_autocrypt(enabled: bool) -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
alice
.set_config_bool(Config::ProtectAutocrypt, true)
.set_config_bool(Config::ProtectAutocrypt, enabled)
.await?;
bob.set_config_bool(Config::ProtectAutocrypt, true).await?;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
let sent = alice.send_text(chat.id, "Hello!").await;
assert_eq!(sent.payload().contains("Autocrypt: "), !enabled);
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), true);
Ok(())
}
/// Tests that if `protect_autocrypt` is enabled,
/// `Autocrypt` header does not appear in the outer headers
/// of encrypted messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt_enabled() -> Result<()> {
test_protect_autocrypt(true).await
}
/// Tests that if `protect_autocrypt` is disabled,
/// `Autocrypt` header appears in the outer headers
/// of encrypted messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt_false() -> Result<()> {
test_protect_autocrypt(false).await
}
/// Tests that CRLF before MIME boundary
/// is not treated as the part body.
///

View File

@@ -18,7 +18,6 @@ use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToK
use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
use crate::constants::KeyGenType;
use crate::key::{DcKey, Fingerprint};
#[cfg(test)]
@@ -181,15 +180,9 @@ impl KeyPair {
///
/// Both secret and public key consist of signing primary key and encryption subkey
/// as [described in the Autocrypt standard](https://autocrypt.org/level1.html#openpgp-based-key-data).
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
let (signing_key_type, encryption_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
KeyGenType::Ed25519 | KeyGenType::Default => (
PgpKeyType::EdDSALegacy,
PgpKeyType::ECDH(ECCCurve::Curve25519),
),
};
pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
let signing_key_type = PgpKeyType::EdDSALegacy;
let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519);
let user_id = format!("<{addr}>");
let key_params = SecretKeyParamsBuilder::default()
@@ -478,16 +471,8 @@ mod tests {
#[test]
fn test_create_keypair() {
let keypair0 = create_keypair(
EmailAddress::new("foo@bar.de").unwrap(),
KeyGenType::Default,
)
.unwrap();
let keypair1 = create_keypair(
EmailAddress::new("two@zwo.de").unwrap(),
KeyGenType::Default,
)
.unwrap();
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap();
assert_ne!(keypair0.public, keypair1.public);
}

View File

@@ -1405,10 +1405,6 @@ async fn add_parts(
)
.await?;
// if the mime-headers should be saved, find out its size
// (the mime-header ends with an empty line)
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await?;
let mime_in_reply_to = mime_parser
.get_header(HeaderDef::InReplyTo)
.unwrap_or_default();
@@ -1434,7 +1430,7 @@ async fn add_parts(
// `true` finally.
let mut save_mime_modified = false;
let mime_headers = if save_mime_headers || mime_parser.is_mime_modified {
let mime_headers = if mime_parser.is_mime_modified {
let headers = if !mime_parser.decoded_data.is_empty() {
mime_parser.decoded_data.clone()
} else {
@@ -1698,7 +1694,7 @@ RETURNING id
},
hidden,
part.bytes as isize,
if (save_mime_headers || save_mime_modified) && !(trash || hidden) {
if save_mime_modified && !(trash || hidden) {
mime_headers.clone()
} else {
Vec::new()
@@ -2411,9 +2407,19 @@ async fn apply_group_changes(
}
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
} else if let Some(old_name) = mime_parser
}
let group_name_timestamp = mime_parser
.get_header(HeaderDef::ChatGroupNameTimestamp)
.and_then(|s| s.parse::<i64>().ok());
if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.map(|s| s.trim())
.or(match group_name_timestamp {
Some(0) => None,
Some(_) => Some(chat.name.as_str()),
None => None,
})
{
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
@@ -2422,13 +2428,15 @@ async fn apply_group_changes(
{
let grpname = &sanitize_single_line(grpname);
let old_name = &sanitize_single_line(old_name);
if chat_id
.update_timestamp(
context,
Param::GroupNameTimestamp,
mime_parser.timestamp_sent,
)
.await?
let chat_group_name_timestamp =
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, old_name)
&& chat_id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
.await?
{
info!(context, "Updating grpname for chat {chat_id}.");
context
@@ -2437,10 +2445,18 @@ async fn apply_group_changes(
.await?;
send_event_chat_modified = true;
}
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
better_msg.get_or_insert(
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
);
}
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
}
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,

View File

@@ -1,3 +1,4 @@
use rand::Rng;
use std::time::Duration;
use tokio::fs;
@@ -1894,44 +1895,11 @@ async fn test_save_mime_headers_off() -> anyhow::Result<()> {
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "hi!");
assert!(!msg.get_showpadlock());
let mime = message::get_mime_headers(&bob, msg.id).await?;
assert!(mime.is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_mime_headers_on() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config_bool(Config::SaveMimeHeaders, true).await?;
let bob = TestContext::new_bob().await;
bob.set_config_bool(Config::SaveMimeHeaders, true).await?;
// alice sends a message to bob, bob sees full mime
let chat_alice = alice.create_chat(&bob).await;
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "hi!");
assert!(!msg.get_showpadlock());
let mime = message::get_mime_headers(&bob, msg.id).await?;
let mime_str = String::from_utf8_lossy(&mime);
assert!(mime_str.contains("Message-ID:"));
assert!(mime_str.contains("From:"));
// another one, from bob to alice, that gets encrypted
let chat_bob = bob.create_chat(&alice).await;
chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "ho!");
assert!(msg.get_showpadlock());
let mime = message::get_mime_headers(&alice, msg.id).await?;
let mime_str = String::from_utf8_lossy(&mime);
assert!(mime_str.contains("Message-ID:"));
assert!(mime_str.contains("From:"));
Ok(())
}
async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -4674,7 +4642,14 @@ async fn test_download_later() -> Result<()> {
let bob = tcm.bob().await;
let bob_chat = bob.create_chat(&alice).await;
let text = String::from_utf8(vec![b'a'; MIN_DOWNLOAD_LIMIT as usize])?;
// Generate a random string so OpenPGP does not compress it.
let text: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(MIN_DOWNLOAD_LIMIT as usize)
.map(char::from)
.collect();
let sent_msg = bob.send_text(bob_chat.id, &text).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
@@ -5428,6 +5403,40 @@ Hello!"
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rename_chat_on_missing_message() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_to_chat_contacts_table(
&alice,
time(),
chat_id,
&[Contact::create(&alice, "bob", "bob@example.net").await?],
)
.await?;
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(&bob).await?;
// Bob changes the group name. NB: If Bob does this too fast, it's not guaranteed that his group
// name wins because "Group-Name-Timestamp" may not increase.
SystemTime::shift(Duration::from_secs(3600));
chat::set_chat_name(&bob, bob_chat_id, "Renamed").await?;
bob.pop_sent_msg().await;
// Bob adds a new member.
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
let add_msg = bob.pop_sent_msg().await;
// Alice only receives the member addition.
alice.recv_msg(&add_msg).await;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert_eq!(chat.get_name(), "Renamed");
Ok(())
}
/// Tests that creating a group
/// is preferred over assigning message to existing
/// chat based on `In-Reply-To` and `References`.

View File

@@ -141,7 +141,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
let bob_chat = bob.create_chat(&alice).await;
let bob_chat = bob.get_chat(&alice).await;
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
assert_eq!(
bob_chat.why_cant_send(&bob).await.unwrap(),
@@ -154,7 +154,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg_trash(&sent).await;
let bob_chat = bob.create_chat(&alice).await;
let bob_chat = bob.get_chat(&alice).await;
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
// Check Bob emitted the JoinerProgress event.
@@ -252,7 +252,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert_eq!(contact_bob.is_bot(), false);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
// (check this before calling alice.get_chat() explicitly below)
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
@@ -267,7 +267,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let chat = alice.get_chat(&bob).await;
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
assert!(msg.is_info());
let expected_text = chat_protection_enabled(&alice).await;
@@ -669,11 +669,11 @@ async fn test_secure_join() -> Result<()> {
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
let bobs_chat_with_alice = bob.create_chat(&alice).await;
let bobs_chat_with_alice = bob.get_chat(&alice).await;
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
Ok(())
}
@@ -816,8 +816,13 @@ async fn test_shared_bobs_key() -> Result<()> {
let bob3_addr = "bob3@example.net";
bob3.configure_addr(bob3_addr).await;
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
tcm.send_recv(bob3, alice, "hi Alice!").await;
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
let chat = bob3.create_email_chat(alice).await;
let sent = bob3.send_text(chat.id, "hi Alice!").await;
let msg = alice.recv_msg(&sent).await;
assert!(!msg.get_showpadlock());
let chat = alice.create_email_chat(bob3).await;
let sent = alice.send_text(chat.id, "hi Bob3!").await;
let msg = bob3.recv_msg(&sent).await;
assert!(msg.get_showpadlock());
let mut bob_ids = HashSet::new();

View File

@@ -914,13 +914,17 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
continue;
}
let Ok(stats) = tokio::fs::metadata(entry.path()).await else {
warn!(
context,
"Cannot get metadata for {}.",
entry.path().display()
);
continue;
let stats = match tokio::fs::metadata(entry.path()).await {
Err(err) => {
warn!(
context,
"Cannot get metadata for {}: {:#}.",
entry.path().display(),
err
);
continue;
}
Ok(stats) => stats,
};
if stats.is_dir() {

View File

@@ -97,24 +97,25 @@ impl Summary {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
if msg.is_info() || chat.is_self_talk() {
if msg.is_info() {
None
} else {
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
}
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
if msg.is_info() || contact.is_none() {
None
} else {
msg.get_override_sender_name()
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
.map(SummaryPrefix::Username)
}
}
Chattype::Single => None,
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::Broadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{
if msg.is_info() || contact.is_none() {
None
} else {
msg.get_override_sender_name()
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
.map(SummaryPrefix::Username)
}
} else {
None
};
let mut text = msg.get_summary_text(context).await;

View File

@@ -29,7 +29,7 @@ use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::constants::DC_GCL_NO_SPECIALS;
use crate::constants::{Blocked, Chattype};
use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::contact::{import_vcard, make_vcard, Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::events::{Event, EventEmitter, EventType, Events};
@@ -715,11 +715,26 @@ impl TestContext {
/// Creates or returns an existing 1:1 [`Chat`] with another account.
///
/// This first creates a contact using the configured details on the other account, then
/// creates a 1:1 chat with this contact.
/// This first creates a contact by exporting a vCard from the `other`
/// and importing it into `self`,
/// then creates a 1:1 chat with this contact.
pub async fn create_chat(&self, other: &TestContext) -> Chat {
let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap();
let contact_ids = import_vcard(self, &vcard).await.unwrap();
assert_eq!(contact_ids.len(), 1);
let contact_id = contact_ids.first().unwrap();
let chat_id = ChatId::create_for_contact(self, *contact_id).await.unwrap();
Chat::load_from_db(self, chat_id).await.unwrap()
}
/// Creates or returns an existing 1:1 [`Chat`] with another account
/// by email address.
///
/// This function can be used to create unencrypted chats.
pub async fn create_email_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact(other).await;
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
Chat::load_from_db(self, chat_id).await.unwrap()
}
@@ -743,6 +758,15 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap()
}
pub async fn assert_no_chat(&self, id: ChatId) {
assert!(Chat::load_from_db(self, id).await.is_err());
assert!(!self
.sql
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (id,))
.await
.unwrap());
}
/// Sends out the text message.
///
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call

View File

@@ -213,6 +213,44 @@ mod tests {
// Assert that the \n was correctly removed from the group name also in the system message
assert_eq!(msg.text.contains('\n'), false);
// This doesn't update the name because Date is the same and name is greater.
receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.org\n\
Message-ID: <msg4@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde123456\n\
Chat-Group-Name: another name update 4\n\
Chat-Group-Name-Changed: another name update\n\
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
\n\
4th message\n",
false,
)
.await?;
let chat = Chat::load_from_db(&t, chat.id).await?;
assert_eq!(chat.name, "another name update");
// This updates the name because Date is the same and name is lower.
receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.org\n\
Message-ID: <msg5@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde123456\n\
Chat-Group-Name: another name updat\n\
Chat-Group-Name-Changed: another name update\n\
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
\n\
5th message\n",
false,
)
.await?;
let chat = Chat::load_from_db(&t, chat.id).await?;
assert_eq!(chat.name, "another name updat");
Ok(())
}
}

View File

@@ -726,7 +726,7 @@ async fn test_send_webxdc_status_update() -> Result<()> {
let bob = TestContext::new_bob().await;
// Alice sends an webxdc instance and a status update
let alice_chat = alice.create_chat(&bob).await;
let alice_chat = alice.create_email_chat(&bob).await;
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
let sent1 = &alice.pop_sent_msg().await;
assert_eq!(alice_instance.viewtype, Viewtype::Webxdc);
@@ -1022,7 +1022,7 @@ async fn test_pop_status_update() -> Result<()> {
async fn test_draft_and_send_webxdc_status_update() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = alice.create_chat(&bob).await.id;
let alice_chat_id = alice.create_email_chat(&bob).await.id;
// prepare webxdc instance,
// status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None)

View File

@@ -0,0 +1,3 @@
Content-Type: multipart/mixed; boundary="foobar"
--foobar--