mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
Compare commits
19 Commits
v1.152.0
...
iequidoo/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8eda42ff4 | ||
|
|
63710513c5 | ||
|
|
0cc80268d2 | ||
|
|
64a1b8e57c | ||
|
|
5772284e82 | ||
|
|
beb6a21ecd | ||
|
|
22bc7567d3 | ||
|
|
a910808b4e | ||
|
|
3d5e442145 | ||
|
|
3af4ea1d00 | ||
|
|
a9e38aa8fc | ||
|
|
9e408c3abd | ||
|
|
67e16d0222 | ||
|
|
5069b585c8 | ||
|
|
6cd6aca7b8 | ||
|
|
d822da3c9f | ||
|
|
9d331483e9 | ||
|
|
1e1e5793dd | ||
|
|
b74ff278ce |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -103,9 +103,9 @@ jobs:
|
||||
- os: macos-latest
|
||||
rust: 1.83.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.81.0
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.77.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## [1.151.2] - 2024-12-24
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Emit ImexProgress(1) after receiving backup size.
|
||||
- `delete_msgs`: Use `transaction()` instead of `call_write()`.
|
||||
- Start ephemeral timers when the chat is noticed.
|
||||
- Start ephemeral timers when the chat is archived.
|
||||
- Revalidate HTTP cache entries once per minute maximum.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Reduce number of `repeat_vars()` calls.
|
||||
- `sanitise_name`: Don't consider punctuation and control chars as part of file extension ([#6362](https://github.com/deltachat/deltachat-core-rust/pull/6362)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove marknoticed_chat_if_older_than().
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove contrib/ directory.
|
||||
|
||||
## [1.152.1] - 2024-12-17
|
||||
|
||||
### Build system
|
||||
|
||||
- Downgrade Rust version used to build binaries.
|
||||
- Reduce MSRV to 1.77.0.
|
||||
|
||||
## [1.152.0] - 2024-12-12
|
||||
|
||||
### API-Changes
|
||||
@@ -5509,3 +5539,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.151.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.4..v1.151.5
|
||||
[1.151.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.5..v1.151.6
|
||||
[1.152.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.6..v1.152.0
|
||||
[1.152.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.0..v1.152.1
|
||||
[1.152.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.1..v1.152.2
|
||||
|
||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1306,7 +1306,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1407,7 +1407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1432,7 +1432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1448,7 +1448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1477,7 +1477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Examples:
|
||||
#
|
||||
# Original server that doesn't use SSL:
|
||||
# ./proxy.py 8080 imap.nauta.cu 143
|
||||
# ./proxy.py 8081 smtp.nauta.cu 25
|
||||
#
|
||||
# Original server that uses SSL:
|
||||
# ./proxy.py 8080 testrun.org 993 --ssl
|
||||
# ./proxy.py 8081 testrun.org 465 --ssl
|
||||
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import selectors
|
||||
import ssl
|
||||
import socket
|
||||
import socketserver
|
||||
|
||||
|
||||
class Proxy(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
|
||||
self.real_host = real_host
|
||||
self.real_port = real_port
|
||||
self.use_ssl = use_ssl
|
||||
super().__init__((proxy_host, proxy_port), RequestHandler)
|
||||
|
||||
|
||||
class RequestHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
|
||||
|
||||
total = 0
|
||||
real_server = (self.server.real_host, self.server.real_port)
|
||||
with socket.create_connection(real_server) as sock:
|
||||
if self.server.use_ssl:
|
||||
context = ssl.create_default_context()
|
||||
sock = context.wrap_socket(
|
||||
sock, server_hostname=real_server[0])
|
||||
|
||||
forward = {self.request: sock, sock: self.request}
|
||||
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(self.request, selectors.EVENT_READ,
|
||||
self.client_address)
|
||||
sel.register(sock, selectors.EVENT_READ, real_server)
|
||||
|
||||
active = True
|
||||
while active:
|
||||
events = sel.select()
|
||||
for key, mask in events:
|
||||
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
|
||||
data = key.fileobj.recv(1024)
|
||||
received = len(data)
|
||||
total += received
|
||||
print(data)
|
||||
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
|
||||
if data:
|
||||
forward[key.fileobj].sendall(data)
|
||||
else:
|
||||
print('\nCLOSING CONNECTION.\n\n')
|
||||
forward[key.fileobj].close()
|
||||
key.fileobj.close()
|
||||
active = False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser(description='Simple Python Proxy')
|
||||
p.add_argument(
|
||||
"proxy_port", help="the port where the proxy will listen", type=int)
|
||||
p.add_argument('host', help="the real host")
|
||||
p.add_argument('port', help="the port of the real host", type=int)
|
||||
p.add_argument("--ssl", help="use ssl to connect to the real host",
|
||||
action="store_true")
|
||||
args = p.parse_args()
|
||||
|
||||
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
|
||||
proxy.serve_forever()
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.152.0"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -73,6 +73,9 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
# Alice deletes "vg-member-added", SecureJoin succeeds before that.
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Check that at least some of the handshake messages are deleted.
|
||||
for ac in [alice, bob]:
|
||||
while True:
|
||||
@@ -80,13 +83,12 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
# Bob deletes "vg-request-with-auth", SecureJoin succeeds later.
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.152.0"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
62
flake.lock
generated
62
flake.lock
generated
@@ -47,16 +47,17 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731393059,
|
||||
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
|
||||
"lastModified": 1711088506,
|
||||
"narHash": "sha256-USdlY7Tx2oJWqFBpp10+03+h7eVhpkQ4s9t1ERjeIJE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
|
||||
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -114,6 +115,25 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"new-fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"rust-analyzer-src": "rust-analyzer-src_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1734417396,
|
||||
"narHash": "sha256-32x1Z+Pz3Jv0cK9EG56cFTKXy/mZ/c+Ikxw+aVfKHp4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a18d41b26e998e95a598858fdb86ba22fb5da47d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1730207686,
|
||||
@@ -174,6 +194,22 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1734119587,
|
||||
"narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
@@ -195,8 +231,9 @@
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"naersk": "naersk",
|
||||
"new-fenix": "new-fenix",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
"nixpkgs": "nixpkgs_5"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
@@ -216,6 +253,23 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1734386068,
|
||||
"narHash": "sha256-Py025JiD9lcPmldB7X1AEjq3WBTS60jZUJRtTDonmaE=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0a706f7d2ac093985eae317781200689cfd48b78",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
|
||||
13
flake.nix
13
flake.nix
@@ -1,14 +1,19 @@
|
||||
{
|
||||
description = "Delta Chat core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
# Old Rust to build releases.
|
||||
fenix.url = "github:nix-community/fenix?rev=85f4139f3c092cf4afd9f9906d7ed218ef262c97";
|
||||
|
||||
# New Rust for development shell.
|
||||
new-fenix.url = "github:nix-community/fenix";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, new-fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
@@ -539,13 +544,13 @@
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
overlays = [ new-fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
(new-fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.152.0"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.152.0"
|
||||
version = "1.151.2"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-12-12
|
||||
2024-12-24
|
||||
@@ -279,7 +279,9 @@ impl<'a> BlobObject<'a> {
|
||||
let ext: String = name
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| !c.is_whitespace())
|
||||
.take_while(|c| {
|
||||
(!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control()
|
||||
})
|
||||
.take(33)
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
@@ -982,6 +984,10 @@ mod tests {
|
||||
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
||||
assert_eq!(stem, "a. tar");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
|
||||
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
|
||||
assert_eq!(ext, ".pdf");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
72
src/chat.rs
72
src/chat.rs
@@ -28,7 +28,7 @@ use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::location;
|
||||
@@ -312,7 +312,7 @@ impl ChatId {
|
||||
|
||||
/// Create a group or mailinglist raw database record with the given parameters.
|
||||
/// The function does not add SELF nor checks if the record already exists.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_multiuser_record(
|
||||
context: &Context,
|
||||
chattype: Chattype,
|
||||
@@ -688,6 +688,10 @@ impl ChatId {
|
||||
})
|
||||
.await?;
|
||||
|
||||
if visibility == ChatVisibility::Archived {
|
||||
start_chat_ephemeral_timers(context, self).await?;
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
@@ -3231,19 +3235,6 @@ pub async fn get_chat_msgs_ex(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub(crate) async fn marknoticed_chat_if_older_than(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
|
||||
if timestamp > chat_timestamp {
|
||||
marknoticed_chat(context, chat_id).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks all messages in the chat as noticed.
|
||||
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
|
||||
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
@@ -3255,10 +3246,10 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
.query_map(
|
||||
"SELECT DISTINCT(m.chat_id) FROM msgs m
|
||||
LEFT JOIN chats c ON m.chat_id=c.id
|
||||
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1",
|
||||
(),
|
||||
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.archived=1",
|
||||
(),
|
||||
|row| row.get::<_, ChatId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into)
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
if chat_ids_in_archive.is_empty() {
|
||||
@@ -3267,32 +3258,40 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id IN ({});",
|
||||
sql::repeat_vars(chat_ids_in_archive.len())
|
||||
),
|
||||
rusqlite::params_from_iter(&chat_ids_in_archive),
|
||||
)
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare(
|
||||
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id = ?",
|
||||
)?;
|
||||
for chat_id_in_archive in &chat_ids_in_archive {
|
||||
stmt.execute((chat_id_in_archive,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
for chat_id_in_archive in chat_ids_in_archive {
|
||||
start_chat_ephemeral_timers(context, chat_id_in_archive).await?;
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
|
||||
}
|
||||
} else if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
} else {
|
||||
start_chat_ephemeral_timers(context, chat_id).await?;
|
||||
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
(MessageState::InNoticed, MessageState::InFresh, chat_id),
|
||||
)
|
||||
.await?
|
||||
== 0
|
||||
{
|
||||
return Ok(());
|
||||
(MessageState::InNoticed, MessageState::InFresh, chat_id),
|
||||
)
|
||||
.await?
|
||||
== 0
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
@@ -3364,6 +3363,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
}
|
||||
|
||||
for c in changed_chats {
|
||||
start_chat_ephemeral_timers(context, c).await?;
|
||||
context.emit_event(EventType::MsgsNoticed(c));
|
||||
chatlist_events::emit_chatlist_item_changed(context, c);
|
||||
}
|
||||
@@ -4444,7 +4444,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
///
|
||||
/// For example, it can be a message showing that a member was added to a group.
|
||||
/// Doesn't fail if the chat doesn't exist.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn add_info_msg_with_cmd(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
|
||||
@@ -129,17 +129,14 @@ impl ContactId {
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
|
||||
sql::repeat_vars(ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&[origin])
|
||||
.chain(params_iter(ids))
|
||||
.chain(params_iter(&[origin])),
|
||||
),
|
||||
)
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction
|
||||
.prepare("UPDATE contacts SET origin=?1 WHERE id = ?2 AND origin < ?1")?;
|
||||
for id in ids {
|
||||
stmt.execute((origin, id))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -805,6 +802,7 @@ impl Contact {
|
||||
}
|
||||
|
||||
let mut name = sanitize_name(name);
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if origin <= Origin::OutgoingTo {
|
||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||
if addr.contains("noreply")
|
||||
|
||||
125
src/ephemeral.rs
125
src/ephemeral.rs
@@ -84,7 +84,6 @@ use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{duration_to_str, time, SystemTime};
|
||||
|
||||
@@ -329,23 +328,44 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
msg_ids: &[MsgId],
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
let count = context
|
||||
let should_interrupt =
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let mut should_interrupt = false;
|
||||
let mut stmt =
|
||||
transaction.prepare(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id=?2")?;
|
||||
for msg_id in msg_ids {
|
||||
should_interrupt |= stmt.execute((now, msg_id))? > 0;
|
||||
}
|
||||
Ok(should_interrupt)
|
||||
}).await?;
|
||||
if should_interrupt {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts ephemeral timer for all messages in the chat.
|
||||
///
|
||||
/// This should be called when chat is marked as noticed.
|
||||
pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
let now = time();
|
||||
let should_interrupt = context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id IN ({})",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::sql::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
|
||||
.chain(params_iter(msg_ids)),
|
||||
),
|
||||
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
|
||||
WHERE chat_id = ?2
|
||||
AND ephemeral_timer > 0
|
||||
AND (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer)",
|
||||
(now, chat_id),
|
||||
)
|
||||
.await?;
|
||||
if count > 0 {
|
||||
.await?
|
||||
> 0;
|
||||
if should_interrupt {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
@@ -695,7 +715,9 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
||||
use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
@@ -1422,4 +1444,77 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that ephemeral timer is started when the chat is noticed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_noticed_ephemeral_timer() -> 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 bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
marknoticed_chat(bob, bob_received_message.chat_id).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that archiving the chat starts ephemeral timer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archived_ephemeral_timer() -> 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 bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
bob_received_message
|
||||
.chat_id
|
||||
.set_visibility(bob, ChatVisibility::Archived)
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
|
||||
// Bob mutes the chat so it is not unarchived.
|
||||
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
|
||||
|
||||
// Now test that for already archived chat
|
||||
// timer is started if all archived chats are marked as noticed.
|
||||
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
|
||||
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
|
||||
|
||||
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(
|
||||
Message::load_from_db_optional(bob, bob_received_message_2.id)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ pub enum HeaderDef {
|
||||
|
||||
/// [Autocrypt](https://autocrypt.org/) header.
|
||||
Autocrypt,
|
||||
AutocryptGossip,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
|
||||
|
||||
78
src/imap.rs
78
src/imap.rs
@@ -46,7 +46,6 @@ use crate::receive_imf::{
|
||||
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
|
||||
};
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{self, create_id, duration_to_str};
|
||||
|
||||
@@ -911,15 +910,15 @@ impl Session {
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("cannot remove deleted messages from imap table")?;
|
||||
.context("Cannot remove deleted messages from imap table")?;
|
||||
|
||||
context.emit_event(EventType::ImapMessageDeleted(format!(
|
||||
"IMAP messages {uid_set} marked as deleted"
|
||||
@@ -942,15 +941,15 @@ impl Session {
|
||||
// Messages are moved or don't exist, IMAP returns OK response in both cases.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("cannot delete moved messages from imap table")?;
|
||||
.context("Cannot delete moved messages from imap table")?;
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} moved to {target}"
|
||||
)));
|
||||
@@ -996,15 +995,15 @@ impl Session {
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE imap SET target='' WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("cannot plan deletion of messages")?;
|
||||
.context("Cannot plan deletion of messages")?;
|
||||
if copy {
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} copied to {target}"
|
||||
@@ -1021,10 +1020,11 @@ impl Session {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, uid, target FROM imap
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
(folder,),
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
AND target_min_smtp_id <= (SELECT IFNULL((SELECT MIN(id) FROM smtp), ?))
|
||||
ORDER BY target, uid",
|
||||
(folder, i64::MAX),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
@@ -1147,15 +1147,16 @@ impl Session {
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"DELETE FROM imap_markseen WHERE id IN ({})",
|
||||
sql::repeat_vars(rowid_set.len())
|
||||
),
|
||||
rusqlite::params_from_iter(rowid_set),
|
||||
)
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
|
||||
for rowid in rowid_set {
|
||||
stmt.execute((rowid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("cannot remove messages marked as seen from imap_markseen table")?;
|
||||
.context("Cannot remove messages marked as seen from imap_markseen table")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,7 +1302,7 @@ impl Session {
|
||||
/// Returns the last UID fetched successfully and the info about each downloaded message.
|
||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||
/// it is skipped and the error is logged.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -2689,6 +2690,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
|
||||
@@ -178,6 +178,7 @@ impl BackupProvider {
|
||||
}
|
||||
|
||||
info!(context, "Received valid backup authentication token.");
|
||||
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
let blobdir = BlobDirContents::new(&context).await?;
|
||||
@@ -309,6 +310,10 @@ pub async fn get_backup2(
|
||||
let mut file_size_buf = [0u8; 8];
|
||||
recv_stream.read_exact(&mut file_size_buf).await?;
|
||||
let file_size = u64::from_be_bytes(file_size_buf);
|
||||
info!(context, "Received backup file size.");
|
||||
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
import_backup_stream(context, recv_stream, file_size, passphrase)
|
||||
.await
|
||||
.context("Failed to import backup from QUIC stream")?;
|
||||
@@ -434,12 +439,14 @@ mod tests {
|
||||
assert!(msg.save_file(&ctx1, &path).await.is_err());
|
||||
|
||||
// Check that both received the ImexProgress events.
|
||||
ctx0.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
ctx1.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
for ctx in [&ctx0, &ctx1] {
|
||||
ctx.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1)))
|
||||
.await;
|
||||
ctx.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -707,9 +707,6 @@ pub(crate) async fn save(
|
||||
))?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
|
||||
}
|
||||
|
||||
@@ -1613,15 +1613,15 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
let update_db = |conn: &mut rusqlite::Connection| {
|
||||
conn.execute(
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
trans.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
(target, msg.rfc724_mid),
|
||||
)?;
|
||||
conn.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = context.sql.call_write(update_db).await {
|
||||
if let Err(e) = context.sql.transaction(update_db).await {
|
||||
error!(context, "delete_msgs: failed to update db: {e:#}.");
|
||||
res = Err(e);
|
||||
continue;
|
||||
|
||||
@@ -1047,6 +1047,7 @@ impl MimeFactory {
|
||||
part.body(text)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn render_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
|
||||
@@ -1279,7 +1279,7 @@ impl MimeMessage {
|
||||
Ok(self.parts.len() > old_part_count)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn do_add_single_file_part(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
|
||||
@@ -151,7 +151,7 @@ async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Re
|
||||
/// Also returns if the response is stale and should be revalidated in the background.
|
||||
async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response, bool)>> {
|
||||
let now = time();
|
||||
let Some((blob_name, mimetype, encoding, is_stale)) = context
|
||||
let Some((blob_name, mimetype, encoding, stale_timestamp)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT blobname, mimetype, encoding, stale
|
||||
@@ -162,13 +162,14 @@ async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response
|
||||
let mimetype: Option<String> = Some(row.get(1)?).filter(|s: &String| !s.is_empty());
|
||||
let encoding: Option<String> = Some(row.get(2)?).filter(|s: &String| !s.is_empty());
|
||||
let stale_timestamp: i64 = row.get(3)?;
|
||||
Ok((blob_name, mimetype, encoding, now > stale_timestamp))
|
||||
Ok((blob_name, mimetype, encoding, stale_timestamp))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let is_stale = now > stale_timestamp;
|
||||
|
||||
let blob_object = BlobObject::from_name(context, blob_name)?;
|
||||
let blob_abs_path = blob_object.to_abs_path();
|
||||
@@ -195,15 +196,16 @@ async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response
|
||||
// Update expiration timestamp
|
||||
// to prevent deletion of the file still in use.
|
||||
//
|
||||
// We do not update stale timestamp here
|
||||
// as we have not revalidated the response.
|
||||
// Stale timestamp is updated only
|
||||
// when the URL is sucessfully fetched.
|
||||
// If the response is stale, the caller should revalidate it in the background, so update
|
||||
// `stale` timestamp to avoid revalidating too frequently (and have many parallel revalidation
|
||||
// tasks) if revalidation fails or the HTTP request takes some time. The stale period >= 1 hour,
|
||||
// so 1 more minute won't be a problem.
|
||||
let stale_timestamp = if is_stale { now + 60 } else { stale_timestamp };
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE http_cache SET expires=? WHERE url=?",
|
||||
(expires, url),
|
||||
"UPDATE http_cache SET expires=?, stale=? WHERE url=?",
|
||||
(expires, stale_timestamp, url),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -305,8 +307,6 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return stale result.
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
@@ -495,6 +495,22 @@ mod tests {
|
||||
);
|
||||
assert_eq!(http_cache_get(t, xdc_pixel_url).await?, None);
|
||||
|
||||
// If we get the blob the second time quickly, it shouldn't be stale because it's supposed
|
||||
// that we've already run a revalidation task which will update the blob soon.
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_editor_url).await?,
|
||||
Some((xdc_response.clone(), false))
|
||||
);
|
||||
// But if the revalidation task hasn't succeeded after some time, the blob is stale again
|
||||
// even if we continue to get it frequently.
|
||||
for i in (0..100).rev() {
|
||||
SystemTime::shift(Duration::from_secs(6));
|
||||
if let Some((_, true)) = http_cache_get(t, xdc_editor_url).await? {
|
||||
break;
|
||||
}
|
||||
assert!(i > 0);
|
||||
}
|
||||
|
||||
// Test that if the file is accidentally removed from the blobdir,
|
||||
// there is no error when trying to load the cache entry.
|
||||
for entry in std::fs::read_dir(t.get_blobdir())? {
|
||||
|
||||
@@ -371,6 +371,7 @@ impl Params {
|
||||
/// Note that in the [ParamsFile::FsPath] case the blob can be
|
||||
/// created without copying if the path already refers to a valid
|
||||
/// blob. If so a [BlobObject] will be returned.
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
pub async fn get_blob<'a>(
|
||||
&self,
|
||||
key: Param,
|
||||
|
||||
@@ -21,9 +21,11 @@ use tokio::runtime::Handle;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[cfg(test)]
|
||||
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
|
||||
@@ -187,7 +187,7 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::assertions_on_constants)]
|
||||
#[allow(clippy::assertions_on_constants)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_thresholds() -> anyhow::Result<()> {
|
||||
assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
|
||||
|
||||
@@ -60,7 +60,8 @@ pub struct ReceivedMsg {
|
||||
/// IDs of inserted rows in messages table.
|
||||
pub msg_ids: Vec<MsgId>,
|
||||
|
||||
/// Whether IMAP messages should be immediately deleted.
|
||||
/// Whether IMAP messages should be deleted. Deletion is done after necessary auto-generated
|
||||
/// messages are sent which is important for SecureJoin.
|
||||
pub needs_delete_job: bool,
|
||||
|
||||
/// Whether the From address was repeated in the signed part
|
||||
@@ -158,7 +159,7 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId>
|
||||
/// If `is_partial_download` is set, it contains the full message size in bytes.
|
||||
/// Do not confuse that with `replace_msg_id` that will be set when the full message is loaded
|
||||
/// later.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn receive_imf_inner(
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
@@ -587,10 +588,20 @@ pub(crate) async fn receive_imf_inner(
|
||||
Some(_) => "target=?1,",
|
||||
None => "",
|
||||
};
|
||||
let target_min_smtp_id_subst = match received_msg.needs_delete_job {
|
||||
true => "target_min_smtp_id=(SELECT IFNULL((SELECT MAX(id)+1 FROM smtp),0)),",
|
||||
false => "",
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
|
||||
&format!(
|
||||
"UPDATE imap SET
|
||||
{target_subst}
|
||||
{target_min_smtp_id_subst}
|
||||
rfc724_mid=?2
|
||||
WHERE rfc724_mid=?3"
|
||||
),
|
||||
(
|
||||
target.as_deref().unwrap_or_default(),
|
||||
rfc724_mid_orig,
|
||||
@@ -679,7 +690,7 @@ pub async fn from_field_to_contact_id(
|
||||
/// Creates a `ReceivedMsg` from given parts which might consist of
|
||||
/// multiple messages (if there are multiple attachments).
|
||||
/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
|
||||
async fn add_parts(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -1690,11 +1701,6 @@ RETURNING id
|
||||
"Message has {icnt} parts and is assigned to chat #{chat_id}."
|
||||
);
|
||||
|
||||
// new outgoing message from another device marks the chat as noticed.
|
||||
if !mime_parser.incoming && !chat_id.is_special() {
|
||||
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
|
||||
}
|
||||
|
||||
if !is_mdn {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
@@ -1834,7 +1840,7 @@ async fn lookup_chat_by_reply(
|
||||
Ok(Some((parent_chat.id, parent_chat.blocked)))
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn lookup_chat_or_create_adhoc_group(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
@@ -1969,7 +1975,7 @@ async fn is_probably_private_reply(
|
||||
/// than two members, a new ad hoc group is created.
|
||||
///
|
||||
/// On success the function returns the created (chat_id, chat_blocked) tuple.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_group(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -2089,6 +2095,7 @@ async fn create_group(
|
||||
/// just omitted.
|
||||
///
|
||||
/// * `is_partial_download` - whether the message is not fully downloaded.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
|
||||
@@ -454,6 +454,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?;
|
||||
inviter_progress(context, contact_id, 800);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
} else {
|
||||
// Setup verified contact.
|
||||
secure_connection_established(
|
||||
@@ -468,8 +471,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
@@ -1353,6 +1356,8 @@ mod tests {
|
||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||
// be strange to have it in "member-added" messages of verified groups only.
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
|
||||
44
src/smtp.rs
44
src/smtp.rs
@@ -21,7 +21,6 @@ use crate::mimefactory::MimeFactory;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::sql;
|
||||
use crate::stock_str::unencrypted_email;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
@@ -104,7 +103,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
/// Connect using the provided login params.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -515,6 +514,27 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
}
|
||||
let folders = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT DISTINCT(folder) FROM imap
|
||||
WHERE target_min_smtp_id <= (SELECT IFNULL((SELECT MIN(id) FROM smtp), ?))",
|
||||
(i64::MAX,),
|
||||
|row| {
|
||||
let folder: String = row.get(0)?;
|
||||
Ok(Some(folder))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
if folders.contains(&context.get_config(Config::ConfiguredInboxFolder).await?) {
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
if folders.contains(&context.get_config(Config::ConfiguredMvboxFolder).await?)
|
||||
|| folders.contains(&context.get_config(Config::ConfiguredSentboxFolder).await?)
|
||||
{
|
||||
context.scheduler.interrupt_oboxes().await;
|
||||
}
|
||||
|
||||
// although by slow sending, ratelimit may have been expired meanwhile,
|
||||
// do not attempt to send MDNs if ratelimited happened before on status-updates/sync:
|
||||
@@ -585,18 +605,16 @@ async fn send_mdn_rfc724_mid(
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,))
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
if !additional_rfc724_mids.is_empty() {
|
||||
let q = format!(
|
||||
"DELETE FROM smtp_mdns WHERE rfc724_mid IN({})",
|
||||
sql::repeat_vars(additional_rfc724_mids.len())
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(&q, rusqlite::params_from_iter(additional_rfc724_mids))
|
||||
.await?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
SendResult::Retry => {
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn new_smtp_transport<S: AsyncBufRead + AsyncWrite + Unpin>(
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn connect_and_auth(
|
||||
context: &Context,
|
||||
proxy_config: &Option<ProxyConfig>,
|
||||
|
||||
@@ -1121,6 +1121,18 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 127)?;
|
||||
if dbversion < migration_version {
|
||||
// Emails must be moved to `target` or deleted only when `smtp.id` reaches
|
||||
// `target_min_smtp_id` to keep SecureJoin working for multi-device and in case of a local
|
||||
// state loss.
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE imap ADD COLUMN target_min_smtp_id INTEGER NOT NULL DEFAULT 0",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
Reference in New Issue
Block a user