Compare commits

..

2 Commits

Author SHA1 Message Date
iequidoo
b4a2360175 fix: Receiving of re-sent locally deleted messages (#7115)
Before, locally deleted messages (because of DeleteDeviceAfter set or external deletion requests)
couldn't be received if they're re-sent because tombstones prevented that forever. This led to
locally deleted webxdcs being unrecoverable.
2025-10-12 15:01:12 -03:00
iequidoo
f0144f8789 fix: Prune tombstone when a message isn't expected to appear on IMAP anymore (#7115)
This allows to receive deleted messages again if they're re-sent:
- After a manual deletion,
- If both DeleteServerAfter and DeleteDeviceAfter are set,

w/o waiting for 2 days when stale tombstones are GC-ed. This is particularly useful for webxdc. If
only DeleteDeviceAfter is set though, this changes nothing and maybe a separate fix is needed.

This may also greately reduce the db size for some bots which receive and immediately delete many
messages.

Also insert a tombstone to the db if a deletion request (incl. sync messages) can't find
`rfc724_mid`. This may happen in case of message reordering and the deleted message mustn't appear
when it finally arrives.
2025-10-12 15:01:09 -03:00
48 changed files with 329 additions and 816 deletions

View File

@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -109,7 +109,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -136,7 +136,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v5

View File

@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -104,5 +104,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary

View File

@@ -36,7 +36,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -55,7 +55,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

View File

@@ -25,7 +25,7 @@ jobs:
run: uvx zizmor --format sarif . > results.sarif
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -1,77 +1,5 @@
# Changelog
## [2.21.0] - 2025-10-16
### Build system
- nix: Remove unused dependencies.
### Features / Changes
- TLS 1.3 session resumption.
- REPL: Add send-sync command.
- Set `User-Agent` for tile.openstreetmap.org requests.
- Cache tile.openstreetmap.org tiles for 7 days.
### Fixes
- Remove Exif with non-fatal errors from images.
- jsonrpc: Use Core's logic for computing VcardContact.color ([#7294](https://github.com/chatmail/core/pull/7294)).
### Miscellaneous Tasks
- deps: Bump cachix/install-nix-action from 31.7.0 to 31.8.0.
- cargo: Bump async_zip from 0.0.17 to 0.0.18 ([#7257](https://github.com/chatmail/core/pull/7257)).
- deps: Bump github/codeql-action from 3 to 4 ([#7304](https://github.com/chatmail/core/pull/7304)).
### Refactor
- Use rustls reexported from tokio_rustls.
- Pass ALPN around as &str.
- mimeparser: Store only one signature fingerprint.
### Tests
- Test expiration of ephemeral messages with unknown viewtype.
- Test expiration of non-ephemeral message with unknown viewtype.
## [2.20.0] - 2025-10-13
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
when trying to delete a message with unknown viewtype.
### Fixes
- Accept unknown viewtype in ephemeral loop.
- Accept unknown viewtype in delete-old-messages loop.
## [2.19.0] - 2025-10-12
### Features / Changes
- Slightly increase saturation of colors.
### Fixes
- Do not fail to receive call accepted/ended messages referring to non-call Message-ID.
- Do not fail to fully download previously trashed messages.
- Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that ([#7296](https://github.com/chatmail/core/pull/7296)).
- Do not try to process calls from partial messages.
### CI
- Update to Python 3.14.
### Refactor
- Use variables directly in formatted strings ([#7284](https://github.com/chatmail/core/pull/7284)).
- Set_chat_profile_image(): Remove !chat.is_mailing_list() check.
### Miscellaneous Tasks
- cargo: Bump quick-xml from 0.37.5 to 0.38.3.
- Add nodejs to nix dev env ([#7283](https://github.com/chatmail/core/pull/7283))
## [2.18.0] - 2025-10-08
### API-Changes
@@ -6954,6 +6882,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.16.0]: https://github.com/chatmail/core/compare/v2.15.0..v2.16.0
[2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0

17
Cargo.lock generated
View File

@@ -346,15 +346,15 @@ dependencies = [
[[package]]
name = "async_zip"
version = "0.0.18"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.17",
"thiserror 1.0.69",
"tokio",
"tokio-util",
]
@@ -1289,7 +1289,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.21.0"
version = "2.18.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1346,6 +1346,7 @@ dependencies = [
"ratelimit",
"regex",
"rusqlite",
"rustls",
"rustls-pki-types",
"sanitize-filename",
"sdp",
@@ -1398,7 +1399,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.21.0"
version = "2.18.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1420,7 +1421,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.21.0"
version = "2.18.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1436,7 +1437,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.21.0"
version = "2.18.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1465,7 +1466,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.21.0"
version = "2.18.0"
dependencies = [
"anyhow",
"deltachat",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.21.0"
version = "2.18.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -47,7 +47,7 @@ async-channel = { workspace = true }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
@@ -87,6 +87,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
sdp = "0.8.0"
serde_json = { workspace = true }

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result};
use anyhow::Result;
use deltachat::calls::{call_state, sdp_has_video, CallState};
use deltachat::context::Context;
@@ -26,9 +26,7 @@ pub struct JsonrpcCallInfo {
impl JsonrpcCallInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
format!("Attempting to get call state of non-call message {msg_id}")
})?;
let call_info = context.load_call_by_id(msg_id).await?;
let sdp_offer = call_info.place_call_info.clone();
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use deltachat::key::{DcKey, SignedPublicKey};
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -130,13 +130,7 @@ pub struct VcardContact {
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let is_self = false;
let fpr = vc.key.as_deref().and_then(|k| {
SignedPublicKey::from_base64(k)
.ok()
.map(|k| k.dc_fingerprint())
});
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,

View File

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

View File

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

View File

@@ -358,7 +358,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-sync <text>\n\
sendempty\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
@@ -909,23 +908,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
}
"send-sync" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No message text given.");
// Send message over a dedicated SMTP connection
// and measure time.
//
// This can be used to benchmark SMTP connection establishment.
let time_start = std::time::Instant::now();
let msg = format!("{arg1} {arg2}");
let mut msg = Message::new_text(msg);
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
let time_needed = time_start.elapsed();
println!("Sent message in {time_needed:?}.");
}
"sendempty" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;

View File

@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 39] = [
const CHAT_COMMANDS: [&str; 38] = [
"listchats",
"listarchived",
"start-realtime",
@@ -199,7 +199,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"dellocations",
"getlocations",
"send",
"send-sync",
"sendempty",
"sendimage",
"sendsticker",

View File

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

View File

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

View File

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

View File

@@ -98,6 +98,9 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
@@ -480,6 +483,12 @@
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \

View File

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

View File

@@ -1 +1 @@
2025-10-16
2025-10-08

View File

@@ -537,11 +537,7 @@ fn file_hash(src: &Path) -> Result<blake3::Hash> {
fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new()
.continue_on_error(true)
.read_from_container(&mut bufreader)
.or_else(|e| e.distill_partial_result(|_errors| {}))
.ok();
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}

View File

@@ -334,28 +334,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_bad_exif() {
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
// detected and removed.
let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
has_exif: true,
original_width: 1000,
original_height: 1000,
compressed_width: 1000,
compressed_height: 1000,
..Default::default()
}
.test()
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../../test-data/image/screenshot.png");
@@ -440,7 +418,7 @@ async fn test_recode_image_balanced_png() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_with_exif() {
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
let bytes = include_bytes!("../../test-data/image/logo.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
bytes,

View File

@@ -213,9 +213,7 @@ impl Context {
call_id: MsgId,
accept_call_info: String,
) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
format!("accept_incoming_call is called with {call_id} which does not refer to a call")
})?;
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
ensure!(call.is_incoming());
if call.is_accepted() || call.is_ended() {
info!(self, "Call already accepted/ended");
@@ -250,9 +248,7 @@ impl Context {
/// Cancel, decline or hangup an incoming or outgoing call.
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
format!("end_call is called with {call_id} which does not refer to a call")
})?;
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
if call.is_ended() {
info!(self, "Call already ended");
return Ok(());
@@ -295,13 +291,7 @@ impl Context {
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
);
return Ok(());
};
let mut call = context.load_call_by_id(call_id).await?;
if !call.is_accepted() && !call.is_ended() {
if call.is_incoming() {
call.mark_as_canceled(&context).await?;
@@ -326,10 +316,7 @@ impl Context {
from_id: ContactId,
) -> Result<()> {
if mime_message.is_call() {
let Some(call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
let call = self.load_call_by_id(call_id).await?;
if call.is_incoming() {
if call.is_stale() {
@@ -365,11 +352,7 @@ impl Context {
} else {
match mime_message.is_system_message {
SystemMessage::CallAccepted => {
let Some(mut call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
let mut call = self.load_call_by_id(call_id).await?;
if call.is_ended() || call.is_accepted() {
info!(self, "CallAccepted received for accepted/ended call");
return Ok(());
@@ -394,11 +377,7 @@ impl Context {
}
}
SystemMessage::CallEnded => {
let Some(mut call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
let mut call = self.load_call_by_id(call_id).await?;
if call.is_ended() {
// may happen eg. if a a message is missed
info!(self, "CallEnded received for ended call");
@@ -442,26 +421,15 @@ impl Context {
}
/// Loads information about the call given its ID.
///
/// If the message referred to by ID is
/// not a call message, returns `None`.
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<CallInfo> {
let call = Message::load_from_db(self, call_id).await?;
Ok(self.load_call_by_message(call))
self.load_call_by_message(call)
}
// Loads information about the call given the `Message`.
//
// If the `Message` is not a call message, returns `None`
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
if call.viewtype != Viewtype::Call {
// This can happen e.g. if a "call accepted"
// or "call ended" message is received
// with `In-Reply-To` referring to non-call message.
return None;
}
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
ensure!(call.viewtype == Viewtype::Call);
Some(CallInfo {
Ok(CallInfo {
place_call_info: call
.param
.get(Param::WebrtcRoom)
@@ -529,13 +497,8 @@ pub enum CallState {
}
/// Returns call state given the message ID.
///
/// Returns an error if the message is not a call message.
pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
let call = context
.load_call_by_id(msg_id)
.await?
.with_context(|| format!("{msg_id} is not a call message"))?;
let call = context.load_call_by_id(msg_id).await?;
let state = if call.is_incoming() {
if call.is_accepted() {
if call.is_ended() {

View File

@@ -1,8 +1,6 @@
use super::*;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::test_utils::{TestContext, TestContextManager};
struct CallSetup {
@@ -55,10 +53,7 @@ async fn setup_call() -> Result<CallSetup> {
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
let info = t
.load_call_by_id(m.id)
.await?
.expect("m should be a call message");
let info = t.load_call_by_id(m.id).await?;
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
@@ -76,10 +71,7 @@ async fn setup_call() -> Result<CallSetup> {
t.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
.await;
let info = t
.load_call_by_id(m.id)
.await?
.expect("IncomingCall event should refer to a call message");
let info = t.load_call_by_id(m.id).await?;
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
@@ -119,10 +111,7 @@ async fn accept_call() -> Result<CallSetup> {
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let sent2 = bob.pop_sent_msg().await;
let info = bob
.load_call_by_id(bob_call.id)
.await?
.expect("bob_call should be a call message");
let info = bob.load_call_by_id(bob_call.id).await?;
assert!(info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
@@ -132,10 +121,7 @@ async fn accept_call() -> Result<CallSetup> {
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let info = bob2
.load_call_by_id(bob2_call.id)
.await?
.expect("bob2_call should be a call message");
let info = bob2.load_call_by_id(bob2_call.id).await?;
assert!(info.is_accepted());
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
@@ -154,10 +140,7 @@ async fn accept_call() -> Result<CallSetup> {
accept_call_info: ACCEPT_INFO.to_string()
}
);
let info = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
let info = alice.load_call_by_id(alice_call.id).await?;
assert!(info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
@@ -477,20 +460,14 @@ async fn test_mark_calls() -> Result<()> {
alice, alice_call, ..
} = setup_call().await?;
let mut call_info: CallInfo = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
assert!(!call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_accepted(&alice).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
let mut call_info: CallInfo = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
@@ -507,10 +484,7 @@ async fn test_update_call_text() -> Result<()> {
alice, alice_call, ..
} = setup_call().await?;
let call_info = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
let call_info = alice.load_call_by_id(alice_call.id).await?;
call_info.update_text(&alice, "foo bar").await?;
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
@@ -555,114 +529,3 @@ async fn test_forward_call() -> Result<()> {
Ok(())
}
/// Tests that "end call" message referring
/// to a text message does not make receive_imf fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_end_text_call() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let received1 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
Chat-Version: 1.0\n\
\n\
Hello\n",
false,
)
.await?
.unwrap();
assert_eq!(received1.msg_ids.len(), 1);
let msg = Message::load_from_db(alice, received1.msg_ids[0])
.await
.unwrap();
assert_eq!(msg.viewtype, Viewtype::Text);
// Receiving "Call ended" message that refers
// to the text message does not result in an error.
let received2 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n",
false,
)
.await?
.unwrap();
assert_eq!(received2.msg_ids.len(), 1);
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
Ok(())
}
/// Tests that partially downloaded "call ended"
/// messages are not processed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_partial_calls() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let seen = false;
// The messages in the test
// have no `Date` on purpose,
// so they are treated as new.
let received_call = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call\n\
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
\n\
Hello, this is a call\n",
seen,
)
.await?
.unwrap();
assert_eq!(received_call.msg_ids.len(), 1);
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
.await
.unwrap();
assert_eq!(call_msg.viewtype, Viewtype::Call);
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
let imf_raw = b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n";
receive_imf_from_inbox(
alice,
"second@example.net",
imf_raw,
seen,
Some(imf_raw.len().try_into().unwrap()),
)
.await?;
// The call is still not ended.
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
// Fully downloading the message ends the call.
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
.await
.context("Failed to fully download end call message")?;
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
Ok(())
}

View File

@@ -36,7 +36,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -1574,10 +1574,17 @@ impl Contact {
Ok(None)
}
/// Returns a color for the contact.
/// See [`self::get_color`].
/// Get a color for the contact.
/// The color is calculated from the contact's fingerprint (for key-contacts)
/// or email address (for address-contacts) and can be used
/// for an fallback avatar with white initials
/// as well as for headlines in bubbles of group chats.
pub fn get_color(&self) -> u32 {
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
if let Some(fingerprint) = self.fingerprint() {
str_to_color(&fingerprint.hex())
} else {
str_to_color(&self.addr.to_lowercase())
}
}
/// Gets the contact's status.
@@ -1673,21 +1680,6 @@ impl Contact {
}
}
/// Returns a color for a contact having given attributes.
///
/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for
/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback
/// avatar with white initials as well as for headlines in bubbles of group chats.
pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 {
if let Some(fingerprint) = fingerprint {
str_to_color(&fingerprint.hex())
} else if is_self {
0x808080
} else {
str_to_color(&to_lowercase(addr))
}
}
// Updates the names of the chats which use the contact name.
//
// This is one of the few duplicated data, however, getting the chat list is easier this way.

View File

@@ -4,7 +4,6 @@ use super::*;
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
#[test]
@@ -774,20 +773,6 @@ async fn test_contact_get_color() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_color_vs_key() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.unconfigured().await;
t.configure_addr("alice@example.org").await;
assert!(t.is_configured().await?);
let color = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
assert_eq!(color, 0x808080);
get_securejoin_qr(t, None).await?;
let color1 = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
assert_ne!(color1, color);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_contact_get_encrinfo() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -30,7 +30,6 @@ use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::net::tls::TlsSessionStore;
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::push::PushSubscriber;
@@ -298,9 +297,6 @@ pub struct InnerContext {
/// True if account has subscribed to push notifications via IMAP.
pub(crate) push_subscribed: AtomicBool,
/// TLS session resumption cache.
pub(crate) tls_session_store: TlsSessionStore,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
@@ -479,7 +475,6 @@ impl Context {
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
tls_session_store: TlsSessionStore::new(),
iroh: Arc::new(RwLock::new(None)),
self_fingerprint: OnceLock::new(),
connectivities: parking_lot::Mutex::new(Vec::new()),

View File

@@ -376,10 +376,6 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
/// For each message a row ID, chat id, viewtype and location ID is returned.
///
/// Unknown viewtypes are returned as `Viewtype::Unknown`
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source)
async fn select_expired_messages(
context: &Context,
now: i64,
@@ -399,11 +395,7 @@ WHERE
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let viewtype: Viewtype = row
.get("type")
.context("Using default viewtype for ephemeral handling.")
.log_err(context)
.unwrap_or_default();
let viewtype: Viewtype = row.get("type")?;
let location_id: u32 = row.get("location_id")?;
Ok((id, chat_id, viewtype, location_id))
},
@@ -445,11 +437,7 @@ WHERE
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let viewtype: Viewtype = row
.get("type")
.context("Using default viewtype for delete-old handling.")
.log_err(context)
.unwrap_or_default();
let viewtype: Viewtype = row.get("type")?;
let location_id: u32 = row.get("location_id")?;
Ok((id, chat_id, viewtype, location_id))
},

View File

@@ -826,68 +826,3 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
Ok(())
}
/// Tests that expiration of a disappearing message
/// with unknown viewtype does not make `delete_expired_messages` fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_disappearing_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
let duration = 60;
chat.id
.set_ephemeral_timer(alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new_text("Expiring message".to_string());
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
// Set message viewtype to unassigned
// type 70 that was previously used for videochat invitations.
alice
.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
.await?;
SystemTime::shift(Duration::from_secs(100));
// This should not fail.
delete_expired_messages(alice, time()).await?;
Ok(())
}
/// Tests that deletion of a message with unknown viewtype
/// triggered by `delete_device_after`
/// does not make `delete_expired_messages` fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_device_after_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
alice
.set_config(Config::DeleteDeviceAfter, Some("600"))
.await?;
let mut msg = Message::new_text("Some message".to_string());
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
// Set message viewtype to unassigned
// type 70 that was previously used for videochat invitations.
alice
.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
.await?;
SystemTime::shift(Duration::from_secs(1000));
// This should not fail.
delete_expired_messages(alice, time()).await?;
Ok(())
}

View File

@@ -634,6 +634,9 @@ impl Imap {
let target = if let Some(message_id) = &message_id {
let msg_info =
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
if msg_info.is_some_and(|(_, ts_sent, _)| ts_sent == 0) {
message::prune_tombstone(context, message_id).await?;
}
let delete = if let Some((_, _, true)) = msg_info {
info!(context, "Deleting locally deleted message {message_id}.");
true
@@ -2342,12 +2345,18 @@ pub(crate) async fn prefetch_should_download(
message_id: &str,
mut flags: impl Iterator<Item = Flag<'_>>,
) -> Result<bool> {
if message::rfc724_mid_exists(context, message_id)
.await?
.is_some()
if let Some((_, _, is_trash)) = message::rfc724_mid_exists_ex(
context,
message_id,
"chat_id=3", // Trash
)
.await?
{
markseen_on_imap_table(context, message_id).await?;
return Ok(false);
let should = is_trash && !message::prune_tombstone(context, message_id).await?;
if !should {
markseen_on_imap_table(context, message_id).await?;
}
return Ok(should);
}
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
@@ -2420,8 +2429,11 @@ pub(crate) async fn prefetch_should_download(
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
// because they are stored to the db before sending. Also consider as duplicates only messages
// with greater timestamp to avoid deleting both messages in a multi-device setting.
// because they are stored to the db before sending. Trashed messages also have zero
// timestamp_sent and mustn't make new messages "duplicates", otherwise if a webxdc message is
// deleted because of DeleteDeviceAfter set, it won't be recovered from a re-sent message. Also
// consider as duplicates only messages with greater timestamp to avoid deleting both messages
// in a multi-device setting.
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
}

View File

@@ -37,12 +37,12 @@ impl DerefMut for Client {
}
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static str {
fn alpn(port: u16) -> &'static [&'static str] {
if port == 993 {
// Do not request ALPN on standard port.
""
&[]
} else {
"imap"
&["imap"]
}
}
@@ -210,15 +210,7 @@ impl Client {
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
let tls_stream = wrap_tls(
strict_tls,
hostname,
addr.port(),
alpn(addr.port()),
logging_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -270,16 +262,9 @@ impl Client {
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
"",
tcp_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
@@ -296,15 +281,7 @@ impl Client {
let proxy_stream = proxy_config
.connect(context, domain, port, strict_tls)
.await?;
let tls_stream = wrap_tls(
strict_tls,
domain,
port,
alpn(port),
proxy_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -357,16 +334,9 @@ impl Client {
let buffered_proxy_stream = client.into_inner();
let proxy_stream = buffered_proxy_stream.into_inner();
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
"",
proxy_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);

View File

@@ -15,7 +15,6 @@ use rand::thread_rng;
use tokio::runtime::Handle;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, info};
use crate::pgp::KeyPair;
use crate::tools::{self, time_elapsed};
@@ -25,7 +24,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
@@ -112,10 +111,7 @@ pub trait DcKey: Serialize + Deserializable + Clone {
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint;
/// Whether the key is private (or public).
fn is_private() -> bool;
/// Returns the OpenPGP Key ID.
fn key_id(&self) -> KeyId;
}
@@ -418,11 +414,15 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
(new_key_id,),
)?;
Ok(new_key_id)
Ok(Some(new_key_id))
})
.await?;
context.emit_event(EventType::AccountsItemChanged);
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
if let Some(new_key_id) = new_key_id {
// Update config cache if transaction succeeded and changed current default key.
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
}
Ok(())
}

View File

@@ -112,18 +112,20 @@ impl MsgId {
.unwrap_or_default())
}
/// Put message into trash chat and delete message text.
/// Put message into trash chat or delete it.
///
/// It means the message is deleted locally, but not on the server.
/// We keep some infos to
/// 1. not download the same message again
/// 2. be able to delete the message on the server if we want to
///
/// * `on_server`: Delete the message on the server also if it is seen on IMAP later, but only
/// if all parts of the message are trashed with this flag. `true` if the user explicitly
/// deletes the message. As for trashing a partially downloaded message when replacing it with
/// a fully downloaded one, see `receive_imf::add_parts()`.
/// * `on_server`: Keep some info to delete the message on the server also if it is seen on IMAP
/// later.
pub(crate) async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
if !on_server {
context
.sql
.execute("DELETE FROM msgs WHERE id=?1", (self,))
.await?;
return Ok(());
}
context
.sql
.execute(
@@ -132,7 +134,7 @@ impl MsgId {
// still adds to the db if chat_id is TRASH.
"INSERT OR REPLACE INTO msgs (id, rfc724_mid, timestamp, chat_id, deleted)
SELECT ?1, rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1",
(self, DC_CHAT_ID_TRASH, on_server),
(self, DC_CHAT_ID_TRASH, true),
)
.await?;
@@ -1590,11 +1592,15 @@ pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result
/// Delete a single message from the database, including references in other tables.
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
pub(crate) async fn delete_msg_locally(
context: &Context,
msg: &Message,
keep_tombstone: bool,
) -> Result<()> {
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await?;
}
let on_server = true;
let on_server = keep_tombstone;
msg.id
.trash(context, on_server)
.await
@@ -1662,6 +1668,7 @@ pub async fn delete_msgs_ex(
let mut modified_chat_ids = HashSet::new();
let mut deleted_rfc724_mid = Vec::new();
let mut res = Ok(());
let mut msgs = Vec::with_capacity(msg_ids.len());
for &msg_id in msg_ids {
let msg = Message::load_from_db(context, msg_id).await?;
@@ -1679,18 +1686,24 @@ pub async fn delete_msgs_ex(
let target = context.get_delete_msgs_target().await?;
let update_db = |trans: &mut rusqlite::Transaction| {
trans.execute(
// NB: If the message isn't sent yet, keeping its tombstone is unnecessary, but safe.
let keep_tombstone = trans.execute(
"UPDATE imap SET target=? WHERE rfc724_mid=?",
(target, msg.rfc724_mid),
)?;
(&target, msg.rfc724_mid),
)? == 0
|| !target.is_empty();
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
Ok(())
Ok(keep_tombstone)
};
if let Err(e) = context.sql.transaction(update_db).await {
error!(context, "delete_msgs: failed to update db: {e:#}.");
res = Err(e);
continue;
}
let keep_tombstone = match context.sql.transaction(update_db).await {
Ok(v) => v,
Err(e) => {
error!(context, "delete_msgs: failed to update db: {e:#}.");
res = Err(e);
continue;
}
};
msgs.push((msg_id, keep_tombstone));
}
res?;
@@ -1719,9 +1732,9 @@ pub async fn delete_msgs_ex(
.await?;
}
for &msg_id in msg_ids {
for (msg_id, keep_tombstone) in msgs {
let msg = Message::load_from_db(context, msg_id).await?;
delete_msg_locally(context, &msg).await?;
delete_msg_locally(context, &msg, keep_tombstone).await?;
}
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
@@ -1731,6 +1744,24 @@ pub async fn delete_msgs_ex(
Ok(())
}
/// Removes from the database a locally deleted message that also doesn't have a server UID.
/// Returns whether the removal happened.
pub(crate) async fn prune_tombstone(context: &Context, rfc724_mid: &str) -> Result<bool> {
Ok(context
.sql
.execute(
"DELETE FROM msgs
WHERE rfc724_mid=?
AND chat_id=?
AND NOT EXISTS (
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
)",
(rfc724_mid, DC_CHAT_ID_TRASH),
)
.await?
> 0)
}
/// Marks requested messages as seen.
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
if msg_ids.is_empty() {

View File

@@ -87,12 +87,12 @@ pub(crate) struct MimeMessage {
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decrypting_failed: bool,
/// Valid signature fingerprint if a message is an
/// Set of valid signature fingerprints if a message is an
/// Autocrypt encrypted and signed message.
///
/// If a message is not encrypted or the signature is not valid,
/// this is `None`.
pub signature: Option<Fingerprint>,
/// this set is empty.
pub signatures: HashSet<Fingerprint>,
/// The addresses for which there was a gossip header
/// and their respective gossiped keys.
@@ -589,7 +589,7 @@ impl MimeMessage {
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
signature: signatures.into_iter().last(),
signatures,
autocrypt_fingerprint,
gossiped_keys,
is_forwarded: false,
@@ -966,7 +966,7 @@ impl MimeMessage {
/// This means the message was both encrypted and signed with a
/// valid signature.
pub fn was_encrypted(&self) -> bool {
self.signature.is_some()
!self.signatures.is_empty()
}
/// Returns whether the email contains a `chat-version` header.

View File

@@ -12,7 +12,6 @@ use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::net::session::SessionStream;
use crate::net::tls::TlsSessionStore;
use crate::sql::Sql;
use crate::tools::time;
@@ -128,19 +127,10 @@ pub(crate) async fn connect_tls_inner(
addr: SocketAddr,
host: &str,
strict_tls: bool,
alpn: &str,
tls_session_store: &TlsSessionStore,
alpn: &[&str],
) -> Result<impl SessionStream + 'static> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
alpn,
tcp_stream,
tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)
}

View File

@@ -16,10 +16,6 @@ use crate::net::session::SessionStream;
use crate::net::tls::wrap_rustls;
use crate::tools::time;
/// User-Agent for HTTP requests if a resource usage policy requires it.
/// By default we do not set User-Agent.
const USER_AGENT: &str = "chatmail/2 (+https://github.com/chatmail/core/)";
/// HTTP(S) GET response.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
@@ -80,13 +76,11 @@ where
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.await?;
let tls_stream =
wrap_rustls(host, port, "", proxy_stream, &context.tls_session_store).await?;
let tls_stream = wrap_rustls(host, &[], proxy_stream).await?;
Box::new(tls_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
let tls_stream =
wrap_rustls(host, port, "", tcp_stream, &context.tls_session_store).await?;
let tls_stream = wrap_rustls(host, &[], tcp_stream).await?;
Box::new(tls_stream)
}
}
@@ -108,13 +102,6 @@ fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
let stale = if url.ends_with(".xdc") {
// WebXDCs are never stale, they just expire.
expires
} else if url.starts_with("https://tile.openstreetmap.org/")
|| url.starts_with("https://vector.openstreetmap.org/")
{
// Policy at <https://operations.osmfoundation.org/policies/tiles/>
// requires that we cache tiles for at least 7 days.
// Do not revalidate earlier than that.
now + 3600 * 24 * 7
} else if mimetype.is_some_and(|s| s.starts_with("image/")) {
// Cache images for 1 day.
//
@@ -256,22 +243,8 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
.context("URL has no authority")?
.clone();
let req = hyper::Request::builder().uri(parsed_url);
// OSM usage policy requires
// that User-Agent is set for HTTP GET requests
// to tile servers:
// <https://operations.osmfoundation.org/policies/tiles/>
// Same for vectory tiles
// at <https://operations.osmfoundation.org/policies/vector/>.
let req =
if authority == "tile.openstreetmap.org" || authority == "vector.openstreetmap.org" {
req.header("User-Agent", USER_AGENT)
} else {
req
};
let req = req
let req = hyper::Request::builder()
.uri(parsed_url)
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;

View File

@@ -429,14 +429,7 @@ impl ProxyConfig {
load_cache,
)
.await?;
let tls_stream = wrap_rustls(
&https_config.host,
https_config.port,
"",
tcp_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_rustls(&https_config.host, &[], tcp_stream).await?;
let auth = if let Some((username, password)) = &https_config.user_password {
Some((username.as_str(), password.as_str()))
} else {

View File

@@ -1,38 +1,27 @@
//! TLS support.
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
use crate::net::session::SessionStream;
use tokio_rustls::rustls::client::ClientSessionStore;
pub async fn wrap_tls<'a>(
strict_tls: bool,
hostname: &str,
port: u16,
alpn: &str,
alpn: &[&str],
stream: impl SessionStream + 'static,
tls_session_store: &TlsSessionStore,
) -> Result<impl SessionStream + 'a> {
if strict_tls {
let tls_stream = wrap_rustls(hostname, port, alpn, stream, tls_session_store).await?;
let tls_stream = wrap_rustls(hostname, alpn, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
} else {
// We use native_tls because it accepts 1024-bit RSA keys.
// Rustls does not support them even if
// certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
let alpns = if alpn.is_empty() {
Box::from([])
} else {
Box::from([alpn])
};
let tls = async_native_tls::TlsConnector::new()
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
.request_alpns(&alpns)
.request_alpns(alpn)
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true);
let tls_stream = tls.connect(hostname, stream).await?;
@@ -41,82 +30,18 @@ pub async fn wrap_tls<'a>(
}
}
/// Map to store TLS session tickets.
///
/// Tickets are separated by port and ALPN
/// to avoid trying to use Postfix ticket for Dovecot and vice versa.
/// Doing so would not be a security issue,
/// but wastes the ticket and the opportunity to resume TLS session unnecessarily.
/// Rustls takes care of separating tickets that belong to different domain names.
#[derive(Debug)]
pub(crate) struct TlsSessionStore {
sessions: Mutex<HashMap<(u16, String), Arc<dyn ClientSessionStore>>>,
}
// This is the default for TLS session store
// as of Rustls version 0.23.16,
// but we want to create multiple caches
// to separate them by port and ALPN.
const TLS_CACHE_SIZE: usize = 256;
impl TlsSessionStore {
/// Creates a new TLS session store.
///
/// One such store should be created per profile
/// to keep TLS sessions independent.
pub fn new() -> Self {
Self {
sessions: Default::default(),
}
}
/// Returns session store for given port and ALPN.
///
/// Rustls additionally separates sessions by hostname.
pub fn get(&self, port: u16, alpn: &str) -> Arc<dyn ClientSessionStore> {
Arc::clone(
self.sessions
.lock()
.entry((port, alpn.to_string()))
.or_insert_with(|| {
Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new(
TLS_CACHE_SIZE,
))
}),
)
}
}
pub async fn wrap_rustls<'a>(
hostname: &str,
port: u16,
alpn: &str,
alpn: &[&str],
stream: impl SessionStream + 'a,
tls_session_store: &TlsSessionStore,
) -> Result<impl SessionStream + 'a> {
let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut config = tokio_rustls::rustls::ClientConfig::builder()
let mut config = rustls::ClientConfig::builder()
.with_root_certificates(root_cert_store)
.with_no_client_auth();
config.alpn_protocols = if alpn.is_empty() {
vec![]
} else {
vec![alpn.as_bytes().to_vec()]
};
// Enable TLS 1.3 session resumption
// as defined in <https://www.rfc-editor.org/rfc/rfc8446#section-2.2>.
//
// Obsolete TLS 1.2 mechanisms defined in RFC 5246
// and RFC 5077 have worse security
// and are not worth increasing
// attack surface: <https://words.filippo.io/we-need-to-talk-about-session-tickets/>.
let resumption_store = tls_session_store.get(port, alpn);
let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store)
.tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled);
config.resumption = resumption;
config.alpn_protocols = alpn.iter().map(|s| s.as_bytes().to_vec()).collect();
let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned();

View File

@@ -44,7 +44,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
use crate::simplify;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix};
use crate::tools::{self, buf_compress, remove_subject_prefix, time};
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
use crate::{contact, imap};
@@ -557,44 +557,56 @@ pub(crate) async fn receive_imf_inner(
// make sure, this check is done eg. before securejoin-processing.
let (replace_msg_id, replace_chat_id);
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
replace_msg_id = Some(old_msg_id);
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
.await?
.filter(|msg| msg.download_state() != DownloadState::Done)
{
let msg = Message::load_from_db_optional(context, old_msg_id).await?;
// The tombstone being pruned means that we expected the message to appear on IMAP after
// deletion. NB: Not all such messages have `msgs.deleted=1`, see how external deletion
// requests deal with message reordering.
match msg.is_none() && !message::prune_tombstone(context, rfc724_mid).await? {
true => replace_msg_id = None,
false => replace_msg_id = Some(old_msg_id),
}
if let Some(msg) = msg.filter(|msg| msg.download_state() != DownloadState::Done) {
// the message was partially downloaded before and is fully downloaded now.
info!(context, "Message already partly in DB, replacing.");
Some(msg.chat_id)
replace_chat_id = Some(msg.chat_id);
} else {
// The message was already fully downloaded
// or cannot be loaded because it is deleted.
None
};
} else {
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
None
} else if let Some((old_msg_id, old_ts_sent)) =
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
{
if imap::is_dup_msg(
mime_parser.has_chat_version(),
mime_parser.timestamp_sent,
old_ts_sent,
) {
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
let target = context.get_delete_msgs_target().await?;
context
.sql
.execute(
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
(target, folder, uidvalidity, uid),
)
.await?;
}
Some(old_msg_id)
replace_chat_id = None;
}
} else if rfc724_mid_orig == rfc724_mid {
replace_msg_id = None;
replace_chat_id = None;
} else if let Some((old_msg_id, old_ts_sent, is_trash)) = message::rfc724_mid_exists_ex(
context,
rfc724_mid_orig,
"chat_id=3", // Trash
)
.await?
{
if is_trash && !message::prune_tombstone(context, rfc724_mid_orig).await? {
replace_msg_id = None;
} else if imap::is_dup_msg(
mime_parser.has_chat_version(),
mime_parser.timestamp_sent,
old_ts_sent,
) {
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
let target = context.get_delete_msgs_target().await?;
context
.sql
.execute(
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
(target, folder, uidvalidity, uid),
)
.await?;
replace_msg_id = Some(old_msg_id);
} else {
None
};
replace_msg_id = Some(old_msg_id);
}
replace_chat_id = None;
} else {
replace_msg_id = None;
replace_chat_id = None;
}
@@ -642,7 +654,7 @@ pub(crate) async fn receive_imf_inner(
// For example, GitHub sends messages from `notifications@github.com`,
// but uses display name of the user whose action generated the notification
// as the display name.
let fingerprint = mime_parser.signature.as_ref();
let fingerprint = mime_parser.signatures.iter().next();
let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
context,
&mime_parser.from,
@@ -999,7 +1011,7 @@ pub(crate) async fn receive_imf_inner(
}
}
if is_partial_download.is_none() && mime_parser.is_call() {
if mime_parser.is_call() {
context
.handle_call_msg(insert_msg_id, &mime_parser, from_id)
.await?;
@@ -1157,9 +1169,8 @@ async fn decide_chat_assignment(
{
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
true
} else if is_partial_download.is_none()
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded)
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded
{
info!(context, "Call state changed (TRASH).");
true
@@ -1987,9 +1998,8 @@ async fn add_parts(
handle_edit_delete(context, mime_parser, from_id).await?;
if is_partial_download.is_none()
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded)
if mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded
{
if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(call) =
@@ -2202,10 +2212,7 @@ RETURNING id
}
if let Some(replace_msg_id) = replace_msg_id {
// Trash the "replace" placeholder with a message that has no parts. If it has the original
// "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
// fully downloaded message later, the server-side deletion is issued.
let on_server = rfc724_mid == rfc724_mid_orig;
let on_server = false;
replace_msg_id.trash(context, on_server).await?;
}
@@ -2313,7 +2320,8 @@ async fn handle_edit_delete(
{
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
if msg.from_id == from_id {
message::delete_msg_locally(context, &msg).await?;
let keep_tombstone = false;
message::delete_msg_locally(context, &msg, keep_tombstone).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
@@ -2324,6 +2332,14 @@ async fn handle_edit_delete(
}
} else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
context
.sql
.execute(
"INSERT INTO msgs (rfc724_mid, timestamp, chat_id)
VALUES (?, ?, ?)",
(rfc724_mid, time(), DC_CHAT_ID_TRASH),
)
.await?;
}
}
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
@@ -3662,10 +3678,7 @@ async fn has_verified_encryption(
));
}
let signed_with_verified_key = mimeparser
.signature
.as_ref()
.is_some_and(|signature| *signature == fingerprint);
let signed_with_verified_key = mimeparser.signatures.contains(&fingerprint);
if signed_with_verified_key {
Ok(Verified)
} else {

View File

@@ -815,6 +815,31 @@ async fn test_concat_multiple_ndns() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_resent_deleted_msg() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?;
let sent = alice.send_text(alice_chat_id, "hi").await;
let alice_msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
bob.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?1, ?2, ?3, ?4, ?5)",
(&alice_msg.rfc724_mid, "INBOX", 1, 1, "INBOX"),
)
.await?;
let bob_msg = bob.recv_msg(&sent).await;
let bob_chat_id = bob_msg.chat_id;
message::delete_msgs(bob, &[bob_msg.id]).await?;
chat::resend_msgs(alice, &[alice_msg.id]).await?;
let bob_msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(bob_msg.chat_id, bob_chat_id);
assert_eq!(bob_msg.text, "hi");
Ok(())
}
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
context
.set_config(Config::ShowEmails, Some("2"))

View File

@@ -623,19 +623,17 @@ fn encrypted_and_signed(
mimeparser: &MimeMessage,
expected_fingerprint: &Fingerprint,
) -> bool {
if let Some(signature) = mimeparser.signature.as_ref() {
if signature == expected_fingerprint {
true
} else {
warn!(
context,
"Message does not match expected fingerprint {expected_fingerprint}.",
);
false
}
} else {
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
false
} else if !mimeparser.signatures.contains(expected_fingerprint) {
warn!(
context,
"Message does not match expected fingerprint {}.", expected_fingerprint,
);
false
} else {
true
}
}

View File

@@ -12,20 +12,20 @@ use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::net::tls::{TlsSessionStore, wrap_tls};
use crate::net::tls::wrap_tls;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::oauth2::get_oauth2_access_token;
use crate::tools::time;
/// Converts port number to ALPN.
fn alpn(port: u16) -> &'static str {
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static [&'static str] {
if port == 465 {
// Do not request ALPN on standard port.
""
&[]
} else {
"smtp"
&["smtp"]
}
}
@@ -109,12 +109,8 @@ async fn connection_attempt(
"Attempting SMTP connection to {host} ({resolved_addr})."
);
let res = match security {
ConnectionSecurity::Tls => {
connect_secure(resolved_addr, host, strict_tls, &context.tls_session_store).await
}
ConnectionSecurity::Starttls => {
connect_starttls(resolved_addr, host, strict_tls, &context.tls_session_store).await
}
ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await,
ConnectionSecurity::Starttls => connect_starttls(resolved_addr, host, strict_tls).await,
ConnectionSecurity::Plain => connect_insecure(resolved_addr).await,
};
match res {
@@ -230,15 +226,7 @@ async fn connect_secure_proxy(
let proxy_stream = proxy_config
.connect(context, hostname, port, strict_tls)
.await?;
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
alpn(port),
proxy_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
@@ -261,16 +249,9 @@ async fn connect_starttls_proxy(
skip_smtp_greeting(&mut buffered_stream).await?;
let transport = new_smtp_transport(buffered_stream).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
"",
tcp_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
@@ -293,16 +274,8 @@ async fn connect_secure(
addr: SocketAddr,
hostname: &str,
strict_tls: bool,
tls_session_store: &TlsSessionStore,
) -> Result<Box<dyn SessionBufStream>> {
let tls_stream = connect_tls_inner(
addr,
hostname,
strict_tls,
alpn(addr.port()),
tls_session_store,
)
.await?;
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
@@ -313,7 +286,6 @@ async fn connect_starttls(
addr: SocketAddr,
host: &str,
strict_tls: bool,
tls_session_store: &TlsSessionStore,
) -> Result<Box<dyn SessionBufStream>> {
let tcp_stream = connect_tcp_inner(addr).await?;
@@ -322,16 +294,9 @@ async fn connect_starttls(
skip_smtp_greeting(&mut buffered_stream).await?;
let transport = new_smtp_transport(buffered_stream).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
"",
tcp_stream,
tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::constants::Blocked;
use crate::constants::{self, Blocked};
use crate::contact::ContactId;
use crate::context::Context;
use crate::log::LogExt;
@@ -317,14 +317,21 @@ impl Context {
for rfc724_mid in msgs {
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
message::delete_msg_locally(self, &msg).await?;
let keep_tombstone = false;
message::delete_msg_locally(self, &msg, keep_tombstone).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
warn!(self, "Sync message delete: Database entry does not exist.");
}
} else {
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
info!(self, "sync_message_deletion: {rfc724_mid:?} not found.");
self.sql
.execute(
"INSERT INTO msgs (rfc724_mid, timestamp, chat_id) VALUES (?, ?, ?)",
(rfc724_mid, time(), constants::DC_CHAT_ID_TRASH),
)
.await?;
}
}
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;

View File

@@ -753,14 +753,6 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result<Vec<u8>> {
Ok(mem::take(decompressor.get_mut()))
}
/// Returns the given `&str` if already lowercased to avoid allocation, otherwise lowercases it.
pub(crate) fn to_lowercase(s: &str) -> Cow<'_, str> {
match s.chars().all(char::is_lowercase) {
true => Cow::Borrowed(s),
false => Cow::Owned(s.to_lowercase()),
}
}
/// Increments `*t` and checks that it equals to `expected` after that.
pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
t: &mut T,

View File

@@ -1853,6 +1853,14 @@ async fn test_status_update_vs_delete_device_after() -> Result<()> {
.await?;
let alice_chat = alice.create_chat(bob).await;
let alice_instance = send_webxdc_instance(alice, alice_chat.id).await?;
// Needed to test receiving of re-sent locally deleted webxdc.
bob.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?1, ?2, ?3, ?4, ?5)",
(&alice_instance.rfc724_mid, "INBOX", 1, 1, "INBOX"),
)
.await?;
let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false);
@@ -1882,8 +1890,15 @@ async fn test_status_update_vs_delete_device_after() -> Result<()> {
SystemTime::shift(Duration::from_secs(2700));
ephemeral::delete_expired_messages(bob, tools::time()).await?;
let bob_instance = Message::load_from_db(bob, bob_instance.id).await?;
assert_eq!(bob_instance.chat_id.is_trash(), false);
SystemTime::shift(Duration::from_secs(1800));
ephemeral::delete_expired_messages(bob, tools::time()).await?;
let bob_instance = Message::load_from_db_optional(bob, bob_instance.id).await?;
assert!(bob_instance.is_none());
// Additionally test that a re-sent instance can be received after deletion.
resend_msgs(alice, &[alice_instance.id]).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
Ok(())
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB