Compare commits

..

2 Commits

Author SHA1 Message Date
link2xt
8ac9c6bb09 more debug logging 2026-03-25 23:03:59 +01:00
link2xt
84459b6495 WIP: more delay debugging 2026-03-25 22:32:12 +01:00
48 changed files with 523 additions and 739 deletions

View File

@@ -10,7 +10,7 @@ permissions:
jobs:
dependabot:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata

View File

@@ -1,18 +1,16 @@
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, py.delta.chat and cffi.delta.chat
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
on:
push:
branches:
- main
- build_jsonrpc_docs_ci
permissions: {}
jobs:
build-rs:
runs-on: ubuntu-latest
environment:
name: rs.delta.chat
url: https://rs.delta.chat/
steps:
- uses: actions/checkout@v6
@@ -25,15 +23,12 @@ jobs:
- name: Upload to rs.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
build-python:
runs-on: ubuntu-latest
environment:
name: py.delta.chat
url: https://py.delta.chat/
steps:
- uses: actions/checkout@v6
@@ -47,15 +42,12 @@ jobs:
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
build-c:
runs-on: ubuntu-latest
environment:
name: c.delta.chat
url: https://c.delta.chat/
steps:
- uses: actions/checkout@v6
@@ -69,16 +61,12 @@ jobs:
- name: Upload to c.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
build-ts:
runs-on: ubuntu-latest
environment:
name: js.jsonrpc.delta.chat
url: https://js.jsonrpc.delta.chat/
defaults:
run:
working-directory: ./deltachat-jsonrpc/typescript
@@ -102,27 +90,6 @@ jobs:
- name: Upload to js.jsonrpc.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
build-cffi:
runs-on: ubuntu-latest
environment:
name: cffi.delta.chat
url: https://cffi.delta.chat/
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"

31
.github/workflows/upload-ffi-docs.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# GitHub Actions workflow
# to build `deltachat_ffi` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- main
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"

View File

