mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
1 Commits
v2.32.0
...
hoc/stats-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b334603e27 |
10
.github/workflows/deltachat-rpc-server.yml
vendored
10
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -388,9 +388,6 @@ jobs:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
runs-on: "ubuntu-latest"
|
||||
environment:
|
||||
name: npm-stdio-rpc-server
|
||||
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
@@ -518,14 +515,11 @@ jobs:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||
if: github.event_name == 'release'
|
||||
working-directory: deltachat-rpc-server/npm-package
|
||||
run: |
|
||||
ls -lah platform_package
|
||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
10
.github/workflows/jsonrpc-client-npm-package.yml
vendored
10
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -10,9 +10,6 @@ jobs:
|
||||
pack-module:
|
||||
name: "Publish @deltachat/jsonrpc-client"
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: npm-jsonrpc-client
|
||||
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -27,11 +24,6 @@ jobs:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
@@ -45,3 +37,5 @@ jobs:
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,52 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.32.0] - 2025-12-04
|
||||
|
||||
Version bump to trigger publishing of npm prebuilds
|
||||
that failed to be published for 2.31.0 due to not configured "trusted publishers".
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Lookup_or_create_adhoc_group(): Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
|
||||
|
||||
## [2.31.0] - 2025-12-04
|
||||
|
||||
### CI
|
||||
|
||||
- Update npm before publishing packages.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use v2 SEIPD when sending messages to self.
|
||||
|
||||
## [2.30.0] - 2025-12-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Disable SNI for STARTTLS ([#7499](https://github.com/chatmail/core/pull/7499)).
|
||||
- Introduce cross-core testing along with improvements to test frameworking.
|
||||
- Synchronize transports via sync messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix shutdown shortly after call.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `TransportsModified` event (for tests).
|
||||
|
||||
### CI
|
||||
|
||||
- Use "trusted publishing" for NPM packages.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump actions/checkout from 5 to 6.
|
||||
- cargo: Bump syn from 2.0.110 to 2.0.111.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.3 to 7.1.4.
|
||||
- cargo: Bump sdp from 0.8.0 to 0.10.0.
|
||||
- Remove two outdated todo comments ([#7550](https://github.com/chatmail/core/pull/7550)).
|
||||
|
||||
## [2.29.0] - 2025-12-01
|
||||
|
||||
### API-Changes
|
||||
@@ -7357,6 +7310,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
|
||||
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
|
||||
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
|
||||
[2.30.0]: https://github.com/chatmail/core/compare/v2.29.0..v2.30.0
|
||||
[2.31.0]: https://github.com/chatmail/core/compare/v2.30.0..v2.31.0
|
||||
[2.32.0]: https://github.com/chatmail/core/compare/v2.31.0..v2.32.0
|
||||
|
||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1304,7 +1304,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1413,7 +1413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1435,7 +1435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1451,7 +1451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1480,7 +1480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
|
||||
9
STYLE.md
9
STYLE.md
@@ -16,8 +16,7 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT DEFAULT '' NOT NULL -- message text
|
||||
) STRICT",
|
||||
)
|
||||
.await
|
||||
.context("CREATE TABLE messages")?;
|
||||
.await?;
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
@@ -30,8 +29,7 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
||||
text TEXT DEFAULT '' NOT NULL \
|
||||
) STRICT",
|
||||
)
|
||||
.await
|
||||
.context("CREATE TABLE messages")?;
|
||||
.await?;
|
||||
```
|
||||
Escaping newlines
|
||||
is prone to errors like this if space before backslash is missing:
|
||||
@@ -65,9 +63,6 @@ an older version. Also don't change the column type, consider adding a new colum
|
||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||
keyword doesn't help here.
|
||||
|
||||
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
||||
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
||||
|
||||
## Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
|
||||
@@ -38,7 +38,7 @@ use deltachat::{
|
||||
internals_for_benches::key_from_asc,
|
||||
internals_for_benches::parse_and_get_text,
|
||||
internals_for_benches::store_self_keypair,
|
||||
pgp::{KeyPair, SeipdVersion, decrypt, pk_encrypt, symm_encrypt_message},
|
||||
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
|
||||
stock_str::StockStrings,
|
||||
};
|
||||
use rand::{Rng, rng};
|
||||
@@ -111,7 +111,6 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
key_pair.secret.clone(),
|
||||
true,
|
||||
true,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -559,7 +559,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::IncomingCallAccepted { .. } => 2560,
|
||||
EventType::OutgoingCallAccepted { .. } => 2570,
|
||||
EventType::CallEnded { .. } => 2580,
|
||||
EventType::TransportsModified => 2600,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -594,8 +593,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::TransportsModified => 0,
|
||||
| EventType::AccountsItemChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. }
|
||||
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
@@ -683,8 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
| EventType::TransportsModified => 0,
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
@@ -783,8 +780,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::TransportsModified => ptr::null_mut(),
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -460,15 +460,6 @@ pub enum EventType {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// One or more transports has changed.
|
||||
///
|
||||
/// This event is used for tests to detect when transport
|
||||
/// synchronization messages arrives.
|
||||
/// UIs don't need to use it, it is unlikely
|
||||
/// that user modifies transports on multiple
|
||||
/// devices simultaneously.
|
||||
TransportsModified,
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -651,8 +642,6 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::TransportsModified => TransportsModified,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.32.0"
|
||||
"version": "2.29.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -30,15 +30,6 @@ $ pip install .
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
## Activating current checkout of deltachat-rpc-client and -server for development
|
||||
|
||||
Go to root repository directory and run:
|
||||
```
|
||||
$ scripts/make-rpc-testenv.sh
|
||||
$ source venv/bin/activate
|
||||
```
|
||||
|
||||
## Using in REPL
|
||||
|
||||
Setup a development environment:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -80,7 +80,6 @@ class EventType(str, Enum):
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
TRANSPORTS_MODIFIED = "TransportsModified"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import execnet
|
||||
import py
|
||||
import pytest
|
||||
|
||||
@@ -25,18 +20,6 @@ Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
def pytest_report_header():
|
||||
for base in os.get_exec_path():
|
||||
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
||||
if fn.exists():
|
||||
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
||||
proc.wait()
|
||||
version = proc.stderr.read().decode().strip()
|
||||
return f"deltachat-rpc-server: {fn} [{version}]"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
|
||||
@@ -214,134 +197,3 @@ def log():
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
|
||||
#
|
||||
# support for testing against different deltachat-rpc-server/clients
|
||||
# installed into a temporary virtualenv and connected via 'execnet' channels
|
||||
#
|
||||
|
||||
|
||||
def find_path(venv, name):
|
||||
is_windows = platform.system() == "Windows"
|
||||
bin = venv / ("bin" if not is_windows else "Scripts")
|
||||
|
||||
tryadd = [""]
|
||||
if is_windows:
|
||||
tryadd += os.environ["PATHEXT"].split(os.pathsep)
|
||||
for ext in tryadd:
|
||||
p = bin.joinpath(name + ext)
|
||||
if p.exists():
|
||||
return str(p)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_core_python_env(tmp_path_factory):
|
||||
"""Return a factory to create virtualenv environments with rpc server/client packages
|
||||
installed.
|
||||
|
||||
The factory takes a version and returns a (python_path, rpc_server_path) tuple
|
||||
of the respective binaries in the virtualenv.
|
||||
"""
|
||||
|
||||
envs = {}
|
||||
|
||||
def get_versioned_venv(core_version):
|
||||
venv = envs.get(core_version)
|
||||
if not venv:
|
||||
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv])
|
||||
|
||||
python = find_path(venv, "python")
|
||||
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
|
||||
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
|
||||
|
||||
envs[core_version] = venv
|
||||
python = find_path(venv, "python")
|
||||
rpc_server_path = find_path(venv, "deltachat-rpc-server")
|
||||
print(f"python={python}\nrpc_server={rpc_server_path}")
|
||||
return python, rpc_server_path
|
||||
|
||||
return get_versioned_venv
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
|
||||
|
||||
The 'eval' function allows to remote-execute arbitrary expressions
|
||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||
"""
|
||||
|
||||
def factory(core_version):
|
||||
python, rpc_server_path = get_core_python_env(core_version)
|
||||
gw = execnet.makegateway(f"popen//python={python}")
|
||||
|
||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||
channel = gw.remote_exec(remote_bob_loop)
|
||||
cm = os.environ.get("CHATMAIL_DOMAIN")
|
||||
|
||||
# trigger getting an online account on bob's side
|
||||
channel.send((accounts_dir, str(rpc_server_path), cm))
|
||||
|
||||
# meanwhile get a local alice account
|
||||
alice = acfactory.get_online_account()
|
||||
channel.send(alice.self_contact.make_vcard())
|
||||
|
||||
# wait for bob to have started
|
||||
sysinfo = channel.receive()
|
||||
assert sysinfo == f"v{core_version}"
|
||||
bob_vcard = channel.receive()
|
||||
[alice_contact_bob] = alice.import_vcard(bob_vcard)
|
||||
|
||||
def eval(eval_str):
|
||||
channel.send(eval_str)
|
||||
return channel.receive()
|
||||
|
||||
return alice, alice_contact_bob, eval
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
def remote_bob_loop(channel):
|
||||
# This function executes with versioned
|
||||
# deltachat-rpc-client/server packages
|
||||
# installed into the virtualenv.
|
||||
#
|
||||
# The "channel" argument is a send/receive pipe
|
||||
# to the process that runs the corresponding remote_exec(remote_bob_loop)
|
||||
|
||||
import os
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||
|
||||
# older core versions don't support specifying rpc_server_path
|
||||
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
||||
basepath = os.path.dirname(rpc_server_path)
|
||||
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
|
||||
rpc = Rpc(accounts_dir=accounts_dir)
|
||||
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||
acfactory = ACFactory(dc)
|
||||
bob = acfactory.get_online_account()
|
||||
alice_vcard = channel.receive()
|
||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||
channel.send(bob.self_contact.make_vcard())
|
||||
|
||||
while 1:
|
||||
eval_str = channel.receive()
|
||||
res = eval(eval_str, ns)
|
||||
try:
|
||||
channel.send(res)
|
||||
except Exception:
|
||||
# some unserializable result
|
||||
channel.send(None)
|
||||
|
||||
@@ -57,7 +57,7 @@ class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs):
|
||||
"""Initialize RPC client.
|
||||
|
||||
The 'kwargs' arguments will be passed to subprocess.Popen().
|
||||
The given arguments will be passed to subprocess.Popen().
|
||||
"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
|
||||
|
||||
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
|
||||
python, rpc_server_path = get_core_python_env("2.24.0")
|
||||
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
|
||||
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
|
||||
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
||||
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
|
||||
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
|
||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
assert msg.get_snapshot().text == "hello"
|
||||
@@ -1,6 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -157,47 +156,3 @@ def test_reconfigure_transport(acfactory) -> None:
|
||||
# Reconfiguring the transport should not reset
|
||||
# the settings as if when configuring the first transport.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
|
||||
|
||||
def test_transport_synchronization(acfactory, log) -> None:
|
||||
"""Test synchronization of transports between devices."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert len(ac1.list_transports()) == 2
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
|
||||
ac1_clone.add_transport_from_qr(qr)
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert len(ac1.list_transports()) == 3
|
||||
assert len(ac1_clone.list_transports()) == 3
|
||||
|
||||
log.section("ac1 clone removes second transport")
|
||||
[transport1, transport2, transport3] = ac1_clone.list_transports()
|
||||
addr3 = transport3["addr"]
|
||||
ac1_clone.delete_transport(transport2["addr"])
|
||||
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport3["addr"])
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
ac2_chat.send_text("Hello!")
|
||||
|
||||
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
|
||||
20
deltachat-rpc-client/tests/test_rpc_virtual.py
Normal file
20
deltachat-rpc-client/tests/test_rpc_virtual.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from platform import system # noqa
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
|
||||
|
||||
@pytest.mark.skipif("system() == 'Windows'")
|
||||
def test_install_venv_and_use_other_core(tmp_path):
|
||||
venv = tmp_path.joinpath("venv1")
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv])
|
||||
python = venv / "bin" / "python"
|
||||
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"])
|
||||
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server"))
|
||||
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.32.0"
|
||||
"version": "2.29.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.32.0"
|
||||
version = "2.29.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-12-04
|
||||
2025-12-01
|
||||
11
src/calls.rs
11
src/calls.rs
@@ -6,7 +6,7 @@ use crate::chat::ChatIdBlocked;
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::{Context, WeakContext};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::warn;
|
||||
@@ -199,9 +199,8 @@ impl Context {
|
||||
call.id = send_msg(self, chat_id, &mut call).await?;
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
let context = self.get_weak_context();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
context,
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.id,
|
||||
));
|
||||
@@ -292,12 +291,11 @@ impl Context {
|
||||
}
|
||||
|
||||
async fn emit_end_call_if_unaccepted(
|
||||
context: WeakContext,
|
||||
context: Context,
|
||||
wait: u64,
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let context = context.upgrade()?;
|
||||
let Some(mut call) = context.load_call_by_id(call_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
@@ -370,9 +368,8 @@ impl Context {
|
||||
}
|
||||
}
|
||||
let wait = call.remaining_ring_seconds();
|
||||
let context = self.get_weak_context();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
context,
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.msg.id,
|
||||
));
|
||||
|
||||
@@ -819,19 +819,11 @@ impl Context {
|
||||
self,
|
||||
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
|
||||
);
|
||||
self.sql
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
(
|
||||
addr,
|
||||
serde_json::to_string(&EnteredLoginParam::default())?,
|
||||
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
|
||||
.await?;
|
||||
ConfiguredLoginParam::from_json(&format!(
|
||||
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
|
||||
))?
|
||||
.save_to_transports_table(self, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
}
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::transport::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, send_sync_transports,
|
||||
ConnectionCandidate,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
@@ -205,7 +205,6 @@ impl Context {
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
let primary_addr = transaction.query_row(
|
||||
@@ -220,13 +219,12 @@ impl Context {
|
||||
if primary_addr == addr {
|
||||
bail!("Cannot delete primary transport");
|
||||
}
|
||||
let (transport_id, add_timestamp) = transaction.query_row(
|
||||
"DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
|
||||
let transport_id = transaction.query_row(
|
||||
"DELETE FROM transports WHERE addr=? RETURNING id",
|
||||
(addr,),
|
||||
|row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let add_timestamp: i64 = row.get(1)?;
|
||||
Ok((id, add_timestamp))
|
||||
Ok(id)
|
||||
},
|
||||
)?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
|
||||
@@ -235,23 +233,9 @@ impl Context {
|
||||
(transport_id,),
|
||||
)?;
|
||||
|
||||
// Removal timestamp should not be lower than addition timestamp
|
||||
// to be accepted by other devices when synced.
|
||||
let remove_timestamp = std::cmp::max(now, add_timestamp);
|
||||
|
||||
transaction.execute(
|
||||
"INSERT INTO removed_transports (addr, remove_timestamp)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
|
||||
(addr, remove_timestamp),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -568,8 +552,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !is_configured {
|
||||
if !ctx.is_configured().await? {
|
||||
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
|
||||
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
|
||||
}
|
||||
@@ -580,10 +563,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
let provider = configured_param.provider;
|
||||
configured_param
|
||||
.clone()
|
||||
.save_to_transports_table(ctx, param, time())
|
||||
.save_to_transports_table(ctx, param)
|
||||
.await?;
|
||||
send_sync_transports(ctx).await?;
|
||||
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, OnceLock, Weak};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
@@ -201,25 +201,6 @@ impl Deref for Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// A weak reference to a [`Context`]
|
||||
///
|
||||
/// Can be used to obtain a [`Context`]. An existing weak reference does not prevent the corresponding [`Context`] from being dropped.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct WeakContext {
|
||||
inner: Weak<InnerContext>,
|
||||
}
|
||||
|
||||
impl WeakContext {
|
||||
/// Returns the [`Context`] if it is still available.
|
||||
pub(crate) fn upgrade(&self) -> Result<Context> {
|
||||
let inner = self
|
||||
.inner
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow::anyhow!("Inner struct has been dropped"))?;
|
||||
Ok(Context { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Actual context, expensive to clone.
|
||||
#[derive(Debug)]
|
||||
pub struct InnerContext {
|
||||
@@ -404,13 +385,6 @@ impl Context {
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
/// Returns a weak reference to this [`Context`].
|
||||
pub(crate) fn get_weak_context(&self) -> WeakContext {
|
||||
WeakContext {
|
||||
inner: Arc::downgrade(&self.inner),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the database with the given passphrase.
|
||||
/// NB: Db encryption is deprecated, so `passphrase` should be empty normally. See
|
||||
/// [`ContextBuilder::with_password()`] for reasoning.
|
||||
|
||||
@@ -8,7 +8,7 @@ use mail_builder::mime::MimePart;
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::context::Context;
|
||||
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
|
||||
use crate::pgp::{self, SeipdVersion};
|
||||
use crate::pgp;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptHelper {
|
||||
@@ -47,7 +47,6 @@ impl EncryptHelper {
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
anonymous_recipients: bool,
|
||||
seipd_version: SeipdVersion,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
@@ -61,7 +60,6 @@ impl EncryptHelper {
|
||||
sign_key,
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
seipd_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -417,15 +417,6 @@ pub enum EventType {
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// One or more transports has changed.
|
||||
///
|
||||
/// This event is used for tests to detect when transport
|
||||
/// synchronization messages arrives.
|
||||
/// UIs don't need to use it, it is unlikely
|
||||
/// that user modifies transports on multiple
|
||||
/// devices simultaneously.
|
||||
TransportsModified,
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
#[cfg(test)]
|
||||
Test,
|
||||
|
||||
@@ -32,7 +32,6 @@ use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{SystemMessage, is_hidden};
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::pgp::SeipdVersion;
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -1259,17 +1258,6 @@ impl MimeFactory {
|
||||
} else {
|
||||
// Asymmetric encryption
|
||||
|
||||
let seipd_version = if encryption_pubkeys.is_empty() {
|
||||
// If message is sent only to self,
|
||||
// use v2 SEIPD.
|
||||
SeipdVersion::V2
|
||||
} else {
|
||||
// If message is sent to others,
|
||||
// they may not support v2 SEIPD yet,
|
||||
// so use v1 SEIPD.
|
||||
SeipdVersion::V1
|
||||
};
|
||||
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
|
||||
@@ -1283,7 +1271,6 @@ impl MimeFactory {
|
||||
message,
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
seipd_version,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
69
src/pgp.rs
69
src/pgp.rs
@@ -160,20 +160,6 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
|
||||
.find(|subkey| subkey.is_encryption_key())
|
||||
}
|
||||
|
||||
/// Version of SEIPD packet to use.
|
||||
///
|
||||
/// See
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9580#name-avoiding-ciphertext-malleab>
|
||||
/// for the discussion on when v2 SEIPD should be used.
|
||||
#[derive(Debug)]
|
||||
pub enum SeipdVersion {
|
||||
/// Use v1 SEIPD, for compatibility.
|
||||
V1,
|
||||
|
||||
/// Use v2 SEIPD when we know that v2 SEIPD is supported.
|
||||
V2,
|
||||
}
|
||||
|
||||
/// Encrypts `plain` text using `public_keys_for_encryption`
|
||||
/// and signs it using `private_key_for_signing`.
|
||||
pub async fn pk_encrypt(
|
||||
@@ -182,7 +168,6 @@ pub async fn pk_encrypt(
|
||||
private_key_for_signing: SignedSecretKey,
|
||||
compress: bool,
|
||||
anonymous_recipients: bool,
|
||||
seipd_version: SeipdVersion,
|
||||
) -> Result<String> {
|
||||
Handle::current()
|
||||
.spawn_blocking(move || {
|
||||
@@ -193,49 +178,21 @@ pub async fn pk_encrypt(
|
||||
.filter_map(select_pk_for_encryption);
|
||||
|
||||
let msg = MessageBuilder::from_bytes("", plain);
|
||||
let encoded_msg = match seipd_version {
|
||||
SeipdVersion::V1 => {
|
||||
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
|
||||
|
||||
for pkey in pkeys {
|
||||
if anonymous_recipients {
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
} else {
|
||||
msg.encrypt_to_key(&mut rng, &pkey)?;
|
||||
}
|
||||
}
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
|
||||
msg.to_armored_string(&mut rng, Default::default())?
|
||||
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
|
||||
for pkey in pkeys {
|
||||
if anonymous_recipients {
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
} else {
|
||||
msg.encrypt_to_key(&mut rng, &pkey)?;
|
||||
}
|
||||
SeipdVersion::V2 => {
|
||||
let mut msg = msg.seipd_v2(
|
||||
&mut rng,
|
||||
SYMMETRIC_KEY_ALGORITHM,
|
||||
AeadAlgorithm::Ocb,
|
||||
ChunkSize::C8KiB,
|
||||
);
|
||||
}
|
||||
|
||||
for pkey in pkeys {
|
||||
if anonymous_recipients {
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
} else {
|
||||
msg.encrypt_to_key(&mut rng, &pkey)?;
|
||||
}
|
||||
}
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
|
||||
msg.to_armored_string(&mut rng, Default::default())?
|
||||
}
|
||||
};
|
||||
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
|
||||
|
||||
Ok(encoded_msg)
|
||||
})
|
||||
@@ -590,7 +547,6 @@ mod tests {
|
||||
KEYS.alice_secret.clone(),
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -760,7 +716,6 @@ mod tests {
|
||||
KEYS.alice_secret.clone(),
|
||||
true,
|
||||
true,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -827,41 +827,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
if let Some(ref sync_items) = mime_parser.sync_items {
|
||||
if from_id == ContactId::SELF {
|
||||
if mime_parser.was_encrypted() {
|
||||
// Receiving encrypted message from self updates primary transport.
|
||||
let from_addr = &mime_parser.from.addr;
|
||||
|
||||
let transport_changed = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let transport_exists = transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(from_addr,),
|
||||
|row| {
|
||||
let count: i64 = row.get(0)?;
|
||||
Ok(count > 0)
|
||||
},
|
||||
)?;
|
||||
|
||||
let transport_changed = if transport_exists {
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(from_addr,),
|
||||
)? > 0
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Received sync message from unknown address {from_addr:?}."
|
||||
);
|
||||
false
|
||||
};
|
||||
Ok(transport_changed)
|
||||
})
|
||||
.await?;
|
||||
if transport_changed {
|
||||
info!(context, "Primary transport changed to {from_addr:?}.");
|
||||
context.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
|
||||
context
|
||||
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
|
||||
.await;
|
||||
@@ -2506,11 +2471,10 @@ async fn lookup_or_create_adhoc_group(
|
||||
id INTEGER PRIMARY KEY
|
||||
) STRICT",
|
||||
(),
|
||||
)
|
||||
.context("CREATE TEMP TABLE temp.contacts")?;
|
||||
)?;
|
||||
let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
|
||||
for &id in &contact_ids {
|
||||
stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
|
||||
stmt.execute((id,))?;
|
||||
}
|
||||
let val = t
|
||||
.query_row(
|
||||
@@ -2532,10 +2496,8 @@ async fn lookup_or_create_adhoc_group(
|
||||
Ok((id, blocked))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.context("Select chat with matching name and members")?;
|
||||
t.execute("DROP TABLE temp.contacts", ())
|
||||
.context("DROP TABLE temp.contacts")?;
|
||||
.optional()?;
|
||||
t.execute("DROP TABLE temp.contacts", ())?;
|
||||
Ok(val)
|
||||
};
|
||||
let query_only = true;
|
||||
|
||||
@@ -1439,21 +1439,6 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 142)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE transports
|
||||
ADD COLUMN add_timestamp INTEGER NOT NULL DEFAULT 0;
|
||||
CREATE TABLE removed_transports (
|
||||
addr TEXT NOT NULL,
|
||||
remove_timestamp INTEGER NOT NULL,
|
||||
UNIQUE(addr)
|
||||
) STRICT;",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -25,8 +25,8 @@ use crate::tools::{create_id, time};
|
||||
|
||||
pub(crate) const STATISTICS_BOT_EMAIL: &str = "self_reporting@testrun.org";
|
||||
const STATISTICS_BOT_VCARD: &str = include_str!("../assets/statistics-bot.vcf");
|
||||
const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; // 1 week
|
||||
// const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
|
||||
// const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; // 1 week
|
||||
const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
|
||||
const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; // 4 minutes (less than the lowest ephemeral messages timeout)
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
54
src/sync.rs
54
src/sync.rs
@@ -9,15 +9,14 @@ use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::log::{LogExt as _, warn};
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::tools::time;
|
||||
use crate::transport::{ConfiguredLoginParamJson, sync_transports};
|
||||
use crate::{message, stock_str, token};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -53,29 +52,6 @@ pub(crate) struct QrTokenData {
|
||||
pub(crate) grpid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct TransportData {
|
||||
/// Configured login parameters.
|
||||
pub(crate) configured: ConfiguredLoginParamJson,
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
///
|
||||
/// They can be used to reconfigure the transport.
|
||||
pub(crate) entered: EnteredLoginParam,
|
||||
|
||||
/// Timestamp of when the transport was last time (re)configured.
|
||||
pub(crate) timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct RemovedTransportData {
|
||||
/// Address of the removed transport.
|
||||
pub(crate) addr: String,
|
||||
|
||||
/// Timestamp of when the transport was removed.
|
||||
pub(crate) timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum SyncData {
|
||||
AddQrToken(QrTokenData),
|
||||
@@ -95,28 +71,6 @@ pub(crate) enum SyncData {
|
||||
DeleteMessages {
|
||||
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
|
||||
},
|
||||
|
||||
/// Update transport configuration.
|
||||
///
|
||||
/// This message contains a list of all added transports
|
||||
/// together with their addition timestamp,
|
||||
/// and all removed transports together with
|
||||
/// the removal timestamp.
|
||||
///
|
||||
/// In case of a tie, addition and removal timestamps
|
||||
/// being the same, removal wins.
|
||||
/// It is more likely that transport is added
|
||||
/// and then removed within a second,
|
||||
/// but unlikely the other way round
|
||||
/// as adding new transport takes time
|
||||
/// to run configuration.
|
||||
Transports {
|
||||
/// Active transports.
|
||||
transports: Vec<TransportData>,
|
||||
|
||||
/// Removed transports with the timestamp of removal.
|
||||
removed_transports: Vec<RemovedTransportData>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -320,10 +274,6 @@ impl Context {
|
||||
SyncData::Config { key, val } => self.sync_config(key, val).await,
|
||||
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
|
||||
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
|
||||
SyncData::Transports {
|
||||
transports,
|
||||
removed_transports,
|
||||
} => sync_transports(self, transports, removed_transports).await,
|
||||
},
|
||||
SyncDataOrUnknown::Unknown(data) => {
|
||||
warn!(self, "Ignored unknown sync item: {data}.");
|
||||
|
||||
@@ -600,7 +600,7 @@ impl TestContext {
|
||||
self.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.expect("Failed to configure address");
|
||||
.unwrap();
|
||||
|
||||
if let Some(name) = addr.split('@').next() {
|
||||
self.set_name(name);
|
||||
|
||||
209
src/transport.rs
209
src/transport.rs
@@ -18,12 +18,10 @@ use crate::config::Config;
|
||||
use crate::configure::server_params::{ServerParams, expand_param_vector};
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::net::load_connection_timestamp;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
|
||||
use crate::sql::Sql;
|
||||
use crate::sync::{RemovedTransportData, SyncData, TransportData};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) enum ConnectionSecurity {
|
||||
@@ -192,10 +190,10 @@ pub(crate) struct ConfiguredLoginParam {
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
/// JSON representation of ConfiguredLoginParam
|
||||
/// for the database and sync messages.
|
||||
/// The representation of ConfiguredLoginParam in the database,
|
||||
/// saved as Json.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct ConfiguredLoginParamJson {
|
||||
struct ConfiguredLoginParamJson {
|
||||
pub addr: String,
|
||||
pub imap: Vec<ConfiguredServerLoginParam>,
|
||||
pub imap_user: String,
|
||||
@@ -559,9 +557,35 @@ impl ConfiguredLoginParam {
|
||||
self,
|
||||
context: &Context,
|
||||
entered_param: &EnteredLoginParam,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
save_transport(context, entered_param, &self.into(), timestamp).await?;
|
||||
let addr = addr_normalize(&self.addr);
|
||||
let provider_id = self.provider.map(|provider| provider.id);
|
||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param",
|
||||
(
|
||||
self.addr.clone(),
|
||||
serde_json::to_string(entered_param)?,
|
||||
self.into_json()?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if configured_addr.is_none() {
|
||||
// If there is no transport yet, set the new transport as the primary one
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -585,7 +609,18 @@ impl ConfiguredLoginParam {
|
||||
}
|
||||
|
||||
pub(crate) fn into_json(self) -> Result<String> {
|
||||
let json: ConfiguredLoginParamJson = self.into();
|
||||
let json = ConfiguredLoginParamJson {
|
||||
addr: self.addr,
|
||||
imap: self.imap,
|
||||
imap_user: self.imap_user,
|
||||
imap_password: self.imap_password,
|
||||
smtp: self.smtp,
|
||||
smtp_user: self.smtp_user,
|
||||
smtp_password: self.smtp_password,
|
||||
provider_id: self.provider.map(|p| p.id.to_string()),
|
||||
certificate_checks: self.certificate_checks,
|
||||
oauth2: self.oauth2,
|
||||
};
|
||||
Ok(serde_json::to_string(&json)?)
|
||||
}
|
||||
|
||||
@@ -603,166 +638,12 @@ impl ConfiguredLoginParam {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
|
||||
fn from(configured_login_param: ConfiguredLoginParam) -> Self {
|
||||
Self {
|
||||
addr: configured_login_param.addr,
|
||||
imap: configured_login_param.imap,
|
||||
imap_user: configured_login_param.imap_user,
|
||||
imap_password: configured_login_param.imap_password,
|
||||
smtp: configured_login_param.smtp,
|
||||
smtp_user: configured_login_param.smtp_user,
|
||||
smtp_password: configured_login_param.smtp_password,
|
||||
provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
|
||||
certificate_checks: configured_login_param.certificate_checks,
|
||||
oauth2: configured_login_param.oauth2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves transport to the database.
|
||||
pub(crate) async fn save_transport(
|
||||
context: &Context,
|
||||
entered_param: &EnteredLoginParam,
|
||||
configured: &ConfiguredLoginParamJson,
|
||||
add_timestamp: i64,
|
||||
) -> Result<()> {
|
||||
let addr = addr_normalize(&configured.addr);
|
||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET entered_param=excluded.entered_param,
|
||||
configured_param=excluded.configured_param,
|
||||
add_timestamp=excluded.add_timestamp",
|
||||
(
|
||||
&addr,
|
||||
serde_json::to_string(entered_param)?,
|
||||
serde_json::to_string(configured)?,
|
||||
add_timestamp,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if configured_addr.is_none() {
|
||||
// If there is no transport yet, set the new transport as the primary one
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a sync message to synchronize transports across devices.
|
||||
pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
||||
info!(context, "Sending transport synchronization message.");
|
||||
|
||||
// Synchronize all transport configurations.
|
||||
//
|
||||
// Transport with ID 1 is never synchronized
|
||||
// because it can only be created during initial configuration.
|
||||
// This also guarantees that credentials for the first
|
||||
// transport are never sent in sync messages,
|
||||
// so this is not worse than when not using multi-transport.
|
||||
// If transport ID 1 is reconfigured,
|
||||
// likely because the password has changed,
|
||||
// user has to reconfigure it manually on all devices.
|
||||
let transports = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT entered_param, configured_param, add_timestamp
|
||||
FROM transports WHERE id>1",
|
||||
(),
|
||||
|row| {
|
||||
let entered_json: String = row.get(0)?;
|
||||
let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
|
||||
let configured_json: String = row.get(1)?;
|
||||
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
|
||||
let timestamp: i64 = row.get(2)?;
|
||||
Ok(TransportData {
|
||||
configured,
|
||||
entered,
|
||||
timestamp,
|
||||
})
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let removed_transports = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr, remove_timestamp FROM removed_transports",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let timestamp: i64 = row.get(1)?;
|
||||
Ok(RemovedTransportData { addr, timestamp })
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.add_sync_item(SyncData::Transports {
|
||||
transports,
|
||||
removed_transports,
|
||||
})
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process received data for transport synchronization.
|
||||
pub(crate) async fn sync_transports(
|
||||
context: &Context,
|
||||
transports: &[TransportData],
|
||||
removed_transports: &[RemovedTransportData],
|
||||
) -> Result<()> {
|
||||
for TransportData {
|
||||
configured,
|
||||
entered,
|
||||
timestamp,
|
||||
} in transports
|
||||
{
|
||||
save_transport(context, entered, configured, *timestamp).await?;
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
for RemovedTransportData { addr, timestamp } in removed_transports {
|
||||
transaction.execute(
|
||||
"DELETE FROM transports
|
||||
WHERE addr=? AND add_timestamp<=?",
|
||||
(addr, timestamp),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"INSERT INTO removed_transports (addr, remove_timestamp)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (addr) DO
|
||||
UPDATE SET remove_timestamp = excluded.remove_timestamp
|
||||
WHERE excluded.remove_timestamp > remove_timestamp",
|
||||
(addr, timestamp),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::log::LogExt as _;
|
||||
use crate::provider::get_provider_by_id;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::time;
|
||||
|
||||
#[test]
|
||||
fn test_configured_certificate_checks_display() {
|
||||
@@ -807,7 +688,7 @@ mod tests {
|
||||
|
||||
param
|
||||
.clone()
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
|
||||
assert_eq!(
|
||||
@@ -1025,7 +906,7 @@ mod tests {
|
||||
certificate_checks: ConfiguredCertificateChecks::Automatic,
|
||||
oauth2: false,
|
||||
}
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user