Compare commits

..

1 Commits

Author SHA1 Message Date
Hocuri
b334603e27 [WIP] For testing, send statistics once every minute 2025-12-04 17:49:17 +01:00
39 changed files with 129 additions and 786 deletions

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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.

View File

@@ -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()

View File

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

View File

@@ -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, ..
} => {

View File

@@ -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"

View File

@@ -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"),

View File

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

View File

@@ -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"

View File

@@ -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:

View File

@@ -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 = [

View File

@@ -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):

View File

@@ -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)

View File

@@ -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"] = {

View File

@@ -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"

View File

@@ -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!"

View 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"

View File

@@ -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"

View File

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

View File

@@ -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"

View File

@@ -1 +1 @@
2025-12-04
2025-12-01

View File

@@ -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,
));

View File

@@ -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| {

View File

@@ -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?;

View File

@@ -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.

View File

@@ -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?;

View File

@@ -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,

View File

@@ -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?
};

View File

@@ -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?;

View File

@@ -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;

View File

@@ -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?

View File

@@ -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)]

View File

@@ -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}.");

View File

@@ -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);

View File

@@ -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();