@@ -1,61 +1,5 @@
# Changelog
## [2.48.0] - 2026-03-30
### Fixes
- Fix reordering problems in multi-relay setups by not sorting received messages below the last seen one.
- Always sort "Messages are end-to-end encrypted" notice to the beginning.
- Make Message-ID of pre-messages stable across resends ([#8007](https://github.com/chatmail/core/pull/8007)).
- Delete `imap_markseen` entries not corresponding to any `imap` rows.
- Cleanup `imap` and `imap_sync` records without transport in housekeeping.
- When receiving MDN, mark all preceding messages as noticed, even having same timestamp ([#7928](https://github.com/chatmail/core/pull/7928)).
- Remove migration 108 preventing upgrades from core 1.86.0 to the latest version.
### Features / Changes
- Improve IMAP loop logs.
- Add decryption error to the device message about outgoing message decryption failure.
- Log received message sort timestamp.
### Performance
- Move sorting outside of SQL query in `store_seen_flags_on_imap`.
### API-Changes
- Add JSON-RPC API `markfresh_chat()`.
- ffi: Correctly declare `dc_event_channel_new()` as having no params ([#7831](https://github.com/chatmail/core/pull/7831)).
### Refactor
- Remove `wal_checkpoint_mutex`, lock `write_mutex` before getting sql connection instead.
- Replace async `RwLock` with sync `RwLock` for stock strings.
- Cleanup remaining Autocrypt Setup Message processing in `mimeparser`.
- SecureJoin: do not check for self address in forwarding protection.
- Fix clippy warnings.
### CI
- Update {c,py}.delta.chat website deployments.
- Use environments for {rs,cffi,js.jsonrpc}.delta.chat deployments.
- Fix https://docs.zizmor.sh/audits/#bot-conditions.
### Documentation
- Add SQL performance tips to STYLE.md.
### Tests
- Remove `test_old_message_5`.
- Do not rely on loading newest chat in `load_imf_email()`.
- Use `load_imf_email()` more.
- The message is sorted correctly in the chat even if it arrives late.
### Miscellaneous Tasks
- cargo: update rustls-webpki to 0.103.10.
## [2.47.0] - 2026-03-24
### Fixes
@@ -8040,4 +7984,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
[2.46.0]: https://github.com/chatmail/core/compare/v2.45.0..v2.46.0
[2.47.0]: https://github.com/chatmail/core/compare/v2.46.0..v2.47.0
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.0

54
Cargo.lock generated
View File

@@ -827,9 +827,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.44"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"num-traits",
@@ -1307,7 +1307,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.48.0"
version = "2.48.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1416,7 +1416,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.48.0"
version = "2.48.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1437,7 +1437,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.48.0"
version = "2.48.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1453,7 +1453,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.48.0"
version = "2.48.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1482,7 +1482,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.48.0"
version = "2.48.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -2860,9 +2860,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.10"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -3260,9 +3260,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.184"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libm"
@@ -3483,9 +3483,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.8.1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
dependencies = [
"num-traits",
"pxfm",
@@ -4235,18 +4235,18 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.11"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.11"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
@@ -4615,9 +4615,9 @@ dependencies = [
[[package]]
name = "proptest"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
dependencies = [
"bitflags 2.11.0",
"num-traits",
@@ -4729,9 +4729,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -5988,9 +5988,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.27.0"
version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
"getrandom 0.3.3",
@@ -6144,9 +6144,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.50.0"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
@@ -6403,9 +6403,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.48.0"
version = "2.48.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
@@ -181,7 +181,7 @@ harness = false
anyhow = "1"
async-channel = "2.5.0"
base64 = "0.22"
chrono = { version = "0.4.44", default-features = false }
chrono = { version = "0.4.43", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
@@ -198,7 +198,7 @@ rusqlite = "0.37"
sanitize-filename = "0.6"
serde = "1.0"
serde_json = "1"
tempfile = "3.27.0"
tempfile = "3.25.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.18"

View File

@@ -68,12 +68,6 @@ keyword doesn't help here.
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
to make sure that indexes are used and large tables are not going to be scanned.
Never run `ANALYZE` on the databases,
this makes query planner unpredictable
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
## Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.

View File

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

View File

@@ -364,14 +364,18 @@ uint32_t dc_get_id (dc_context_t* context);
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
* Events are broadcasted to all existing event emitters.
* Events emitted before creation of event emitter
* are not available to event emitter.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per context.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* may or may not be available to event emitter.
*/
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
@@ -3319,14 +3323,18 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
*
* Events are broadcasted to all existing event emitters.
* Events emitted before creation of event emitter
* are not available to event emitter.
*
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
@@ -5971,14 +5979,21 @@ void dc_event_channel_unref(dc_event_channel_t* event_channel);
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
* Events are broadcasted to all existing event emitters.
* Events emitted before creation of event emitter
* are not available to event emitter.
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
*
* @memberof dc_event_channel_t
* @param The event channel.
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager / event channel.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.48.0"
version = "2.48.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.48.0"
"version": "2.48.0-dev"
}

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.48.0"
version = "2.48.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -1047,7 +1047,6 @@ def test_no_old_msg_is_fresh(acfactory):
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
assert len(list(ac1.get_fresh_messages())) == 1
ac1_clone.wait_for_incoming_msg_event()
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.48.0"
version = "2.48.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -288,7 +288,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
chat2.accept()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")

View File

@@ -1 +1 @@
2026-03-30
2026-03-24

View File

@@ -10,8 +10,8 @@ use anyhow::{Context as _, Result, ensure, format_err};
use base64::Engine as _;
use futures::StreamExt;
use image::ImageReader;
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
use num_traits::FromPrimitive;
use tokio::{fs, task};
use tokio_stream::wrappers::ReadDirStream;
@@ -362,10 +362,7 @@ impl<'a> BlobObject<'a> {
return Ok(name);
}
let mut img = imgreader.decode().context("image decode failure")?;
let orientation = exif
.as_ref()
.map(|exif| exif_orientation(exif, context))
.unwrap_or(Orientation::NoTransforms);
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
if *vt == Viewtype::Sticker {
@@ -384,7 +381,13 @@ impl<'a> BlobObject<'a> {
return Ok(name);
}
}
img.apply_orientation(orientation);
img = match orientation {
Some(90) => img.rotate90(),
Some(180) => img.rotate180(),
Some(270) => img.rotate270(),
_ => img,
};
// max_wh is the maximum image width and height, i.e. the resolution-limit.
// target_wh target-resolution for resizing the image.
@@ -548,17 +551,18 @@ fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
Ok((len, exif))
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)
&& let Some(val) = orientation.value.get_uint(0)
&& let Ok(val) = TryInto::<u8>::try_into(val)
{
return Orientation::from_exif(val).unwrap_or({
warn!(context, "Exif orientation value ignored: {val:?}.");
Orientation::NoTransforms
});
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return 180,
Some(6) => return 90,
Some(8) => return 270,
other => warn!(context, "Exif orientation value ignored: {other:?}."),
}
}
Orientation::NoTransforms
0
}
/// All files in the blobdir.

View File

@@ -305,7 +305,7 @@ async fn test_recode_image_2() {
has_exif: true,
original_width: 2000,
original_height: 1800,
orientation: Some(Orientation::Rotate270),
orientation: 270,
compressed_width: 1800,
compressed_height: 2000,
..Default::default()
@@ -336,28 +336,6 @@ async fn test_recode_image_2() {
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_vflipped() {
let bytes = include_bytes!("../../test-data/image/rectangle200x180-vflipped.jpg");
let img_rotated = SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
has_exif: true,
original_width: 200,
original_height: 180,
orientation: Some(Orientation::FlipVertical),
compressed_width: 200,
compressed_height: 180,
..Default::default()
}
.test()
.await
.unwrap();
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_bad_exif() {
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
@@ -552,7 +530,7 @@ struct SendImageCheckMediaquality<'a> {
pub(crate) has_exif: bool,
pub(crate) original_width: u32,
pub(crate) original_height: u32,
pub(crate) orientation: Option<Orientation>,
pub(crate) orientation: i32,
pub(crate) res_viewtype: Option<Viewtype>,
pub(crate) compressed_width: u32,
pub(crate) compressed_height: u32,
@@ -568,7 +546,7 @@ impl SendImageCheckMediaquality<'_> {
let has_exif = self.has_exif;
let original_width = self.original_width;
let original_height = self.original_height;
let orientation = self.orientation.unwrap_or(Orientation::NoTransforms);
let orientation = self.orientation;
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
let compressed_width = self.compressed_width;
let compressed_height = self.compressed_height;

View File

@@ -477,17 +477,12 @@ impl ChatId {
/// Adds message "Messages are end-to-end encrypted".
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
let text = stock_str::messages_e2ee_info_msg(context);
// Sort this notice to the very beginning of the chat.
// We don't want any message to appear before this notice
// which is normally added when encrypted chat is created.
let sort_timestamp = 0;
add_info_msg_with_cmd(
context,
self,
&text,
SystemMessage::ChatE2ee,
Some(sort_timestamp),
Some(timestamp),
timestamp,
None,
None,
@@ -930,17 +925,6 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
.unwrap_or(0))
}
/// Returns timestamp of us joining the chat if we are the member of the chat.
pub(crate) async fn join_timestamp(self, context: &Context) -> Result<Option<i64>> {
context
.sql
.query_get_value(
"SELECT add_timestamp FROM chats_contacts WHERE chat_id=? AND contact_id=?",
(self, ContactId::SELF),
)
.await
}
/// Returns timestamp of the latest message in the chat,
/// including hidden messages or a draft if there is one.
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
@@ -1228,11 +1212,15 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
/// corresponding event in case of a system message (usually the current system time).
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
/// to the chat bottom.
/// `received` -- whether the message is received. Otherwise being sent.
/// `incoming` -- whether the message is incoming.
pub(crate) async fn calc_sort_timestamp(
self,
context: &Context,
message_timestamp: i64,
always_sort_to_bottom: bool,
received: bool,
incoming: bool,
) -> Result<i64> {
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
@@ -1252,6 +1240,38 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
(self, MessageState::OutDraft),
)
.await?
} else if received {
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
// middle of the chat, so we go after the newest non fresh message.
//
// But if a received outgoing message is older than some seen message, better sort the
// received message purely by timestamp. We could place it just before that seen
// message, but anyway the user may not notice it.
//
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
// fresh ones, they rather mean that older incoming messages are actually seen as well.
context
.sql
.query_row_optional(
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
FROM msgs
WHERE chat_id=? AND hidden=0 AND state>?
HAVING COUNT(*) > 0",
(MessageState::InSeen, self, MessageState::InFresh),
|row| {
let ts: i64 = row.get(0)?;
let ts_sent_seen: i64 = row.get(1)?;
Ok((ts, ts_sent_seen))
},
)
.await?
.and_then(|(ts, ts_sent_seen)| {
match incoming || ts_sent_seen <= message_timestamp {
true => Some(ts),
false => None,
}
})
} else {
None
};
@@ -1262,16 +1282,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
sort_timestamp = last_msg_time;
}
if let Some(join_timestamp) = self.join_timestamp(context).await? {
// If we are the member of the chat, don't add messages
// before the timestamp of us joining it.
// This is needed to avoid sorting "Member added"
// or automatically sent bot welcome messages
// above SecureJoin system messages.
Ok(std::cmp::max(sort_timestamp, join_timestamp))
} else {
Ok(sort_timestamp)
}
Ok(sort_timestamp)
}
}
@@ -4927,8 +4938,15 @@ pub(crate) async fn add_info_msg_with_cmd(
ts
} else {
let sort_to_bottom = true;
let (received, incoming) = (false, false);
chat_id
.calc_sort_timestamp(context, smeared_time(context), sort_to_bottom)
.calc_sort_timestamp(
context,
smeared_time(context),
sort_to_bottom,
received,
incoming,
)
.await?
};

View File

@@ -2792,7 +2792,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&vc_pubkey).await;
assert!(parsed_by_bob.decryption_error.is_some());
assert!(parsed_by_bob.decrypting_failed);
charlie.recv_msg_trash(&vc_pubkey).await;
}
@@ -2821,7 +2821,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&member_added).await;
assert!(parsed_by_bob.decryption_error.is_some());
assert!(parsed_by_bob.decrypting_failed);
let rcvd = charlie.recv_msg(&member_added).await;
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberAddedToGroup);
@@ -2836,7 +2836,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&hi_msg).await;
assert!(parsed_by_bob.decryption_error.is_none());
assert_eq!(parsed_by_bob.decrypting_failed, false);
}
tcm.section("Alice removes Charlie. Bob must not see it.");
@@ -2853,7 +2853,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&member_removed).await;
assert!(parsed_by_bob.decryption_error.is_some());
assert!(parsed_by_bob.decrypting_failed);
let rcvd = charlie.recv_msg(&member_removed).await;
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberRemovedFromGroup);
@@ -3768,7 +3768,14 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
// The contact should be marked as verified.
check_direct_chat_is_hidden_and_contact_is_verified(alice, bob0).await;
check_direct_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
// TODO: There is a known bug in `observe_securejoin_on_other_device()`:
// When Bob joins a group or broadcast with his first device,
// then a chat with Alice will pop up on his second device.
// When it's fixed, the 2 following lines can be replaced with
// `check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;`
let bob1_alice_contact = bob1.add_or_lookup_contact_no_key(alice).await;
assert!(bob1_alice_contact.is_verified(bob1).await.unwrap());
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
@@ -6105,54 +6112,3 @@ async fn test_leftgrps() -> Result<()> {
Ok(())
}
/// Tests that if the message arrives late,
/// it can still be sorted above the last seen message.
///
/// Versions 2.47 and below always sorted incoming messages
/// after the last seen message, but with
/// the introduction of multi-relay it is possible
/// that some messages arrive only to some relays
/// and are fetched after the already arrived seen message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_late_message_above_seen() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_chat_id = alice
.create_group_with_members("Group", &[bob, charlie])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hello everyone!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
bob_chat_id.accept(bob).await?;
let charlie_chat_id = charlie.recv_msg(&alice_sent).await.chat_id;
charlie_chat_id.accept(charlie).await?;
// Bob sends a message, but the message is delayed.
let bob_sent = bob.send_text(bob_chat_id, "Hello from Bob!").await;
SystemTime::shift(Duration::from_secs(1000));
let charlie_sent = charlie
.send_text(charlie_chat_id, "Hello from Charlie!")
.await;
// Alice immediately receives a message from Charlie and reads it.
let alice_received_from_charlie = alice.recv_msg(&charlie_sent).await;
assert_eq!(
alice_received_from_charlie.get_text(),
"Hello from Charlie!"
);
message::markseen_msgs(alice, vec![alice_received_from_charlie.id]).await?;
// Bob message arrives later, it should be above the message from Charlie.
let alice_received_from_bob = alice.recv_msg(&bob_sent).await;
assert_eq!(alice_received_from_bob.get_text(), "Hello from Bob!");
// The last message in the chat is still from Charlie.
let last_msg = alice.get_last_msg_in(alice_chat_id).await;
assert_eq!(last_msg.get_text(), "Hello from Charlie!");
Ok(())
}

