Compare commits

...

28 Commits

Author SHA1 Message Date
link2xt
5820c4ce95 Serialize mime_compressed 2024-04-06 16:37:51 +00:00
link2xt
12ba33d9d4 Serialize uid 2024-04-06 15:54:12 +00:00
link2xt
60a7bbc9b5 Do not serialize is_default
It is only stored for compatibility with old versions
2024-04-06 15:27:11 +00:00
link2xt
e9f434b562 Serialize backward_verified_key_id 2024-04-06 02:48:27 +00:00
link2xt
2423cb8175 feat: add support for dumping the database to stream 2024-04-06 01:45:14 +00:00
link2xt
65c9e72bf4 test: test withdrawing group join QR codes 2024-04-05 22:34:02 +00:00
link2xt
ea4d954c77 fix: do not emit MSGS_CHANGED event for outgoing hidden messages
This includes synchronization messages.
2024-04-05 22:34:02 +00:00
link2xt
43523a96a2 api(deltachat-rpc-client): add check_qr and set_config_from_qr APIs 2024-04-05 22:34:02 +00:00
link2xt
2e2fa9e74f chore: update lockfile in /fuzz 2024-04-05 19:44:51 +00:00
B. Petersen
e43ffb20a1 chore(release): prepare for 1.137.2 2024-04-05 14:21:15 +00:00
link2xt
2f0f247e70 refactor: use Rust 1.77.0 support for recursion in async functions 2024-04-04 17:01:15 +00:00
Simon Laux
5bda4f0c26 update node constants for #5387 (#5429) 2024-04-04 15:31:02 +02:00
iequidoo
d39c8a3a19 refactor: is_probably_private_reply: Remove reaction-specific code
Instead, look up the 1:1 chat in `receive_imf::add_parts()`. This is a more generic approach to fix
assigning outgoing reactions to 1:1 chats in the multi-device setup. Although currently both
approaches give the same result, this way we can even implement a "react privately"
functionality. Maybe it sounds useless, but it seems better to have less reaction-specific code.
2024-04-03 21:29:27 -03:00
link2xt
e465415039 fix: do not ignore Message::load_from_db errors 2024-04-04 01:44:50 +02:00
B. Petersen
5cef77b8e6 fix: do not show empty summary if message reacted to is deleted
we checked for tombstones already using `is_trash()`,
however, we've overseen that tombstones get deleted at some point :)

therefore, just do not treat loading failures of the weak msg_id as errors -
usually, they are not - and if, just the normal summary is shown.
in theory, we could check for existance explicitly before tryong load_from_db,
however, that would be additional code (and maybe another database call)
and not worth the effort.

anyways, this commit also adds an explicit test
for physical deletion after housekeeping.
2024-04-04 01:44:50 +02:00
dependabot[bot]
60e733c30c chore(cargo): bump fast-socks5 from 0.9.5 to 0.9.6
Bumps [fast-socks5](https://github.com/dizda/fast-socks5) from 0.9.5 to 0.9.6.
- [Release notes](https://github.com/dizda/fast-socks5/releases)
- [Commits](https://github.com/dizda/fast-socks5/commits/v0.9.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 21:51:57 +00:00
dependabot[bot]
8b98816eb9 Merge pull request #5404 from deltachat/dependabot/cargo/rustyline-14.0.0 2024-04-03 21:19:38 +00:00
link2xt
50165b3e35 chore: upgrade image from 0.24.9 to 0.25.1 2024-04-03 20:59:06 +00:00
link2xt
0be8b5a5c4 chore: upgrade h2 from 0.4.3 to 0.4.4
This upgrade contains a fix for RUSTSEC-2024-0332
2024-04-03 20:34:14 +00:00
B. Petersen
451bb6e9db add tests for get_summary_text_without_prefix() 2024-04-03 20:55:22 +02:00
B. Petersen
83196d4cb5 add get_summary_text_without_prefix()
use get_summary_text_without_prefix() to get raw summaries without prefixes,
this is needed for reaction summaries,
where we also do not show the name, so we do not want to show "Forwarded" as well.
2024-04-03 20:55:22 +02:00
B. Petersen
0003e55ad5 test reactions for forwarded messages 2024-04-03 20:55:22 +02:00
link2xt
02014eda6c chore: update from brotli 3.4.0 to brotli 4.0.0 2024-04-03 17:25:28 +00:00
link2xt
f1c6cd69e9 chore: update chrono from 0.4.34 to 0.4.37 2024-04-03 16:49:19 +00:00
bjoern
ace281ff6c feat: show reactions in summaries (#5387)
shows the last reaction in chatlist's summaries if there is no
newer message.

the reason to show reactions in the summary, is to make them a _little_
more visible when one is not in the chat. esp. in not-so-chatty or in
one-to-ones chats this becomes handy: imaging a question and someone
"answers" with "thumbs up" ... 

otoh, reactions are still tuned down on purpose: no notifications, chats
are opend as usual, the chatlist is not sorted by reactions and also the
date in the summary refer to the last message - i thought quite a bit
about that, this seems to be good compromise and will raise the fewest
questions. it is somehow clear to the users that reactions are not the
same as a real message. also, it is comparable easy to implement - no
UI changes required :)

all that is very close to what whatsapp is doing (figured that out by
quite some testing ... to cite @adbenitez: if in doubt, we can blame
whatsapp :)

technically, i first wanted to go for the "big solution" and add two
more columns, chat_id and timestamp, however, it seemed a bit bloated if
we really only need the last one. therefore, i just added the last
reaction information to the chat's param, which seems more performant
but also easier to code :)
2024-04-03 08:50:05 +00:00
dependabot[bot]
c9edd525e0 chore(cargo): bump rustyline from 13.0.0 to 14.0.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 13.0.0 to 14.0.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v13.0.0...v14.0.0)

---
updated-dependencies:
- dependency-name: rustyline
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 06:12:01 +00:00
link2xt
3f35b442c3 chore(release): prepare for 1.137.1 2024-04-03 01:28:13 +00:00
link2xt
87e9365016 ci: remove android builds for x86 and x86_64
They are failing to build.
2024-04-03 01:17:21 +00:00
42 changed files with 3193 additions and 380 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.77.0
RUSTUP_TOOLCHAIN: 1.77.1
steps:
- uses: actions/checkout@v4
with:
@@ -83,15 +83,15 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.77.0
rust: 1.77.1
- os: windows-latest
rust: 1.77.0
rust: 1.77.1
- os: macos-latest
rust: 1.77.0
rust: 1.77.1
# Minimum Supported Rust Version = 1.70.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
rust: 1.70.0
rust: 1.77.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

View File

@@ -99,7 +99,7 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: [arm64-v8a, armeabi-v7a, x86, x86_64]
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -201,18 +201,6 @@ jobs:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Download Android binary for x86
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86-android
path: deltachat-rpc-server-x86-android.d
- name: Download Android binary for x86_64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-android
path: deltachat-rpc-server-x86_64-android.d
- name: Create bin/ directory
run: |
mkdir -p bin
@@ -227,8 +215,6 @@ jobs:
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
mv deltachat-rpc-server-x86-android.d/deltachat-rpc-server bin/deltachat-rpc-server-x86-android
mv deltachat-rpc-server-x86_64-android.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-android
- name: List binaries
run: ls -l bin/

View File

@@ -1,5 +1,39 @@
# Changelog
## [1.137.2] - 2024-04-05
### API-Changes
- [**breaking**] Increase Minimum Supported Rust Version to 1.77.0.
### Features / Changes
- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)).
### Tests
- Test reactions for forwarded messages
### Refactor
- `is_probably_private_reply`: Remove reaction-specific code.
- Use Rust 1.77.0 support for recursion in async functions.
### Miscellaneous Tasks
- cargo: Bump rustyline from 13.0.0 to 14.0.0.
- Update chrono from 0.4.34 to 0.4.37.
- Update from brotli 3.4.0 to brotli 4.0.0.
- Upgrade `h2` from 0.4.3 to 0.4.4.
- Upgrade `image` from 0.24.9 to 0.25.1.
- cargo: Bump fast-socks5 from 0.9.5 to 0.9.6.
## [1.137.1] - 2024-04-03
### CI
- Remove android builds for `x86` and `x86_64`.
## [1.137.0] - 2024-04-02
### API-Changes
@@ -3836,3 +3870,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2

88
Cargo.lock generated
View File

@@ -507,9 +507,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "3.4.0"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -518,9 +518,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "2.5.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -646,6 +646,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "charset"
version = "0.1.3"
@@ -658,9 +664,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.34"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -1085,7 +1091,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.137.0"
version = "1.137.2"
dependencies = [
"ansi_term",
"anyhow",
@@ -1096,7 +1102,9 @@ dependencies = [
"async_zip",
"backtrace",
"base64 0.21.7",
"bitflags 1.3.2",
"brotli",
"bstr",
"chrono",
"criterion",
"deltachat-time",
@@ -1166,7 +1174,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.137.0"
version = "1.137.2"
dependencies = [
"anyhow",
"async-channel 2.2.0",
@@ -1190,7 +1198,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.137.0"
version = "1.137.2"
dependencies = [
"ansi_term",
"anyhow",
@@ -1205,7 +1213,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.137.0"
version = "1.137.2"
dependencies = [
"anyhow",
"deltachat",
@@ -1234,7 +1242,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.137.0"
version = "1.137.2"
dependencies = [
"anyhow",
"deltachat",
@@ -1848,9 +1856,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fast-socks5"
version = "0.9.5"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbcc731f3c17a5053e07e6a2290918da75cd8b9b1217b419721f715674ac520c"
checksum = "f89f36d4ee12370d30d57b16c7e190950a1a916e7dbbb5fd5a412f5ef913fe84"
dependencies = [
"anyhow",
"async-trait",
@@ -2166,9 +2174,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
dependencies = [
"bytes",
"fnv",
@@ -2505,17 +2513,29 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.9"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"jpeg-decoder",
"image-webp",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c"
dependencies = [
"byteorder",
"thiserror",
]
[[package]]
@@ -2640,12 +2660,6 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]]
name = "js-sys"
version = "0.3.69"
@@ -2972,12 +2986,13 @@ dependencies = [
[[package]]
name = "nix"
version = "0.27.1"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"cfg_aliases",
"libc",
]
@@ -4225,9 +4240,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
version = "13.0.0"
version = "14.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86"
checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@@ -4242,7 +4257,7 @@ dependencies = [
"unicode-segmentation",
"unicode-width",
"utf8parse",
"winapi",
"windows-sys 0.52.0",
]
[[package]]
@@ -5900,3 +5915,18 @@ dependencies = [
"quote",
"syn 2.0.57",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
dependencies = [
"zune-core",
]

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.137.0"
version = "1.137.2"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
rust-version = "1.77"
repository = "https://github.com/deltachat/deltachat-core-rust"
[profile.dev]
@@ -45,7 +45,9 @@ async-smtp = { version = "0.9", default-features = false, features = ["runtime-t
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
brotli = { version = "3.4", default-features=false, features = ["std"] }
brotli = { version = "4", default-features=false, features = ["std"] }
bitflags = "1.3"
bstr = { version = "1.4.0", default-features=false, features = ["std", "alloc"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
@@ -57,7 +59,7 @@ futures-lite = "2.3.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.24.9", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }

View File

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

View File

@@ -7296,6 +7296,22 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the provider's domain.
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
/// "You reacted %1$s to '%2$s'"
///
/// `%1$s` will be replaced by the reaction, usually an emoji
/// `%2$s` will be replaced by the summary of the message the reaction refers to
///
/// Used in summaries.
#define DC_STR_YOU_REACTED 176
/// "%1$s reacted %2$s to '%3$s'"
///
/// `%1$s` will be replaced by the name the contact who reacted
/// `%2$s` will be replaced by the reaction, usually an emoji
/// `%3$s` will be replaced by the summary of the message the reaction refers to
///
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/**
* @}

View File

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

View File

@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.137.0"
"version": "1.137.2"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.137.0"
version = "1.137.2"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
@@ -13,7 +13,7 @@ dirs = "5"
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.31"
rustyline = "13"
rustyline = "14"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]

View File

@@ -339,6 +339,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
export-keys\n\
import-keys\n\
export-setup\n\
dump <filename>\n\n
read <filename>\n\n
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
@@ -514,6 +516,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
&setup_code,
);
}
"dump" => {
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
serialize_database(&context, arg1).await?;
}
"read" => {
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
deserialize_database(&context, arg1).await?;
}
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.137.0"
version = "1.137.2"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -76,6 +76,12 @@ class Account:
"""Get self avatar."""
return self.get_config("selfavatar")
def check_qr(self, qr):
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
def configure(self):
"""Configure an account."""

View File

@@ -1,7 +1,7 @@
import logging
import pytest
from deltachat_rpc_client import Chat, SpecialContactId
from deltachat_rpc_client import Chat, EventType, SpecialContactId
def test_qr_setup_contact(acfactory, tmp_path) -> None:
@@ -579,3 +579,40 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
logging.info("Alice withdraws QR code.")
qr = alice.check_qr(qr_code)
assert qr["kind"] == "withdrawVerifyGroup"
alice.set_config_from_qr(qr_code)
logging.info("Bob scans withdrawn QR code.")
bob_chat = bob.secure_join(qr_code)
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))

View File

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

View File

@@ -263,14 +263,6 @@
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android19-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:

201
fuzz/Cargo.lock generated
View File

@@ -339,6 +339,12 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "base64ct"
version = "1.5.3"
@@ -515,9 +521,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "3.4.0"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -526,9 +532,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "2.5.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -953,7 +959,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.136.0"
version = "1.137.2"
dependencies = [
"anyhow",
"async-channel 2.1.1",
@@ -989,6 +995,7 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"openssl-src",
"parking_lot",
"percent-encoding",
"pgp",
@@ -1785,9 +1792,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand 2.0.1",
"futures-core",
@@ -1912,9 +1919,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
dependencies = [
"bytes",
"fnv",
@@ -2051,9 +2058,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@@ -2062,12 +2069,24 @@ dependencies = [
[[package]]
name = "http-body"
version = "0.4.5"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
@@ -2077,12 +2096,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humansize"
version = "2.1.2"
@@ -2094,39 +2107,58 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.23"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.4.7",
"smallvec",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2 0.5.4",
"tokio",
"tower",
"tower-service",
"tracing",
]
[[package]]
@@ -2180,17 +2212,29 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.9"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"jpeg-decoder",
"image-webp",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c"
dependencies = [
"byteorder",
"thiserror",
]
[[package]]
@@ -2314,12 +2358,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -3480,11 +3518,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
version = "0.11.24"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19"
dependencies = [
"base64 0.21.0",
"base64 0.22.0",
"bytes",
"encoding_rs",
"futures-core",
@@ -3492,8 +3530,10 @@ dependencies = [
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
@@ -3502,7 +3542,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"rustls-pemfile 2.1.1",
"serde",
"serde_json",
"serde_urlencoded",
@@ -3515,7 +3555,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
"winreg 0.52.0",
]
[[package]]
@@ -3724,7 +3764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pemfile 1.0.2",
"schannel",
"security-framework",
]
@@ -3738,6 +3778,22 @@ dependencies = [
"base64 0.21.0",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
dependencies = [
"base64 0.21.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@@ -4055,9 +4111,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.10.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smawk"
@@ -4294,22 +4350,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.38"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.107",
"syn 2.0.52",
]
[[package]]
@@ -4442,9 +4498,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -4514,6 +4570,28 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -5169,9 +5247,9 @@ dependencies = [
[[package]]
name = "winreg"
version = "0.50.0"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
@@ -5265,3 +5343,18 @@ dependencies = [
"syn 1.0.107",
"synstructure",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
dependencies = [
"zune-core",
]

View File

@@ -257,6 +257,7 @@ module.exports = {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_REACTED_BY: 177,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
@@ -284,6 +285,7 @@ module.exports = {
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
DC_STR_VOICEMESSAGE: 7,
DC_STR_WELCOME_MESSAGE: 71,
DC_STR_YOU_REACTED: 176,
DC_TEXT1_DRAFT: 1,
DC_TEXT1_SELF: 3,
DC_TEXT1_USERNAME: 2,

View File

@@ -257,6 +257,7 @@ export enum C {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_REACTED_BY = 177,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
@@ -284,6 +285,7 @@ export enum C {
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_STR_YOU_REACTED = 176,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,

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.137.0"
"version": "1.137.2"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.137.0"
version = "1.137.2"
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-04-02
2024-04-05

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.77.0
RUST_VERSION=1.77.1
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -11,9 +11,8 @@ use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::{
DynamicImage, GenericImage, GenericImageView, ImageFormat, ImageOutputFormat, Pixel, Rgba,
};
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
@@ -37,6 +36,12 @@ pub struct BlobObject<'a> {
name: String,
}
#[derive(Debug, Clone)]
enum ImageOutputFormat {
Png,
Jpeg { quality: u8 },
}
impl<'a> BlobObject<'a> {
/// Creates a new blob object with a unique name.
///
@@ -457,9 +462,13 @@ impl<'a> BlobObject<'a> {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
Ok(ImageFormat::Jpeg) => {
add_white_bg = false;
ImageOutputFormat::Jpeg(jpeg_quality)
ImageOutputFormat::Jpeg {
quality: jpeg_quality,
}
}
_ => ImageOutputFormat::Jpeg(jpeg_quality),
_ => ImageOutputFormat::Jpeg {
quality: jpeg_quality,
},
};
// We need to rewrite images with Exif to remove metadata such as location,
// camera model, etc.
@@ -530,7 +539,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))
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
{
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No image file name (???)")?;
@@ -685,7 +694,13 @@ fn encode_img(
) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, fmt)?;
match fmt {
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
ImageOutputFormat::Jpeg { quality } => {
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
img.write_with_encoder(encoder)?;
}
}
Ok(())
}

View File

@@ -2698,7 +2698,9 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
}
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
context.emit_msgs_changed(msg.chat_id, msg.id);
if !msg.hidden {
context.emit_msgs_changed(msg.chat_id, msg.id);
}
if msg.param.exists(Param::SetLatitude) {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));

View File

@@ -416,7 +416,7 @@ impl Chatlist {
if chat.id.is_archived_link() {
Ok(Default::default())
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await
} else {
Ok(Summary {
text: stock_str::no_messages(context).await,

View File

@@ -7,12 +7,8 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use std::future::Future;
use std::pin::Pin;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use futures::future::FutureExt;
use lettre_email::mime::Mime;
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
@@ -116,119 +112,109 @@ impl HtmlMsgParser {
/// Usually, there is at most one plain-text and one HTML-text part,
/// multiple plain-text parts might be used for mailinglist-footers,
/// therefore we use the first one.
fn collect_texts_recursive<'a>(
async fn collect_texts_recursive<'a>(
&'a mut self,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
self.collect_texts_recursive(cur_data).await?
}
Ok(())
) -> Result<()> {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.collect_texts_recursive(cur_data)).await?
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.collect_texts_recursive(&mail).await
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
Box::pin(self.collect_texts_recursive(&mail)).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
});
self.html = decoded_data;
}
}
Ok(())
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
});
}
}
Ok(())
}
}
.boxed()
}
/// Replace cid:-protocol by the data:-protocol where appropriate.
/// This allows the final html-file to be self-contained.
fn cid_to_data_recursive<'a>(
async fn cid_to_data_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
self.cid_to_data_recursive(context, cur_data).await?;
}
Ok(())
) -> Result<()> {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.cid_to_data_recursive(context, cur_data)).await?;
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.cid_to_data_recursive(context, &mail).await
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail) {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}",
re_string,
e
),
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
Box::pin(self.cid_to_data_recursive(context, &mail)).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail) {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}", re_string, e
),
}
}
}
}
Ok(())
}
Ok(())
}
}
.boxed()
}
}

View File

@@ -10,6 +10,7 @@ use futures::StreamExt;
use futures_lite::FutureExt;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
use tokio::io::BufWriter;
use tokio_tar::Archive;
use crate::blob::{BlobDirContents, BlobObject};
@@ -499,7 +500,7 @@ fn get_next_backup_path(
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
.context("can't get next backup path")?
// Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
@@ -816,6 +817,20 @@ async fn export_database(
.await
}
/// Serializes the database to a file.
pub async fn serialize_database(context: &Context, filename: &str) -> Result<()> {
let file = File::create(filename).await?;
context.sql.serialize(BufWriter::new(file)).await?;
Ok(())
}
/// Deserializes the database from a file.
pub async fn deserialize_database(context: &Context, filename: &str) -> Result<()> {
let file = File::open(filename).await?;
context.sql.deserialize(file).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::time::Duration;

View File

@@ -137,7 +137,7 @@ impl Kml {
// 0 4 7 10 13 16 19
match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
Ok(res) => {
self.curr.timestamp = res.timestamp();
self.curr.timestamp = res.and_utc().timestamp();
let now = time();
if self.curr.timestamp > now {
self.curr.timestamp = now;
@@ -540,7 +540,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(Strin
fn get_kml_timestamp(utc: i64) -> String {
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
chrono::NaiveDateTime::from_timestamp_opt(utc, 0)
chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
.unwrap()
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string()

View File

@@ -459,7 +459,19 @@ impl Message {
}
/// Loads message with given ID from the database.
///
/// Returns an error if the message does not exist.
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
let message = Self::load_from_db_optional(context, id)
.await?
.context("Message {id} does not exist")?;
Ok(message)
}
/// Loads message with given ID from the database.
///
/// Returns `None` if the message does not exist.
pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
ensure!(
!id.is_special(),
"Can not load special message ID {} from DB",
@@ -467,7 +479,7 @@ impl Message {
);
let msg = context
.sql
.query_row(
.query_row_optional(
concat!(
"SELECT",
" m.id AS id,",
@@ -796,7 +808,7 @@ impl Message {
None
};
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
Summary::new(context, self, chat, contact.as_ref()).await
}
// It's a little unfortunate that the UI has to first call `dc_msg_get_override_sender_name` and then if it was `NULL`, call

View File

@@ -575,11 +575,7 @@ impl<'a> MimeFactory<'a> {
.protected
.push(Header::new("Subject".into(), encoded_subject));
let date = chrono::Utc
.from_local_datetime(
&chrono::NaiveDateTime::from_timestamp_opt(self.timestamp, 0)
.context("can't convert timestamp to NativeDateTime")?,
)
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp, 0)
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));

View File

@@ -2,9 +2,7 @@
use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::str;
use anyhow::{bail, Context as _, Result};
@@ -818,59 +816,53 @@ impl MimeMessage {
self.headers.get(headerdef.get_headername())
}
fn parse_mime_recursive<'a>(
async fn parse_mime_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
is_related: bool,
) -> Pin<Box<dyn Future<Output = Result<bool>> + 'a + Send>> {
use futures::future::FutureExt;
) -> Result<bool> {
enum MimeS {
Multiple,
Single,
Message,
}
// Boxed future to deal with recursion
async move {
enum MimeS {
Multiple,
Single,
Message,
}
let mimetype = mail.ctype.mimetype.to_lowercase();
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.contains_key("boundary") {
MimeS::Multiple
} else {
MimeS::Single
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
MimeS::Message
} else {
MimeS::Single
}
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.contains_key("boundary") {
MimeS::Multiple
} else {
MimeS::Single
};
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
MimeS::Message
} else {
MimeS::Single
}
} else {
MimeS::Single
};
let is_related = is_related || mimetype == "multipart/related";
match m {
MimeS::Multiple => self.handle_multiple(context, mail, is_related).await,
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false);
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
let is_related = is_related || mimetype == "multipart/related";
match m {
MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false);
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.parse_mime_recursive(context, &mail, is_related).await
}
MimeS::Single => {
self.add_single_part_if_known(context, mail, is_related)
.await
}
Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
}
MimeS::Single => {
self.add_single_part_if_known(context, mail, is_related)
.await
}
}
.boxed()
}
async fn handle_multiple(

View File

@@ -64,6 +64,15 @@ pub enum Param {
/// For Messages: the message is a reaction.
Reaction = b'x',
/// For Chats: the timestamp of the last reaction.
LastReactionTimestamp = b'y',
/// For Chats: Message ID of the last reaction.
LastReactionMsgId = b'Y',
/// For Chats: Contact ID of the last reaction.
LastReactionContactId = b'1',
/// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
Bot = b'b',

View File

@@ -20,11 +20,12 @@ use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, ChatId};
use crate::chat::{send_msg, Chat, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
use crate::param::Param;
/// A single reaction consisting of multiple emoji sequences.
///
@@ -170,6 +171,7 @@ async fn set_msg_id_reaction(
msg_id: MsgId,
chat_id: ChatId,
contact_id: ContactId,
timestamp: i64,
reaction: Reaction,
) -> Result<()> {
if reaction.is_empty() {
@@ -194,6 +196,17 @@ async fn set_msg_id_reaction(
(msg_id, contact_id, reaction.as_str()),
)
.await?;
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat
.param
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
{
chat.param
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
chat.param
.set_i64(Param::LastReactionContactId, i64::from(contact_id.to_u32()));
chat.update_param(context).await?;
}
}
context.emit_event(EventType::ReactionsChanged {
@@ -223,7 +236,15 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
// Only set reaction if we successfully sent the message.
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
set_msg_id_reaction(
context,
msg_id,
msg.chat_id,
ContactId::SELF,
reaction_msg.timestamp_sort,
reaction,
)
.await?;
Ok(reaction_msg_id)
}
@@ -250,10 +271,11 @@ pub(crate) async fn set_msg_reaction(
in_reply_to: &str,
chat_id: ChatId,
contact_id: ContactId,
timestamp: i64,
reaction: Reaction,
) -> Result<()> {
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await
} else {
info!(
context,
@@ -307,18 +329,72 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
Ok(Reactions { reactions })
}
impl Chat {
/// Check if there is a reaction newer than the given timestamp.
///
/// If so, reaction details are returned and can be used to create a summary string.
pub async fn get_last_reaction_if_newer_than(
&self,
context: &Context,
timestamp: i64,
) -> Result<Option<(Message, ContactId, String)>> {
if let Some(reaction_timestamp) = self.param.get_i64(Param::LastReactionTimestamp) {
if reaction_timestamp > timestamp {
let reaction_msg_id = MsgId::new(
self.param
.get_int(Param::LastReactionMsgId)
.unwrap_or_default() as u32,
);
// The message reacted to may be deleted physically (`load_from_db()` fails) or marked as a tombstone (`is_trash()`).
// These are no errors as `Param::LastReaction*` are just weak pointers.
// Instead, just return `Ok(None)` and let the caller create another summary.
if let Some(reaction_msg) =
Message::load_from_db_optional(context, reaction_msg_id).await?
{
if !reaction_msg.chat_id.is_trash() {
let reaction_contact_id = ContactId::new(
self.param
.get_int(Param::LastReactionContactId)
.unwrap_or_default() as u32,
);
if let Some(reaction) = context
.sql
.query_row_optional(
r#"SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?"#,
(reaction_msg.id, reaction_contact_id),
|row| {
let reaction: String = row.get(0)?;
Ok(reaction)
},
)
.await?
{
return Ok(Some((reaction_msg, reaction_contact_id, reaction)));
}
}
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{get_chat_msgs, send_text_msg};
use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, ContactAddress, Origin};
use crate::download::DownloadState;
use crate::message::MessageState;
use crate::message::{delete_msgs, MessageState};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::sql::housekeeping;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools::SystemTime;
use std::time::Duration;
#[test]
fn test_parse_reaction() {
@@ -549,6 +625,146 @@ Here's my footer -- bob@example.net"
Ok(())
}
async fn assert_summary(t: &TestContext, expected: &str) {
let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
let summary = chatlist.get_summary(t, 0, None).await.unwrap();
assert_eq!(summary.text, expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_summary() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice.set_config(Config::Displayname, Some("ALICE")).await?;
bob.set_config(Config::Displayname, Some("BOB")).await?;
// Alice sends message to Bob
let alice_chat = alice.create_chat(&bob).await;
let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
let bob_msg1 = bob.recv_msg(&alice_msg1).await;
// Bob reacts to Alice's message, this is shown in the summaries
SystemTime::shift(Duration::from_secs(10));
bob_msg1.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg1.id, "👍").await?;
let bob_send_reaction = bob.pop_sent_msg().await;
let alice_rcvd_reaction = alice.recv_msg(&bob_send_reaction).await;
assert!(alice_rcvd_reaction.get_timestamp() > bob_msg1.get_timestamp());
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
assert_eq!(summary.text, "You reacted 👍 to \"Party?\"");
assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
assert!(summary.prefix.is_none());
assert!(summary.thumbnail_path.is_none());
assert_summary(&alice, "BOB reacted 👍 to \"Party?\"").await;
// Alice reacts to own message as well
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
let alice_send_reaction = alice.pop_sent_msg().await;
bob.recv_msg(&alice_send_reaction).await;
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
// Alice sends a newer message, this overwrites reaction summaries
SystemTime::shift(Duration::from_secs(10));
let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
bob.recv_msg(&alice_msg2).await;
assert_summary(&alice, "kewl").await;
assert_summary(&bob, "kewl").await;
// Reactions to older messages still overwrite newer messages
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🤘").await?;
let alice_send_reaction = alice.pop_sent_msg().await;
bob.recv_msg(&alice_send_reaction).await;
assert_summary(&alice, "You reacted 🤘 to \"Party?\"").await;
assert_summary(&bob, "ALICE reacted 🤘 to \"Party?\"").await;
// Retracted reactions remove all summary reactions
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
let alice_remove_reaction = alice.pop_sent_msg().await;
bob.recv_msg(&alice_remove_reaction).await;
assert_summary(&alice, "kewl").await;
assert_summary(&bob, "kewl").await;
// Alice adds another reaction and then deletes the message reacted to; this will also delete reaction summary
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🧹").await?;
assert_summary(&alice, "You reacted 🧹 to \"Party?\"").await;
delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; // this will leave a tombstone
assert_summary(&alice, "kewl").await;
housekeeping(&alice).await?; // this will delete the tombstone
assert_summary(&alice, "kewl").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_forwarded_summary() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice adds a message to "Saved Messages"
let self_chat = alice.get_self_chat().await;
let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
assert_summary(&alice, "foo").await;
// Alice reacts to that message
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, msg_id, "🐫").await?;
assert_summary(&alice, "You reacted 🐫 to \"foo\"").await;
let reactions = get_msg_reactions(&alice, msg_id).await?;
assert_eq!(reactions.reactions.len(), 1);
// Alice forwards that message to Bob: Reactions are not forwarded, the message is prefixed by "Forwarded".
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
assert_summary(&alice, "Forwarded: foo").await; // forwarded messages are prefixed
let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
assert!(reactions.reactions.is_empty()); // reactions are not forwarded
// Alice reacts to forwarded message:
// For reaction summary neither original message author nor "Forwarded" prefix is shown
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, forwarded_msg_id, "🐳").await?;
assert_summary(&alice, "You reacted 🐳 to \"foo\"").await;
let reactions = get_msg_reactions(&alice, msg_id).await?;
assert_eq!(reactions.reactions.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
let alice0 = TestContext::new_alice().await;
let alice1 = TestContext::new_alice().await;
let chat = alice0.get_self_chat().await;
let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
alice1.recv_msg(&alice0.pop_sent_msg().await).await;
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice0, msg_id, "👆").await?;
let sync = alice0.pop_sent_msg().await;
receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
assert_summary(&alice0, "You reacted 👆 to \"mom's birthday!\"").await;
assert_summary(&alice1, "You reacted 👆 to \"mom's birthday!\"").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -665,8 +881,7 @@ Here's my footer -- bob@example.net"
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
send_reaction(&alice0, alice0_msg_id, "👀").await?;
let sync = alice0.pop_sent_msg().await;
receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
alice1.recv_msg(&alice0.pop_sent_msg().await).await;
expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)

View File

@@ -1077,6 +1077,12 @@ async fn add_parts(
chat_id_blocked = chat.blocked;
}
}
if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
// automatically unblock chat when the user sends a message
if chat_id_blocked != Blocked::Not {
@@ -1374,6 +1380,7 @@ async fn add_parts(
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
sort_timestamp,
Reaction::from(reaction_str.as_str()),
)
.await?;
@@ -1721,11 +1728,6 @@ async fn is_probably_private_reply(
}
}
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
if is_reaction {
return Ok(false);
}
Ok(true)
}

View File

@@ -9,7 +9,7 @@ use crate::chat::{
ChatVisibility,
};
use crate::chatlist::Chatlist;
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::constants::{ShowEmails, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::imap::prefetch_should_download;
use crate::imex::{imex, ImexMode};
@@ -142,6 +142,35 @@ async fn test_adhoc_group_show_accepted_contact_unknown() {
assert_eq!(chats.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config(
Config::ShowEmails,
Some(&ShowEmails::AcceptedContacts.to_string()),
)
.await?;
tcm.send_recv(alice, bob, "hi").await;
receive_imf(
bob,
b"From: bob@example.net\n\
To: alice@example.org, claire@example.com\n\
Message-ID: <3333@example.net>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
false,
)
.await?;
let chats = Chatlist::try_load(bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0)?;
assert_eq!(chat_id.get_msg_cnt(bob).await?, 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_adhoc_group_show_accepted_contact_known() {
let t = TestContext::new_alice().await;

View File

@@ -46,10 +46,12 @@ pub(crate) fn params_iter(
iter.iter().map(|item| item as &dyn crate::sql::ToSql)
}
mod deserialize;
mod migrations;
mod pool;
mod serialize;
use pool::Pool;
use pool::{Pool, PooledConnection};
/// A wrapper around the underlying Sqlite3 object.
#[derive(Debug)]
@@ -363,6 +365,12 @@ impl Sql {
self.write_mtx.lock().await
}
pub(crate) async fn get_connection(&self) -> Result<PooledConnection> {
let lock = self.pool.read().await;
let pool = lock.as_ref().context("no SQL connection")?;
pool.get().await
}
/// Allocates a connection and calls `function` with the connection. If `function` does write
/// queries,
/// - either first take a lock using `write_lock()`
@@ -374,9 +382,7 @@ impl Sql {
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
R: Send + 'static,
{
let lock = self.pool.read().await;
let pool = lock.as_ref().context("no SQL connection")?;
let mut conn = pool.get().await?;
let mut conn = self.get_connection().await?;
let res = tokio::task::block_in_place(move || function(&mut conn))?;
Ok(res)
}

1349
src/sql/deserialize.rs Normal file

File diff suppressed because it is too large Load Diff

963
src/sql/serialize.rs Normal file
View File

@@ -0,0 +1,963 @@
//! Database serialization module.
//!
//! The module contains functions to serialize database into a stream.
//!
//! Output format is based on [bencoding](http://bittorrent.org/beps/bep_0003.html).
/// Database version supported by the current serialization code.
///
/// Serialization code MUST be updated before increasing this number.
///
/// If this version is below the actual database version,
/// serialization code is outdated.
/// If this version is above the actual database version,
/// migrations have to be run first to update the database.
const SERIALIZE_DBVERSION: &str = "99";
use anyhow::{anyhow, Context as _, Result};
use rusqlite::types::ValueRef;
use rusqlite::Transaction;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use super::Sql;
struct Encoder<'a, W: AsyncWrite + Unpin> {
tx: Transaction<'a>,
w: W,
}
async fn write_bytes(w: &mut (impl AsyncWrite + Unpin), b: &[u8]) -> Result<()> {
let bytes_len = format!("{}:", b.len());
w.write_all(bytes_len.as_bytes()).await?;
w.write_all(b).await?;
Ok(())
}
async fn write_str(w: &mut (impl AsyncWrite + Unpin), s: &str) -> Result<()> {
write_bytes(w, s.as_bytes()).await?;
Ok(())
}
async fn write_i64(w: &mut (impl AsyncWrite + Unpin), i: i64) -> Result<()> {
let s = format!("{i}");
w.write_all(b"i").await?;
w.write_all(s.as_bytes()).await?;
w.write_all(b"e").await?;
Ok(())
}
async fn write_u32(w: &mut (impl AsyncWrite + Unpin), i: u32) -> Result<()> {
let s = format!("{i}");
w.write_all(b"i").await?;
w.write_all(s.as_bytes()).await?;
w.write_all(b"e").await?;
Ok(())
}
async fn write_f64(w: &mut (impl AsyncWrite + Unpin), f: f64) -> Result<()> {
write_bytes(w, &f.to_be_bytes()).await?;
Ok(())
}
async fn write_bool(w: &mut (impl AsyncWrite + Unpin), b: bool) -> Result<()> {
if b {
w.write_all(b"i1e").await?;
} else {
w.write_all(b"i0e").await?;
}
Ok(())
}
impl<'a, W: AsyncWrite + Unpin> Encoder<'a, W> {
fn new(tx: Transaction<'a>, w: W) -> Self {
Self { tx, w }
}
/// Serializes `config` table.
async fn serialize_config(&mut self) -> Result<()> {
// FIXME: sort the dictionary in lexicographical order
// dbversion should be the first, so store it as "_config._dbversion"
let mut stmt = self.tx.prepare("SELECT keyname,value FROM config")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"d").await?;
while let Some(row) = rows.next()? {
let keyname: String = row.get(0)?;
let value: String = row.get(1)?;
write_str(&mut self.w, &keyname).await?;
write_str(&mut self.w, &value).await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_acpeerstates(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare("SELECT addr, backward_verified_key_id, last_seen, last_seen_autocrypt, public_key, prefer_encrypted, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let addr: String = row.get("addr")?;
let backward_verified_key_id: Option<i64> = row.get("backward_verified_key_id")?;
let prefer_encrypted: i64 = row.get("prefer_encrypted")?;
let last_seen: i64 = row.get("last_seen")?;
let last_seen_autocrypt: i64 = row.get("last_seen_autocrypt")?;
let public_key: Option<Vec<u8>> = row.get("public_key")?;
let public_key_fingerprint: Option<String> = row.get("public_key_fingerprint")?;
let gossip_timestamp: i64 = row.get("gossip_timestamp")?;
let gossip_key: Option<Vec<u8>> = row.get("gossip_key")?;
let gossip_key_fingerprint: Option<String> = row.get("gossip_key_fingerprint")?;
let verified_key: Option<Vec<u8>> = row.get("verified_key")?;
let verified_key_fingerprint: Option<String> = row.get("verified_key_fingerprint")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "addr").await?;
write_str(&mut self.w, &addr).await?;
if let Some(backward_verified_key_id) = backward_verified_key_id {
write_str(&mut self.w, "backward_verified_key_id").await?;
write_i64(&mut self.w, backward_verified_key_id).await?;
}
if let Some(gossip_key) = gossip_key {
write_str(&mut self.w, "gossip_key").await?;
write_bytes(&mut self.w, &gossip_key).await?;
}
if let Some(gossip_key_fingerprint) = gossip_key_fingerprint {
write_str(&mut self.w, "gossip_key_fingerprint").await?;
write_str(&mut self.w, &gossip_key_fingerprint).await?;
}
write_str(&mut self.w, "gossip_timestamp").await?;
write_i64(&mut self.w, gossip_timestamp).await?;
write_str(&mut self.w, "last_seen").await?;
write_i64(&mut self.w, last_seen).await?;
write_str(&mut self.w, "last_seen_autocrypt").await?;
write_i64(&mut self.w, last_seen_autocrypt).await?;
write_str(&mut self.w, "prefer_encrypted").await?;
write_i64(&mut self.w, prefer_encrypted).await?;
if let Some(public_key) = public_key {
write_str(&mut self.w, "public_key").await?;
write_bytes(&mut self.w, &public_key).await?;
}
if let Some(public_key_fingerprint) = public_key_fingerprint {
write_str(&mut self.w, "public_key_fingerprint").await?;
write_str(&mut self.w, &public_key_fingerprint).await?;
}
if let Some(verified_key) = verified_key {
write_str(&mut self.w, "verified_key").await?;
write_bytes(&mut self.w, &verified_key).await?;
}
if let Some(verified_key_fingerprint) = verified_key_fingerprint {
write_str(&mut self.w, "verified_key_fingerprint").await?;
write_str(&mut self.w, &verified_key_fingerprint).await?;
}
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes chats.
async fn serialize_chats(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare(
"SELECT \
id,\
type,\
name,\
blocked,\
grpid,\
param,\
archived,\
gossiped_timestamp,\
locations_send_begin,\
locations_send_until,\
locations_last_sent,\
created_timestamp,\
muted_until,\
ephemeral_timer,\
protected FROM chats",
)?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: u32 = row.get("id")?;
let typ: u32 = row.get("type")?;
let name: String = row.get("name")?;
let blocked: u32 = row.get("blocked")?;
let grpid: String = row.get("grpid")?;
let param: String = row.get("param")?;
let archived: bool = row.get("archived")?;
let gossiped_timestamp: i64 = row.get("gossiped_timestamp")?;
let locations_send_begin: i64 = row.get("locations_send_begin")?;
let locations_send_until: i64 = row.get("locations_send_until")?;
let locations_last_sent: i64 = row.get("locations_last_sent")?;
let created_timestamp: i64 = row.get("created_timestamp")?;
let muted_until: i64 = row.get("muted_until")?;
let ephemeral_timer: i64 = row.get("ephemeral_timer")?;
let protected: u32 = row.get("protected")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "archived").await?;
write_bool(&mut self.w, archived).await?;
write_str(&mut self.w, "blocked").await?;
write_u32(&mut self.w, blocked).await?;
write_str(&mut self.w, "created_timestamp").await?;
write_i64(&mut self.w, created_timestamp).await?;
write_str(&mut self.w, "ephemeral_timer").await?;
write_i64(&mut self.w, ephemeral_timer).await?;
write_str(&mut self.w, "gossiped_timestamp").await?;
write_i64(&mut self.w, gossiped_timestamp).await?;
write_str(&mut self.w, "grpid").await?;
write_str(&mut self.w, &grpid).await?;
write_str(&mut self.w, "id").await?;
write_u32(&mut self.w, id).await?;
write_str(&mut self.w, "locations_last_sent").await?;
write_i64(&mut self.w, locations_last_sent).await?;
write_str(&mut self.w, "locations_send_begin").await?;
write_i64(&mut self.w, locations_send_begin).await?;
write_str(&mut self.w, "locations_send_until").await?;
write_i64(&mut self.w, locations_send_until).await?;
write_str(&mut self.w, "muted_until").await?;
write_i64(&mut self.w, muted_until).await?;
write_str(&mut self.w, "name").await?;
write_str(&mut self.w, &name).await?;
write_str(&mut self.w, "param").await?;
write_str(&mut self.w, &param).await?;
write_str(&mut self.w, "protected").await?;
write_u32(&mut self.w, protected).await?;
write_str(&mut self.w, "type").await?;
write_u32(&mut self.w, typ).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_chats_contacts(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT chat_id, contact_id FROM chats_contacts")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let chat_id: u32 = row.get("chat_id")?;
let contact_id: u32 = row.get("contact_id")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "chat_id").await?;
write_u32(&mut self.w, chat_id).await?;
write_str(&mut self.w, "contact_id").await?;
write_u32(&mut self.w, contact_id).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes contacts.
async fn serialize_contacts(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare(
"SELECT \
id,\
name,\
addr,\
origin,\
blocked,\
last_seen,\
param,\
authname,\
selfavatar_sent,\
status FROM contacts",
)?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: u32 = row.get("id")?;
let name: String = row.get("name")?;
let authname: String = row.get("authname")?;
let addr: String = row.get("addr")?;
let origin: u32 = row.get("origin")?;
let blocked: Option<bool> = row.get("blocked")?;
let blocked = blocked.unwrap_or_default();
let last_seen: i64 = row.get("last_seen")?;
let selfavatar_sent: i64 = row.get("selfavatar_sent")?;
let param: String = row.get("param")?;
let status: Option<String> = row.get("status")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "addr").await?;
write_str(&mut self.w, &addr).await?;
write_str(&mut self.w, "authname").await?;
write_str(&mut self.w, &authname).await?;
write_str(&mut self.w, "blocked").await?;
write_bool(&mut self.w, blocked).await?;
write_str(&mut self.w, "id").await?;
write_u32(&mut self.w, id).await?;
write_str(&mut self.w, "last_seen").await?;
write_i64(&mut self.w, last_seen).await?;
write_str(&mut self.w, "name").await?;
write_str(&mut self.w, &name).await?;
write_str(&mut self.w, "origin").await?;
write_u32(&mut self.w, origin).await?;
// TODO: parse param instead of serializeing as is
write_str(&mut self.w, "param").await?;
write_str(&mut self.w, &param).await?;
write_str(&mut self.w, "selfavatar_sent").await?;
write_i64(&mut self.w, selfavatar_sent).await?;
if let Some(status) = status {
if !status.is_empty() {
write_str(&mut self.w, "status").await?;
write_str(&mut self.w, &status).await?;
}
}
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_dns_cache(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT hostname, address, timestamp FROM dns_cache")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let hostname: String = row.get("hostname")?;
let address: String = row.get("address")?;
let timestamp: i64 = row.get("timestamp")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "address").await?;
write_str(&mut self.w, &address).await?;
write_str(&mut self.w, "hostname").await?;
write_str(&mut self.w, &hostname).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_imap(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, rfc724_mid, folder, target, uid, uidvalidity FROM imap")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let folder: String = row.get("folder")?;
let target: String = row.get("target")?;
let uid: i64 = row.get("uid")?;
let uidvalidity: i64 = row.get("uidvalidity")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "folder").await?;
write_str(&mut self.w, &folder).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "rfc724_mid").await?;
write_str(&mut self.w, &rfc724_mid).await?;
write_str(&mut self.w, "target").await?;
write_str(&mut self.w, &target).await?;
write_str(&mut self.w, "uid").await?;
write_i64(&mut self.w, uid).await?;
write_str(&mut self.w, "uidvalidity").await?;
write_i64(&mut self.w, uidvalidity).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_imap_sync(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT folder, uidvalidity, uid_next, modseq FROM imap_sync")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let folder: String = row.get("folder")?;
let uidvalidity: i64 = row.get("uidvalidity")?;
let uidnext: i64 = row.get("uid_next")?;
let modseq: i64 = row.get("modseq")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "folder").await?;
write_str(&mut self.w, &folder).await?;
write_str(&mut self.w, "modseq").await?;
write_i64(&mut self.w, modseq).await?;
write_str(&mut self.w, "uidnext").await?;
write_i64(&mut self.w, uidnext).await?;
write_str(&mut self.w, "uidvalidity").await?;
write_i64(&mut self.w, uidvalidity).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_keypairs(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id,addr,private_key,public_key,created FROM keypairs")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: u32 = row.get("id")?;
let addr: String = row.get("addr")?;
let private_key: Vec<u8> = row.get("private_key")?;
let public_key: Vec<u8> = row.get("public_key")?;
let created: i64 = row.get("created")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "addr").await?;
write_str(&mut self.w, &addr).await?;
write_str(&mut self.w, "created").await?;
write_i64(&mut self.w, created).await?;
write_str(&mut self.w, "id").await?;
write_u32(&mut self.w, id).await?;
write_str(&mut self.w, "private_key").await?;
write_bytes(&mut self.w, &private_key).await?;
write_str(&mut self.w, "public_key").await?;
write_bytes(&mut self.w, &public_key).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_leftgroups(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare("SELECT grpid FROM leftgrps")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let grpid: String = row.get("grpid")?;
write_str(&mut self.w, &grpid).await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_locations(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, latitude, longitude, accuracy, timestamp, chat_id, from_id, independent FROM locations")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let latitude: f64 = row.get("latitude")?;
let longitude: f64 = row.get("longitude")?;
let accuracy: f64 = row.get("accuracy")?;
let timestamp: i64 = row.get("timestamp")?;
let chat_id: u32 = row.get("chat_id")?;
let from_id: u32 = row.get("from_id")?;
let independent: u32 = row.get("independent")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "accuracy").await?;
write_f64(&mut self.w, accuracy).await?;
write_str(&mut self.w, "chat_id").await?;
write_u32(&mut self.w, chat_id).await?;
write_str(&mut self.w, "from_id").await?;
write_u32(&mut self.w, from_id).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "independent").await?;
write_u32(&mut self.w, independent).await?;
write_str(&mut self.w, "latitude").await?;
write_f64(&mut self.w, latitude).await?;
write_str(&mut self.w, "longitude").await?;
write_f64(&mut self.w, longitude).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes MDNs.
async fn serialize_mdns(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT msg_id, contact_id, timestamp_sent FROM msgs_mdns")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let msg_id: u32 = row.get("msg_id")?;
let contact_id: u32 = row.get("contact_id")?;
let timestamp_sent: i64 = row.get("timestamp_sent")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "contact_id").await?;
write_u32(&mut self.w, contact_id).await?;
write_str(&mut self.w, "msg_id").await?;
write_u32(&mut self.w, msg_id).await?;
write_str(&mut self.w, "timestamp_sent").await?;
write_i64(&mut self.w, timestamp_sent).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes messages.
async fn serialize_messages(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare(
"SELECT
id,
rfc724_mid,
chat_id,
from_id, to_id,
timestamp,
type,
state,
msgrmsg,
bytes,
txt,
txt_raw,
param,
timestamp_sent,
timestamp_rcvd,
hidden,
mime_compressed,
mime_headers,
mime_in_reply_to,
mime_references,
location_id FROM msgs",
)?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let chat_id: i64 = row.get("chat_id")?;
let from_id: i64 = row.get("from_id")?;
let to_id: i64 = row.get("to_id")?;
let timestamp: i64 = row.get("timestamp")?;
let typ: i64 = row.get("type")?;
let state: i64 = row.get("state")?;
let msgrmsg: i64 = row.get("msgrmsg")?;
let bytes: i64 = row.get("bytes")?;
let txt: String = row.get("txt")?;
let txt_raw: String = row.get("txt_raw")?;
let param: String = row.get("param")?;
let timestamp_sent: i64 = row.get("timestamp_sent")?;
let timestamp_rcvd: i64 = row.get("timestamp_rcvd")?;
let hidden: i64 = row.get("hidden")?;
let mime_compressed: i64 = row.get("mime_compressed")?;
let mime_headers: Vec<u8> =
row.get("mime_headers")
.or_else(|err| match row.get_ref("mime_headers")? {
ValueRef::Null => Ok(Vec::new()),
ValueRef::Text(text) => Ok(text.to_vec()),
ValueRef::Blob(blob) => Ok(blob.to_vec()),
ValueRef::Integer(_) | ValueRef::Real(_) => Err(err),
})?;
let mime_in_reply_to: Option<String> = row.get("mime_in_reply_to")?;
let mime_references: Option<String> = row.get("mime_references")?;
let location_id: i64 = row.get("location_id")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "bytes").await?;
write_i64(&mut self.w, bytes).await?;
write_str(&mut self.w, "chat_id").await?;
write_i64(&mut self.w, chat_id).await?;
write_str(&mut self.w, "from_id").await?;
write_i64(&mut self.w, from_id).await?;
write_str(&mut self.w, "hidden").await?;
write_i64(&mut self.w, hidden).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "location_id").await?;
write_i64(&mut self.w, location_id).await?;
write_str(&mut self.w, "mime_compressed").await?;
write_i64(&mut self.w, mime_compressed).await?;
write_str(&mut self.w, "mime_headers").await?;
write_bytes(&mut self.w, &mime_headers).await?;
if let Some(mime_in_reply_to) = mime_in_reply_to {
write_str(&mut self.w, "mime_in_reply_to").await?;
write_str(&mut self.w, &mime_in_reply_to).await?;
}
if let Some(mime_references) = mime_references {
write_str(&mut self.w, "mime_references").await?;
write_str(&mut self.w, &mime_references).await?;
}
write_str(&mut self.w, "msgrmsg").await?;
write_i64(&mut self.w, msgrmsg).await?;
write_str(&mut self.w, "param").await?;
write_str(&mut self.w, &param).await?;
write_str(&mut self.w, "rfc724_mid").await?;
write_str(&mut self.w, &rfc724_mid).await?;
write_str(&mut self.w, "state").await?;
write_i64(&mut self.w, state).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
write_str(&mut self.w, "timestamp_rcvd").await?;
write_i64(&mut self.w, timestamp_rcvd).await?;
write_str(&mut self.w, "timestamp_sent").await?;
write_i64(&mut self.w, timestamp_sent).await?;
write_str(&mut self.w, "to_id").await?;
write_i64(&mut self.w, to_id).await?;
write_str(&mut self.w, "txt").await?;
write_str(&mut self.w, &txt).await?;
write_str(&mut self.w, "txt_raw").await?;
write_str(&mut self.w, &txt_raw).await?;
write_str(&mut self.w, "type").await?;
write_i64(&mut self.w, typ).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_msgs_status_updates(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, msg_id, uid, update_item FROM msgs_status_updates")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let msg_id: i64 = row.get("msg_id")?;
let uid: String = row.get("uid")?;
let update_item: String = row.get("update_item")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "msg_id").await?;
write_i64(&mut self.w, msg_id).await?;
write_str(&mut self.w, "uid").await?;
write_str(&mut self.w, &uid).await?;
write_str(&mut self.w, "update_item").await?;
write_str(&mut self.w, &update_item).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes reactions.
async fn serialize_reactions(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT msg_id, contact_id, reaction FROM reactions")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let msg_id: u32 = row.get("msg_id")?;
let contact_id: u32 = row.get("contact_id")?;
let reaction: String = row.get("reaction")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "contact_id").await?;
write_u32(&mut self.w, contact_id).await?;
write_str(&mut self.w, "msg_id").await?;
write_u32(&mut self.w, msg_id).await?;
write_str(&mut self.w, "reaction").await?;
write_str(&mut self.w, &reaction).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_sending_domains(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT domain, dkim_works FROM sending_domains")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let domain: String = row.get("domain")?;
let dkim_works: i64 = row.get("dkim_works")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "dkim_works").await?;
write_i64(&mut self.w, dkim_works).await?;
write_str(&mut self.w, "domain").await?;
write_str(&mut self.w, &domain).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_tokens(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, namespc, foreign_id, token, timestamp FROM tokens")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let namespace: u32 = row.get("namespc")?;
let foreign_id: u32 = row.get("foreign_id")?;
let token: String = row.get("token")?;
let timestamp: i64 = row.get("timestamp")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "foreign_id").await?;
write_u32(&mut self.w, foreign_id).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "namespace").await?;
write_u32(&mut self.w, namespace).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
write_str(&mut self.w, "token").await?;
write_str(&mut self.w, &token).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize(&mut self) -> Result<()> {
let dbversion: String = self.tx.query_row(
"SELECT value FROM config WHERE keyname='dbversion'",
(),
|row| row.get(0),
)?;
if dbversion != SERIALIZE_DBVERSION {
return Err(anyhow!(
"cannot serialize database version {dbversion}, expected {SERIALIZE_DBVERSION}"
));
}
self.w.write_all(b"d").await?;
write_str(&mut self.w, "_config").await?;
self.serialize_config().await?;
write_str(&mut self.w, "acpeerstates").await?;
self.serialize_acpeerstates()
.await
.context("serialize autocrypt peerstates")?;
write_str(&mut self.w, "chats").await?;
self.serialize_chats().await?;
write_str(&mut self.w, "chats_contacts").await?;
self.serialize_chats_contacts()
.await
.context("serialize chats_contacts")?;
write_str(&mut self.w, "contacts").await?;
self.serialize_contacts().await?;
write_str(&mut self.w, "dns_cache").await?;
self.serialize_dns_cache()
.await
.context("serialize dns_cache")?;
write_str(&mut self.w, "imap").await?;
self.serialize_imap().await.context("serialize imap")?;
write_str(&mut self.w, "imap_sync").await?;
self.serialize_imap_sync()
.await
.context("serialize imap_sync")?;
write_str(&mut self.w, "keypairs").await?;
self.serialize_keypairs().await?;
write_str(&mut self.w, "leftgroups").await?;
self.serialize_leftgroups().await?;
write_str(&mut self.w, "locations").await?;
self.serialize_locations().await?;
write_str(&mut self.w, "mdns").await?;
self.serialize_mdns().await?;
write_str(&mut self.w, "messages").await?;
self.serialize_messages()
.await
.context("serialize messages")?;
write_str(&mut self.w, "msgs_status_updates").await?;
self.serialize_msgs_status_updates()
.await
.context("serialize msgs_status_updates")?;
write_str(&mut self.w, "reactions").await?;
self.serialize_reactions().await?;
write_str(&mut self.w, "sending_domains").await?;
self.serialize_sending_domains()
.await
.context("serialize sending_domains")?;
write_str(&mut self.w, "tokens").await?;
self.serialize_tokens().await?;
// jobs table is skipped
// multi_device_sync is skipped
// imap_markseen is skipped, it is usually empty and the device exporting the
// database should still be able to clear it.
// smtp, smtp_mdns and smtp_status_updates tables are skipped, they are part of the
// outgoing message queue.
// devmsglabels is skipped, it is reset in `delete_and_reset_all_device_msgs()` on import
// anyway
// bobstate is not serialized, it is temporary for joining or adding a contact.
//
// TODO insert welcome message on import like done in `delete_and_reset_all_device_msgs()`?
self.w.write_all(b"e").await?;
self.w.flush().await?;
Ok(())
}
}
impl Sql {
/// Serializes the database into a bytestream.
pub async fn serialize(&self, w: impl AsyncWrite + Unpin) -> Result<()> {
let mut conn = self.get_connection().await?;
// Start a read transaction to take a database snapshot.
let transaction = conn.transaction()?;
let mut encoder = Encoder::new(transaction, w);
encoder.serialize().await?;
Ok(())
}
}

View File

@@ -429,6 +429,12 @@ pub enum StockMessage {
fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
))]
CantDecryptOutgoingMsgs = 175,
#[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
MsgYouReacted = 176,
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
MsgReactedBy = 177,
}
impl StockMessage {
@@ -730,6 +736,27 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
}
}
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
pub(crate) async fn msg_reacted(
context: &Context,
by_contact: ContactId,
reaction: &str,
summary: &str,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouReacted)
.await
.replace1(reaction)
.replace2(summary)
} else {
translated(context, StockMessage::MsgReactedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace2(reaction)
.replace3(summary)
}
}
/// Stock string: `GIF`.
pub(crate) async fn gif(context: &Context) -> String {
translated(context, StockMessage::Gif).await

View File

@@ -10,7 +10,9 @@ use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use crate::stock_str::msg_reacted;
use crate::tools::truncate;
use anyhow::Result;
/// Prefix displayed before message and separated by ":" in the chatlist.
#[derive(Debug)]
@@ -62,7 +64,24 @@ impl Summary {
msg: &Message,
chat: &Chat,
contact: Option<&Contact>,
) -> Self {
) -> Result<Summary> {
if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
.get_last_reaction_if_newer_than(context, msg.timestamp_sort)
.await?
{
// there is a reaction newer than the latest message, show that.
// sorting and therefore date is still the one of the last message,
// the reaction is is more sth. that overlays temporarily.
let summary = reaction_msg.get_summary_text_without_prefix(context).await;
return Ok(Summary {
prefix: None,
text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
timestamp: msg.get_timestamp(), // message timestamp (not reaction) to make timestamps more consistent with chats ordering
state: msg.state, // message state (not reaction) - indicating if it was me sending the last message
thumbnail_path: None,
});
}
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
@@ -102,13 +121,13 @@ impl Summary {
None
};
Self {
Ok(Summary {
prefix,
text,
timestamp: msg.get_timestamp(),
state: msg.state,
thumbnail_path,
}
})
}
/// Returns the [`Summary::text`] attribute truncated to an approximate length.
@@ -120,6 +139,17 @@ impl Summary {
impl Message {
/// Returns a summary text.
async fn get_summary_text(&self, context: &Context) -> String {
let summary = self.get_summary_text_without_prefix(context).await;
if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context).await, summary)
} else {
summary
}
}
/// Returns a summary text without "Forwarded:" prefix.
async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
let (emoji, type_name, type_file, append_text);
match self.viewtype {
Viewtype::Image => {
@@ -230,12 +260,6 @@ impl Message {
summary
};
let summary = if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context).await, summary)
} else {
summary
};
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
}
}
@@ -246,6 +270,11 @@ mod tests {
use crate::param::Param;
use crate::test_utils as test;
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
assert_eq!(msg.get_summary_text(ctx).await, expected);
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_text() {
let d = test::TestContext::new().await;
@@ -255,131 +284,81 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.to_string());
assert_eq!(
msg.get_summary_text(ctx).await,
"bla bla" // for simple text, the type is not added to the summary
);
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
let mut msg = Message::new(Viewtype::Image);
msg.set_file("foo.jpg", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"📷 Image" // file names are not added for images
);
assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images
let mut msg = Message::new(Viewtype::Image);
msg.set_text(some_text.to_string());
msg.set_file("foo.jpg", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"📷 bla bla" // type is visible by emoji if text is set
);
assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set
let mut msg = Message::new(Viewtype::Video);
msg.set_file("foo.mp4", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎥 Video" // file names are not added for videos
);
assert_summary_texts(&msg, ctx, "🎥 Video").await; // file names are not added for videos
let mut msg = Message::new(Viewtype::Video);
msg.set_text(some_text.to_string());
msg.set_file("foo.mp4", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎥 bla bla" // type is visible by emoji if text is set
);
assert_summary_texts(&msg, ctx, "🎥 bla bla").await; // type is visible by emoji if text is set
let mut msg = Message::new(Viewtype::Gif);
msg.set_file("foo.gif", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"GIF" // file names are not added for GIFs
);
assert_summary_texts(&msg, ctx, "GIF").await; // file names are not added for GIFs
let mut msg = Message::new(Viewtype::Gif);
msg.set_text(some_text.to_string());
msg.set_file("foo.gif", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"GIF \u{2013} bla bla" // file names are not added for GIFs
);
assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; // file names are not added for GIFs
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file("foo.png", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Sticker" // file names are not added for stickers
);
assert_summary_texts(&msg, ctx, "Sticker").await; // file names are not added for stickers
let mut msg = Message::new(Viewtype::Voice);
msg.set_file("foo.mp3", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎤 Voice message" // file names are not added for voice messages
);
assert_summary_texts(&msg, ctx, "🎤 Voice message").await; // file names are not added for voice messages
let mut msg = Message::new(Viewtype::Voice);
msg.set_text(some_text.clone());
msg.set_file("foo.mp3", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎤 bla bla" // `\u{2013}` explicitly checks for "EN DASH"
);
assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.mp3", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎵 foo.mp3" // file name is added for audio
);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.mp3", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎵 foo.mp3" // file name is added for audio, empty text is not added
);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio, empty text is not added
let mut msg = Message::new(Viewtype::Audio);
msg.set_text(some_text.clone());
msg.set_file("foo.mp3", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"🎵 foo.mp3 \u{2013} bla bla" // file name and text added for audio
);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; // file name and text added for audio
let mut msg = Message::new(Viewtype::File);
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"📎 foo.bar" // file name is added for files
);
assert_summary_texts(&msg, ctx, "📎 foo.bar").await; // file name is added for files
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"📎 foo.bar \u{2013} bla bla" // file name is added for files
);
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Video chat invitation" // text is not added for videochat invitations
);
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
// Forwarded
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.clone());
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(
msg.get_summary_text(ctx).await,
"Forwarded: bla bla" // for simple text, the type is not added to the summary
);
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
@@ -389,14 +368,15 @@ mod tests {
msg.get_summary_text(ctx).await,
"Forwarded: 📎 foo.bar \u{2013} bla bla"
);
assert_eq!(
msg.get_summary_text_without_prefix(ctx).await,
"📎 foo.bar \u{2013} bla bla"
); // skipping prefix used for reactions summaries
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.param.set(Param::File, "foo.bar");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
assert_eq!(
msg.get_summary_text(ctx).await,
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; // file name is not added for autocrypt setup messages
}
}

View File

@@ -181,6 +181,7 @@ pub fn get_release_timestamp() -> i64 {
*crate::release::DATE,
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
.and_utc()
.timestamp_millis()
/ 1_000
}
@@ -205,7 +206,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
),
)
.await;
if let Some(timestamp) = chrono::NaiveDateTime::from_timestamp_opt(now, 0) {
if let Some(timestamp) = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0) {
add_device_msg_with_importance(
context,
Some(
@@ -232,7 +233,7 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::update_reminder_msg_body(context).await;
if let Some(timestamp) = chrono::NaiveDateTime::from_timestamp_opt(now, 0) {
if let Some(timestamp) = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0) {
add_device_msg(
context,
Some(
@@ -1214,6 +1215,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
.and_utc()
.timestamp_millis()
/ 1_000;
@@ -1329,6 +1331,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
.and_utc()
.timestamp_millis()
/ 1_000;
assert!(get_release_timestamp() <= time());