View File

@@ -142,7 +142,7 @@ impl Chatlist {
AND c.blocked!=1
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
process_row,
).await?
@@ -168,7 +168,7 @@ impl Chatlist {
AND c.blocked!=1
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft,),
process_row,
)
@@ -204,7 +204,7 @@ impl Chatlist {
AND IFNULL(c.name_normalized,c.name) LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row,
)
@@ -253,7 +253,7 @@ impl Chatlist {
AND NOT c.archived=?
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
GROUP BY c.id
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
Chattype::Group, ContactId::SELF,
@@ -279,7 +279,7 @@ impl Chatlist {
AND (c.blocked=0 OR c.blocked=2)
AND NOT c.archived=?
GROUP BY c.id
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
process_row,
).await?

View File

@@ -243,6 +243,11 @@ impl Context {
Ok((id, add_timestamp))
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
transaction.execute(
"DELETE FROM imap_sync WHERE transport_id=?",
(transport_id,),
)?;
// Removal timestamp should not be lower than addition timestamp
// to be accepted by other devices when synced.

View File

@@ -323,7 +323,8 @@ impl Imap {
if !ratelimit_duration.is_zero() {
warn!(
context,
"IMAP got rate limited, waiting for {} until can connect.",
"Transport {}: IMAP got rate limited, waiting for {} until can connect.",
self.transport_id,
duration_to_str(ratelimit_duration),
);
let interrupted = async {
@@ -335,12 +336,16 @@ impl Imap {
if interrupted {
info!(
context,
"Connecting to IMAP without waiting for ratelimit due to interrupt."
"Transport {}: Connecting to IMAP without waiting for ratelimit due to interrupt.",
self.transport_id
);
}
}
info!(context, "Connecting to IMAP server.");
info!(
context,
"Transport {}: Connecting to IMAP server.", self.transport_id
);
self.connectivity.set_connecting(context);
self.conn_last_try = tools::Time::now();
@@ -355,7 +360,10 @@ impl Imap {
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
let mut first_error = None;
for lp in login_params {
info!(context, "IMAP trying to connect to {}.", &lp.connection);
info!(
context,
"Transport {}: IMAP trying to connect to {}.", self.transport_id, &lp.connection
);
let connection_candidate = lp.connection.clone();
let client = match Client::connect(
context,
@@ -403,7 +411,10 @@ impl Imap {
let resync_request_sender = self.resync_request_sender.clone();
let session = if capabilities.can_compress {
info!(context, "Enabling IMAP compression.");
info!(
context,
"Transport {}: Enabling IMAP compression.", self.transport_id
);
let compressed_session = session
.compress(|s| {
let session_stream: Box<dyn SessionStream> = Box::new(s);
@@ -436,7 +447,10 @@ impl Imap {
lp.user
)));
self.connectivity.set_preparing(context);
info!(context, "Successfully logged into IMAP server.");
info!(
context,
"Transport {}: Successfully logged into IMAP server.", self.transport_id
);
return Ok(session);
}
@@ -444,7 +458,10 @@ impl Imap {
let imap_user = lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user);
warn!(context, "IMAP failed to login: {err:#}.");
warn!(
context,
"Transport {}: IMAP failed to login: {err:#}.", self.transport_id
);
first_error.get_or_insert(format_err!("{message} ({err:#})"));
// If it looks like the password is wrong, send a notification:
@@ -463,7 +480,11 @@ impl Imap {
)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
warn!(
context,
"Transport {}: Failed to add device message: {e:#}.",
self.transport_id
);
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
@@ -525,10 +546,21 @@ impl Imap {
bail!("IMAP operation attempted while it is torn down");
}
let transport_id = session.transport_id();
info!(
context,
"Transport {transport_id}: fetch_move_delete start."
);
let msgs_fetched = self
.fetch_new_messages(context, session, watch_folder, folder_meaning)
.await
.context("fetch_new_messages")?;
info!(
context,
"Transport {transport_id}: fetch_move_delete finished fetch_new_messages."
);
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
// New messages were fetched and shall be deleted later, restart ephemeral loop.
// Note that the `Config::DeleteDeviceAfter` timer starts as soon as the messages are
@@ -567,10 +599,18 @@ impl Imap {
return Ok(false);
}
info!(
context,
"Transport {transport_id}: fetch_new_messages selects folder {folder:?}."
);
let folder_exists = session
.select_with_uidvalidity(context, folder)
.await
.with_context(|| format!("Failed to select folder {folder:?}"))?;
info!(
context,
"Transport {transport_id}: fetch_new_messages selected folder {folder:?}."
);
if !session.new_mail {
info!(
@@ -1103,22 +1143,16 @@ impl Session {
return Ok(());
}
context
.sql
.execute(
"DELETE FROM imap_markseen WHERE id NOT IN (SELECT imap.id FROM imap)",
(),
)
.await?;
let transport_id = self.transport_id();
let mut rows = context
info!(context, "Transport {transport_id}: Storing seen flags.");
let rows = context
.sql
.query_map_vec(
"SELECT imap.id, uid, folder FROM imap, imap_markseen
WHERE imap.id = imap_markseen.id
AND imap.transport_id=?
AND target = folder",
AND target = folder
ORDER BY folder, uid",
(transport_id,),
|row| {
let rowid: i64 = row.get(0)?;
@@ -1129,16 +1163,6 @@ impl Session {
)
.await?;
// Number of SQL results is expected to be low as
// we usually don't have many messages to mark on IMAP at once.
// We are sorting outside of SQL to avoid SQLite constructing a query plan
// that scans the whole `imap` table. Scanning `imap_markseen` is fine
// as it should not have many items.
// If you change the SQL query, test it with `EXPLAIN QUERY PLAN`.
rows.sort_unstable_by(|(_rowid1, uid1, folder1), (_rowid2, uid2, folder2)| {
(folder1, uid1).cmp(&(folder2, uid2))
});
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
Err(err) => {
@@ -1155,13 +1179,15 @@ impl Session {
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
"Transport {transport_id}: Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
);
continue;
} else {
info!(
context,
"Marked messages {} in folder {} as seen.", uid_set, folder
"Transport {transport_id}: Marked messages {} in folder {} as seen.",
uid_set,
folder
);
}
context
@@ -1176,6 +1202,10 @@ impl Session {
.await
.context("Cannot remove messages marked as seen from imap_markseen table")?;
}
info!(
context,
"Transport {transport_id}: Finished storing seen flags."
);
Ok(())
}
@@ -1516,9 +1546,10 @@ impl Session {
return Ok(());
}
let transport_id = self.transport_id();
info!(
context,
"Server supports metadata, retrieving server comment and admin contact."
"Transport {transport_id}: Server supports metadata, retrieving server comment and admin contact."
);
let mut comment = None;
@@ -1551,7 +1582,8 @@ impl Session {
} else {
warn!(
context,
"Got invalid URL from iroh relay metadata: {:?}.", value
"Transport {transport_id}: Got invalid URL from iroh relay metadata: {:?}.",
value
);
}
}
@@ -1580,6 +1612,7 @@ impl Session {
create_fallback_ice_servers()
};
info!(context, "Transport {transport_id}: Got IMAP metadata.");
*lock = Some(ServerMetadata {
comment,
admin,

View File

@@ -1137,16 +1137,4 @@ mod tests {
Ok(())
}
/// Tests importing a backup from Delta Chat 1.30.3 for Android (core v1.86.0).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_ancient_backup() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = &tcm.unconfigured().await;
let backup_path = Path::new("test-data/core-1.86.0-backup.tar");
imex(context, ImexMode::ImportBackup, backup_path, None).await?;
Ok(())
}
}

View File

@@ -815,11 +815,7 @@ impl Message {
/// Returns the timestamp of the message for sorting.
pub fn get_sort_timestamp(&self) -> i64 {
if self.timestamp_sort != 0 {
self.timestamp_sort
} else {
self.timestamp_sent
}
self.timestamp_sort
}
/// Returns the text of the message.

View File

@@ -852,13 +852,7 @@ impl MimeFactory {
let rfc724_mid = match &self.loaded {
Loaded::Message { msg, .. } => match &self.pre_message_mode {
PreMessageMode::Pre { .. } => {
if msg.pre_rfc724_mid.is_empty() {
create_outgoing_rfc724_mid()
} else {
msg.pre_rfc724_mid.clone()
}
}
PreMessageMode::Pre { .. } => create_outgoing_rfc724_mid(),
_ => msg.rfc724_mid.clone(),
},
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),

View File

@@ -86,9 +86,7 @@ pub(crate) struct MimeMessage {
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
/// Decryption error if decryption of the message has failed.
pub decryption_error: Option<String>,
pub decrypting_failed: bool,
/// Valid signature fingerprint if a message is an
/// Autocrypt encrypted and signed message and corresponding intended recipient fingerprints
@@ -373,7 +371,7 @@ impl MimeMessage {
hop_info += "\n\n";
hop_info += &dkim_results.to_string();
let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
let incoming = !context.is_self_addr(&from.addr).await?;
let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
@@ -438,7 +436,7 @@ impl MimeMessage {
};
let mut autocrypt_header = None;
if from_is_not_self_addr {
if incoming {
// See `get_all_addresses_from_header()` for why we take the last valid header.
for val in aheader_values.iter().rev() {
autocrypt_header = match Aheader::from_str(val) {
@@ -469,7 +467,7 @@ impl MimeMessage {
None
};
let mut public_keyring = if from_is_not_self_addr {
let mut public_keyring = if incoming {
if let Some(autocrypt_header) = autocrypt_header {
vec![autocrypt_header.public_key]
} else {
@@ -654,15 +652,6 @@ impl MimeMessage {
.into_iter()
.last()
.map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
let incoming = if let Some((ref sig_fp, _)) = signature {
sig_fp.hex() != key::self_fingerprint(context).await?
} else {
// rare case of getting a cleartext message
// so we determine 'incoming' flag by From-address
from_is_not_self_addr
};
let mut parser = MimeMessage {
parts: Vec::new(),
headers,
@@ -675,7 +664,7 @@ impl MimeMessage {
from,
incoming,
chat_disposition_notification_to,
decryption_error: mail.err().map(|err| format!("{err:#}")),
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
signature,
@@ -916,7 +905,7 @@ impl MimeMessage {
&& let Some(ref subject) = self.get_subject()
{
let mut prepend_subject = true;
if self.decryption_error.is_none() {
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
@@ -957,7 +946,7 @@ impl MimeMessage {
self.parse_attachments();
// See if an MDN is requested from the other side
if self.decryption_error.is_none()
if !self.decrypting_failed
&& !self.parts.is_empty()
&& let Some(ref dn_to) = self.chat_disposition_notification_to
{
@@ -1089,7 +1078,7 @@ impl MimeMessage {
#[cfg(test)]
/// Returns whether the decrypted data contains the given `&str`.
pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
assert!(self.decryption_error.is_none());
assert!(!self.decrypting_failed);
let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
decoded_str.contains(s)
}

View File

@@ -14,9 +14,7 @@ use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ChatVisibility, is_contact_in_chat, save_broadcast_secret,
};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatVisibility, save_broadcast_secret};
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
@@ -47,7 +45,6 @@ use crate::securejoin::{
self, get_secure_join_step, handle_securejoin_handshake, observe_securejoin_on_other_device,
};
use crate::simplify;
use crate::smtp::msg_has_pending_smtp_job;
use crate::stats::STATISTICS_BOT_EMAIL;
use crate::stock_str;
use crate::sync::Sync::*;
@@ -585,7 +582,14 @@ pub(crate) async fn receive_imf_inner(
(rfc724_mid_orig, &self_addr),
)
.await?;
if !msg_has_pending_smtp_job(context, msg_id).await? {
if !context
.sql
.exists(
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
(rfc724_mid_orig,),
)
.await?
{
msg_id.set_delivered(context).await?;
}
return Ok(None);
@@ -723,7 +727,7 @@ pub(crate) async fn receive_imf_inner(
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
.unwrap_or_default();
let allow_creation = if mime_parser.decryption_error.is_some() {
let allow_creation = if mime_parser.decrypting_failed {
false
} else if is_dc_message == MessengerMessage::No
&& !context.get_config_bool(Config::IsChatmail).await?
@@ -1006,11 +1010,11 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
&& msg.chat_visibility == ChatVisibility::Archived;
updated_chats
.entry(msg.chat_id)
.and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
.or_insert((msg.timestamp_sort, msg.id));
.and_modify(|ts| *ts = cmp::max(*ts, msg.timestamp_sort))
.or_insert(msg.timestamp_sort);
}
}
for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
for (chat_id, timestamp_sort) in updated_chats {
context
.sql
.execute(
@@ -1019,13 +1023,12 @@ UPDATE msgs SET state=? WHERE
state=? AND
hidden=0 AND
chat_id=? AND
(timestamp,id)<(?,?)",
timestamp<?",
(
MessageState::InNoticed,
MessageState::InFresh,
chat_id,
timestamp_sort,
msg_id,
),
)
.await
@@ -1208,18 +1211,14 @@ async fn decide_chat_assignment(
{
info!(context, "Call state changed (TRASH).");
true
} else if let Some(ref decryption_error) = mime_parser.decryption_error
&& !mime_parser.incoming
{
} else if mime_parser.decrypting_failed && !mime_parser.incoming {
// Outgoing undecryptable message.
let last_time = context
.get_config_i64(Config::LastCantDecryptOutgoingMsgs)
.await?;
let now = tools::time();
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
let txt = format!(
"⚠️ 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. (Error: {decryption_error}, {rfc724_mid})."
);
let txt = "⚠️ 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.";
let mut msg = Message::new_text(txt.to_string());
chat::add_device_msg(context, None, Some(&mut msg))
.await
@@ -1836,16 +1835,6 @@ async fn add_parts(
}
}
// Sort message to the bottom if we are not in the chat
// so if we are added via QR code scan
// the message about our addition goes after all the info messages.
// Info messages are sorted by local smeared_timestamp()
// which advances quickly during SecureJoin,
// while "member added" message may have older timestamp
// corresponding to the sender clock.
// In practice inviter clock may even be slightly in the past.
let sort_to_bottom = !chat.is_self_in_chat(context).await?;
let is_location_kml = mime_parser.location_kml.is_some();
let mut group_changes = match chat.typ {
_ if chat.id.is_special() => GroupChangesInfo::default(),
@@ -1896,8 +1885,16 @@ async fn add_parts(
};
let in_fresh = state == MessageState::InFresh;
let sort_to_bottom = false;
let received = true;
let sort_timestamp = chat_id
.calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
.calc_sort_timestamp(
context,
mime_parser.timestamp_sent,
sort_to_bottom,
received,
mime_parser.incoming,
)
.await?;
// Apply ephemeral timer changes to the chat.
@@ -2293,7 +2290,7 @@ RETURNING id
if trash { 0 } else { ephemeral_timestamp },
if trash {
DownloadState::Done
} else if mime_parser.decryption_error.is_some() {
} else if mime_parser.decrypting_failed {
DownloadState::Undecipherable
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
DownloadState::Available
@@ -2357,7 +2354,7 @@ RETURNING id
info!(
context,
"Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
"Message has {icnt} parts and is assigned to chat #{chat_id}."
);
if !chat_id.is_trash() && !hidden {
@@ -2706,7 +2703,7 @@ async fn lookup_or_create_adhoc_group(
allow_creation: bool,
create_blocked: Blocked,
) -> Result<Option<(ChatId, Blocked, bool)>> {
if mime_parser.decryption_error.is_some() {
if mime_parser.decrypting_failed {
warn!(
context,
"Not creating ad-hoc group for message that cannot be decrypted."
@@ -2928,7 +2925,7 @@ async fn create_group(
if let Some(chat_id) = chat_id {
Ok(Some((chat_id, chat_id_blocked)))
} else if mime_parser.decryption_error.is_some() {
} else if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
// Chat-Group-Name, which is in the encrypted part, was
@@ -3123,18 +3120,17 @@ async fn apply_group_changes(
}
}
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
is_from_in_chat,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
if is_from_in_chat {
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
// Avoid insertion of `from_id` into a group with inappropriate encryption state.
if from_is_key_contact != chat.grpid.is_empty()
&& chat.member_list_is_stale(context).await?
@@ -3308,7 +3304,6 @@ async fn apply_chat_name_avatar_and_description_changes(
context: &Context,
mime_parser: &MimeMessage,
from_id: ContactId,
is_from_in_chat: bool,
chat: &mut Chat,
send_event_chat_modified: &mut bool,
better_msg: &mut Option<String>,
@@ -3337,8 +3332,7 @@ async fn apply_chat_name_avatar_and_description_changes(
let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if is_from_in_chat
&& (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
&& chat
.id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
@@ -3359,19 +3353,14 @@ async fn apply_chat_name_avatar_and_description_changes(
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
if is_from_in_chat {
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_name_changed(context, old_name, grpname)
} else {
stock_str::msg_grp_name(context, old_name, grpname, from_id).await
},
);
} else {
// Attempt to change group name by non-member, trash it.
*better_msg = Some(String::new());
}
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_name_changed(context, old_name, grpname)
} else {
stock_str::msg_grp_name(context, old_name, grpname, from_id).await
},
);
}
}
@@ -3394,8 +3383,7 @@ async fn apply_chat_name_avatar_and_description_changes(
let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
// To provide consistency, compare descriptions if timestamps are equal.
if is_from_in_chat
&& (old_timestamp, &old_description) < (new_timestamp, &new_description)
if (old_timestamp, &old_description) < (new_timestamp, &new_description)
&& chat
.id
.update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
@@ -3416,13 +3404,8 @@ async fn apply_chat_name_avatar_and_description_changes(
.get_header(HeaderDef::ChatGroupDescriptionChanged)
.is_some()
{
if is_from_in_chat {
better_msg
.get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
} else {
// Attempt to change group description by non-member, trash it.
*better_msg = Some(String::new());
}
better_msg
.get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
}
}
@@ -3432,46 +3415,39 @@ async fn apply_chat_name_avatar_and_description_changes(
&& value == "group-avatar-changed"
&& let Some(avatar_action) = &mime_parser.group_avatar
{
if is_from_in_chat {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_img_changed(context)
} else {
match avatar_action {
AvatarAction::Delete => {
stock_str::msg_grp_img_deleted(context, from_id).await
}
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_img_changed(context)
} else {
match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
},
);
} else {
// Attempt to change group avatar by non-member, trash it.
*better_msg = Some(String::new());
}
}
},
);
}
if let Some(avatar_action) = &mime_parser.group_avatar
&& is_from_in_chat
&& chat
if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "Group-avatar change for {}.", chat.id);
if chat
.param
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
{
info!(context, "Group-avatar change for {}.", chat.id);
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
*send_event_chat_modified = true;
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
*send_event_chat_modified = true;
}
}
Ok(())
@@ -3585,14 +3561,7 @@ async fn create_or_lookup_mailinglist_or_broadcast(
chattype,
&listid,
name,
if chattype == Chattype::InBroadcast {
// If we joined the broadcast, we have scanned a QR code.
// Even if 1:1 chat does not exist or is in a contact request,
// create the channel as unblocked.
Blocked::Not
} else {
create_blocked
},
create_blocked,
param,
mime_parser.timestamp_sent,
)
@@ -3778,12 +3747,10 @@ async fn apply_out_broadcast_changes(
let mut added_removed_id: Option<ContactId> = None;
if from_id == ContactId::SELF {
let is_from_in_chat = true;
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
is_from_in_chat,
chat,
&mut send_event_chat_modified,
&mut better_msg,
@@ -3872,12 +3839,10 @@ async fn apply_in_broadcast_changes(
let mut send_event_chat_modified = false;
let mut better_msg = None;
let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
is_from_in_chat,
chat,
&mut send_event_chat_modified,
&mut better_msg,

View File

@@ -13,10 +13,9 @@ use crate::constants::DC_GCL_FOR_FORWARDING;
use crate::contact;
use crate::imap::prefetch_should_download;
use crate::imex::{ImexMode, imex};
use crate::key;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::tools::{SystemTime, time};
@@ -820,12 +819,9 @@ async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
let received_msg = receive_imf(context, imf_raw, false)
.await
.expect("receive_imf failure")
.expect("No message received");
assert_eq!(received_msg.msg_ids.len(), 1);
let msg_id = received_msg.msg_ids[0];
receive_imf(context, imf_raw, false).await.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
Message::load_from_db(context, msg_id).await.unwrap()
}
@@ -2876,8 +2872,9 @@ async fn test_rfc1847_encapsulation() -> Result<()> {
// Alice sends a message to Bob using Thunderbird.
let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml");
receive_imf(bob, raw, false).await?;
let msg = load_imf_email(bob, raw).await;
let msg = bob.get_last_msg().await;
assert!(msg.get_showpadlock());
Ok(())
@@ -3085,8 +3082,8 @@ async fn test_auto_accept_for_bots() -> Result<()> {
async fn test_auto_accept_group_for_bots() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::Bot, Some("1")).await.unwrap();
let msg = load_imf_email(&t, GRP_MAIL).await;
receive_imf(&t, GRP_MAIL, false).await?;
let msg = t.get_last_msg().await;
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(!chat.is_contact_request());
Ok(())
@@ -3330,7 +3327,7 @@ async fn test_outgoing_undecryptable() -> Result<()> {
assert!(
dev_msg
.text
.starts_with("⚠️ 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. (Error:")
.contains("⚠️ 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.")
);
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
@@ -3559,9 +3556,9 @@ async fn test_messed_up_message_id() -> Result<()> {
let t = TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/messed_up_message_id.eml");
let msg = load_imf_email(&t, raw).await;
receive_imf(&t, raw, false).await?;
assert_eq!(
msg.rfc724_mid,
t.get_last_msg().await.rfc724_mid,
"0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org"
);
@@ -4378,42 +4375,39 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_group(alice, "Group").await?;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group(&alice, "Group").await?;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
alice.add_or_lookup_contact_id(&bob).await,
)
.await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
let fiona = &tcm.fiona().await;
let fiona = TestContext::new_fiona().await;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(fiona).await,
alice.add_or_lookup_contact_id(&fiona).await,
)
.await?;
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
fiona_chat_id.accept(fiona).await?;
fiona_chat_id.accept(&fiona).await?;
SystemTime::shift(Duration::from_secs(60));
chat::set_chat_name(fiona, fiona_chat_id, "Renamed").await?;
// Message about chat name change from non-member is trashed.
bob.recv_msg_trash(&fiona.pop_sent_msg().await).await;
chat::set_chat_name(&fiona, fiona_chat_id, "Renamed").await?;
bob.recv_msg(&fiona.pop_sent_msg().await).await;
// Bob missed the message adding fiona, but mustn't recreate the member list or apply the group
// name change.
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = bob.add_or_lookup_contact_id(alice).await;
assert!(is_contact_in_chat(bob, bob_chat_id, bob_alice_contact).await?);
let chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert_eq!(chat.get_name(), "Group");
Ok(())
}
@@ -5366,46 +5360,6 @@ async fn test_outgoing_unencrypted_chat_assignment() {
assert_eq!(received.chat_id, chat.id);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_incoming_reply_with_date_in_past() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let msg0 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <message@example.net>\n\
Date: Sun, 22 Mar 2020 22:22:22 +0000\n\
\n\
This device has an atomic clock\n",
false,
)
.await?
.unwrap();
let msg1 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <message1@example.net>\n\
In-Reply-To: <message@example.net>\n\
Date: Sun, 22 Mar 2020 11:11:11 +0000\n\
\n\
And this one has a wind-up clock\n",
false,
)
.await?
.unwrap();
assert_eq!(msg1.chat_id, msg0.chat_id);
assert!(msg1.sort_timestamp >= msg0.sort_timestamp);
assert_eq!(
alice.get_last_msg_in(msg0.chat_id).await.id,
*msg1.msg_ids.last().unwrap()
);
Ok(())
}
/// Tests Bob receiving a message from Alice
/// in a new group she just created
/// with only Alice and Bob.
@@ -5605,90 +5559,3 @@ async fn test_calendar_alternative() -> Result<()> {
Ok(())
}
/// Tests that outgoing encrypted messages are detected
/// by verifying own signature, completely ignoring From address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_determined_by_signature() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// alice_dev2: same key, different address.
let different_from = "very@different.from";
assert!(!alice.is_self_addr(different_from).await?);
let alice_dev2 = &tcm.unconfigured().await;
alice_dev2.configure_addr(different_from).await;
key::store_self_keypair(alice_dev2, &alice_keypair()).await?;
assert_ne!(
alice.get_config(Config::Addr).await?.unwrap(),
different_from
);
// Send message from alice_dev2 and check alice sees it as outgoing
let chat_id = alice_dev2.create_chat_id(bob).await;
let sent_msg = alice_dev2.send_text(chat_id, "hello from new device").await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.state, MessageState::OutDelivered);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mark_message_as_delivered_only_after_sent_out_fully() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice.set_config_bool(Config::BccSelf, true).await?;
let alice_chat_id = alice.create_chat_id(bob).await;
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(alice, "a.jpg", file_bytes, None)?;
let msg_id = chat::send_msg(alice, alice_chat_id, &mut msg)
.await
.unwrap();
let (pre_msg_id, pre_msg_payload) = first_row_in_smtp_queue(alice).await;
assert_eq!(msg_id, pre_msg_id);
assert!(pre_msg_payload.len() < file_bytes.len());
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
// Alice receives her own pre-message because of bcc_self
// This should not yet mark the message as delivered,
// because not everything was sent,
// but it does remove the pre-message from the SMTP queue
receive_imf(alice, pre_msg_payload.as_bytes(), false).await?;
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
let (post_msg_id, post_msg_payload) = first_row_in_smtp_queue(alice).await;
assert_eq!(msg_id, post_msg_id);
assert!(post_msg_payload.len() > file_bytes.len());
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
// Alice receives her own post-message because of bcc_self
// This should now mark the message as delivered,
// because everything was sent by now.
receive_imf(alice, post_msg_payload.as_bytes(), false).await?;
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
Ok(())
}
/// Queries the first sent message in the SMTP queue
/// without removing it from the SMTP queue.
/// This simulates the case that a message is successfully sent out,
/// but the 'OK' answer from the server doesn't arrive,
/// so that the SMTP row stays in the database.
pub(crate) async fn first_row_in_smtp_queue(alice: &TestContext) -> (MsgId, String) {
alice
.sql
.query_row_optional("SELECT msg_id, mime FROM smtp ORDER BY id", (), |row| {
let msg_id: MsgId = row.get(0)?;
let mime: String = row.get(1)?;
Ok((msg_id, mime))
})
.await
.expect("query_row_optional failed")
.expect("No SMTP row found")
}

View File

@@ -450,8 +450,10 @@ pub(crate) async fn handle_securejoin_handshake(
) {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for key in mime_message.gossiped_keys.values() {
if key.public_key.dc_fingerprint() == self_fingerprint {
for (addr, key) in &mime_message.gossiped_keys {
if key.public_key.dc_fingerprint() == self_fingerprint
&& context.is_self_addr(addr).await?
{
self_found = true;
break;
}
@@ -839,6 +841,13 @@ pub(crate) async fn observe_securejoin_on_other_device(
inviter_progress(context, contact_id, chat_id, chat_type)?;
}
if matches!(step, SecureJoinStep::RequestWithAuth) {
// This actually reflects what happens on the first device (which does the secure
// join) and causes a subsequent "vg-member-added" message to create an unblocked
// verified group.
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
}
if matches!(step, SecureJoinStep::MemberAdded) {
Ok(HandshakeMessage::Propagate)
} else {

View File

@@ -465,7 +465,11 @@ pub(crate) async fn send_msg_to_smtp(
match status {
SendResult::Retry => Err(format_err!("Retry")),
SendResult::Success => {
if !msg_has_pending_smtp_job(context, msg_id).await? {
if !context
.sql
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
.await?
{
msg_id.set_delivered(context).await?;
}
Ok(())
@@ -474,16 +478,6 @@ pub(crate) async fn send_msg_to_smtp(
}
}
pub(crate) async fn msg_has_pending_smtp_job(
context: &Context,
msg_id: MsgId,
) -> Result<bool, Error> {
context
.sql
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
.await
}
/// Attempts to send queued MDNs.
async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
loop {

View File

@@ -883,24 +883,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
.log_err(context)
.ok();
// Cleanup `imap` and `imap_sync` entries for deleted transports.
//
// Transports may be deleted directly or via sync messages,
// so it is easier to cleanup orphaned entries in a single place.
context
.sql
.execute(
"DELETE FROM imap WHERE transport_id NOT IN (SELECT transports.id FROM transports)",
(),
)
.await
.log_err(context)
.ok();
context.sql.execute(
"DELETE FROM imap_sync WHERE transport_id NOT IN (SELECT transports.id FROM transports)",
(),
).await.log_err(context).ok();
// Delete POI locations
// which don't have corresponding message.
delete_orphaned_poi_locations(context)

View File

@@ -16,6 +16,7 @@ use crate::constants::ShowEmails;
use crate::context::Context;
use crate::key::DcKey;
use crate::log::warn;
use crate::message::MsgId;
use crate::provider::get_provider_info;
use crate::sql::Sql;
use crate::tools::{Time, inc_and_check, time_elapsed};
@@ -733,6 +734,12 @@ impl Sql {
Ok(())
}
async fn set_db_version_in_cache(&self, version: i32) -> Result<()> {
let mut lock = self.config_cache.write().await;
lock.insert(VERSION_CFG.to_string(), Some(format!("{version}")));
Ok(())
}
async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
self.execute_migration_transaction(
|transaction| {
@@ -1605,11 +1612,51 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
.await?;
}
// Migration 108 is removed, it was using high level code
// to split SMTP queue messages into chunks with smaller number of recipients
// and started to fail later as high level code started
// expecting `transports` table that is only added in future migrations.
// Migration 108 was not changing the database schema.
if dbversion < 108 {
let version = 108;
let chunk_size = context.get_max_smtp_rcpt_to().await?;
sql.transaction(move |trans| {
Sql::set_db_version_trans(trans, version)?;
let id_max =
trans.query_row("SELECT IFNULL((SELECT MAX(id) FROM smtp), 0)", (), |row| {
let id_max: i64 = row.get(0)?;
Ok(id_max)
})?;
while let Some((id, rfc724_mid, mime, msg_id, recipients, retries)) = trans
.query_row(
"SELECT id, rfc724_mid, mime, msg_id, recipients, retries FROM smtp \
WHERE id<=? LIMIT 1",
(id_max,),
|row| {
let id: i64 = row.get(0)?;
let rfc724_mid: String = row.get(1)?;
let mime: String = row.get(2)?;
let msg_id: MsgId = row.get(3)?;
let recipients: String = row.get(4)?;
let retries: i64 = row.get(5)?;
Ok((id, rfc724_mid, mime, msg_id, recipients, retries))
},
)
.optional()?
{
trans.execute("DELETE FROM smtp WHERE id=?", (id,))?;
let recipients = recipients.split(' ').collect::<Vec<_>>();
for recipients in recipients.chunks(chunk_size) {
let recipients = recipients.join(" ");
trans.execute(
"INSERT INTO smtp (rfc724_mid, mime, msg_id, recipients, retries) \
VALUES (?, ?, ?, ?, ?)",
(&rfc724_mid, &mime, msg_id, recipients, retries),
)?;
}
}
Ok(())
})
.await
.with_context(|| format!("migration failed for version {version}"))?;
sql.set_db_version_in_cache(version).await?;
}
if dbversion < 109 {
sql.execute_migration(

View File

@@ -554,6 +554,7 @@ async fn get_message_stats(context: &Context) -> Result<BTreeMap<Chattype, Messa
}
pub(crate) async fn update_message_stats(context: &Context) -> Result<()> {
info!(context, "Updating message statistics.");
for chattype in [Chattype::Single, Chattype::Group, Chattype::OutBroadcast] {
update_message_stats_inner(context, chattype).await?;
}

View File

@@ -40,7 +40,6 @@ use crate::message::{Message, MessageState, MsgId, update_msg_state};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::smtp::msg_has_pending_smtp_job;
use crate::stock_str::StockStrings;
use crate::tools::time;
@@ -659,7 +658,10 @@ impl TestContext {
.execute("DELETE FROM smtp WHERE id=?;", (rowid,))
.await
.expect("failed to remove job");
if !msg_has_pending_smtp_job(self, msg_id)
if !self
.ctx
.sql
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
.await
.expect("Failed to check for more jobs")
{
@@ -713,8 +715,7 @@ impl TestContext {
}
pub async fn get_smtp_rows_for_msg<'a>(&'a self, msg_id: MsgId) -> Vec<SentMessage<'a>> {
let sent_msgs = self
.ctx
self.ctx
.sql
.query_map_vec(
"SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=?",
@@ -736,23 +737,7 @@ impl TestContext {
sender_context: &self.ctx,
recipients,
})
.collect();
self.ctx
.sql
.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))
.await
.expect("Delete smtp jobs");
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("Update message state");
self.sql
.execute(
"UPDATE msgs SET timestamp_sent=? WHERE id=?",
(time(), msg_id),
)
.await
.expect("Update timestamp_sent");
sent_msgs
.collect()
}
/// Parses a message.

View File

@@ -56,30 +56,26 @@ async fn test_sending_pre_message() -> Result<()> {
.is_some()
);
let post_rfc724_mid = post_message_parsed
.headers
.get_header_value(HeaderDef::MessageId);
assert_eq!(
post_rfc724_mid,
post_message_parsed
.headers
.get_header_value(HeaderDef::MessageId),
Some(format!("<{}>", msg.rfc724_mid)),
"Post-Message should have the rfc message id of the database message"
);
let pre_rfc724_mid = pre_message_parsed
.headers
.get_header_value(HeaderDef::MessageId);
assert_ne!(
pre_rfc724_mid, post_rfc724_mid,
pre_message_parsed
.headers
.get_header_value(HeaderDef::MessageId),
post_message_parsed
.headers
.get_header_value(HeaderDef::MessageId),
"message ids of Pre-Message and Post-Message should be different"
);
assert_eq!(
pre_rfc724_mid,
Some(format!("<{}>", msg.pre_rfc724_mid)),
"Unexpected pre-message RFC 724 ID"
);
let decrypted_post_message = bob.parse_msg(post_message).await;
assert!(decrypted_post_message.decryption_error.is_none());
assert_eq!(decrypted_post_message.decrypting_failed, false);
assert_eq!(
decrypted_post_message.header_exists(HeaderDef::ChatPostMessageId),
false
@@ -90,7 +86,9 @@ async fn test_sending_pre_message() -> Result<()> {
decrypted_pre_message
.get_header(HeaderDef::ChatPostMessageId)
.map(String::from),
post_rfc724_mid,
post_message_parsed
.headers
.get_header_value(HeaderDef::MessageId)
);
assert!(
pre_message_parsed
@@ -100,25 +98,6 @@ async fn test_sending_pre_message() -> Result<()> {
"no Chat-Post-Message-ID header in unprotected headers of Pre-Message"
);
chat::resend_msgs(alice, &[msg_id]).await?;
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
assert_eq!(smtp_rows.len(), 2);
let pre_message_parsed = mailparse::parse_mail(smtp_rows[0].payload.as_bytes())?;
assert_eq!(
pre_message_parsed
.headers
.get_header_value(HeaderDef::MessageId),
pre_rfc724_mid
);
let post_message_parsed = mailparse::parse_mail(smtp_rows[1].payload.as_bytes())?;
assert_eq!(
post_message_parsed
.headers
.get_header_value(HeaderDef::MessageId),
post_rfc724_mid
);
Ok(())
}

View File

@@ -246,6 +246,46 @@ async fn test_old_message_4() -> Result<()> {
Ok(())
}
/// Alice is offline for some time.
/// When they come online, first their mvbox is synced and then their inbox.
/// This test tests that the messages are still in the right order.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_5() -> Result<()> {
let alice = TestContext::new_alice().await;
let msg_sent = receive_imf(
&alice,
b"From: alice@example.org\n\
To: Bob <bob@example.net>\n\
Message-ID: <1234-2-4@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Happy birthday, Bob!\n",
true,
)
.await?
.unwrap();
let msg_incoming = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sun, 07 Dec 2019 19:00:26 +0000\n\
\n\
Happy birthday to me, Alice!\n",
false,
)
.await?
.unwrap();
assert!(msg_sent.sort_timestamp == msg_incoming.sort_timestamp);
alice
.golden_test_chat(msg_sent.chat_id, "test_old_message_5")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -21,6 +21,7 @@ pub use std::time::SystemTime as Time;
#[cfg(not(test))]
pub use std::time::SystemTime;
use crate::log::LogExt as _;
use anyhow::{Context as _, Result, bail, ensure};
use base64::Engine as _;
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
@@ -248,6 +249,8 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
true,
)
.await
.context("Failed to add bad time warning")
.log_err(context)
.ok();
} else {
warn!(context, "Can't convert current timestamp");

View File

@@ -64,11 +64,9 @@ DKIM Results: Passed=true";
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
let t = TestContext::new_alice().await;
let received = receive_imf(&t, raw, false).await.unwrap().unwrap();
assert_eq!(received.msg_ids.len(), 1);
let msg_id = received.msg_ids[0];
let msg_info = msg_id.get_info(&t).await.unwrap();
receive_imf(&t, raw, false).await.unwrap();
let msg = t.get_last_msg().await;
let msg_info = msg.id.get_info(&t).await.unwrap();
// Ignore the first rows of the msg_info because they contain a
// received time that depends on the test time which makes it impossible to

Binary file not shown.

View File

@@ -2,9 +2,9 @@ Group#Chat#1001: Group [5 member(s)]
--------------------------------------------------------------------------------
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1002🔒: Me (Contact#Contact#Self): populate √
Msg#1007🔒: (Contact#Contact#1001): Member fiona@example.net removed by bob@example.net. [FRESH][INFO]
Msg#1003: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO]
Msg#1004: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO]
Msg#1005🔒: (Contact#Contact#1001): Member elena@example.net added by bob@example.net. [FRESH][INFO]
Msg#1006🔒: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o
Msg#1007🔒: (Contact#Contact#1001): Member fiona@example.net removed by bob@example.net. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
Single#Chat#1001: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
--------------------------------------------------------------------------------
Msg#1002: Me (Contact#Contact#Self): I'm Alice too √
Msg#1001: Me (Contact#Contact#Self): We share this account √
Msg#1002: Me (Contact#Contact#Self): I'm Alice too √
--------------------------------------------------------------------------------

View File

@@ -0,0 +1,5 @@
Single#Chat#11001: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
--------------------------------------------------------------------------------
Msg#11001: Me (Contact#Contact#Self): Happy birthday, Bob! √
Msg#11002: (Contact#Contact#11001): Happy birthday to me, Alice! [FRESH]
--------------------------------------------------------------------------------

View File

@@ -1,7 +1,7 @@
OutBroadcast#Chat#1001: Channel [0 member(s)]
--------------------------------------------------------------------------------
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1008🔒: Me (Contact#Contact#Self): hi √
Msg#1006🔒: Me (Contact#Contact#Self): Member bob@example.net added. [INFO] √
Msg#1008🔒: Me (Contact#Contact#Self): hi √
Msg#1009🔒: Me (Contact#Contact#Self): You removed member bob@example.net. [INFO] √
--------------------------------------------------------------------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB