mirror of
https://github.com/chatmail/core.git
synced 2026-05-07 08:56:30 +03:00
Compare commits
1 Commits
iequidoo/S
...
iequidoo/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e7e8c35da |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.95.0
|
||||
RUST_VERSION: 1.94.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.88.0
|
||||
|
||||
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -1360,7 +1360,7 @@ dependencies = [
|
||||
"proptest",
|
||||
"qrcodegen",
|
||||
"quick-xml",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.4",
|
||||
"ratelimit",
|
||||
"regex",
|
||||
@@ -2981,7 +2981,7 @@ dependencies = [
|
||||
"pin-project",
|
||||
"pkarr",
|
||||
"portmapper",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"ring",
|
||||
@@ -3056,7 +3056,7 @@ dependencies = [
|
||||
"iroh-metrics",
|
||||
"n0-future",
|
||||
"postcard",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"serde-error",
|
||||
@@ -3119,7 +3119,7 @@ checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.2.16",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
@@ -3172,7 +3172,7 @@ dependencies = [
|
||||
"pin-project",
|
||||
"pkarr",
|
||||
"postcard",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"rustls-webpki 0.102.8",
|
||||
@@ -3776,7 +3776,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
@@ -4206,7 +4206,7 @@ dependencies = [
|
||||
"p256",
|
||||
"p384",
|
||||
"p521",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"replace_with",
|
||||
"ripemd",
|
||||
@@ -4453,7 +4453,7 @@ dependencies = [
|
||||
"nested_enum_utils",
|
||||
"netwatch",
|
||||
"num_enum",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"snafu",
|
||||
@@ -4776,9 +4776,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
@@ -5870,7 +5870,7 @@ dependencies = [
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"pnet_packet",
|
||||
"rand 0.8.6",
|
||||
"rand 0.8.5",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
|
||||
@@ -4234,8 +4234,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* true if the Webxdc should get internet access;
|
||||
* this is the case i.e. for experimental maps integration.
|
||||
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
* - is_app_sender: Define if the local user is the one who initially shared the webxdc application in the chat.
|
||||
* - is_broadcast: Define if the app runs in a broadcasting context.
|
||||
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
|
||||
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
|
||||
|
||||
@@ -318,15 +318,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Requests to clear storage on all chatmail relays.
|
||||
///
|
||||
/// I/O must be started for this request to take effect.
|
||||
async fn clear_all_relay_storage(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.clear_all_relay_storage().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get top-level info for an account.
|
||||
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
|
||||
let context_option = self.accounts.read().await.get_account(account_id);
|
||||
|
||||
@@ -37,10 +37,6 @@ pub struct WebxdcMessageInfo {
|
||||
internet_access: bool,
|
||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
self_addr: String,
|
||||
/// Define if the local user is the one who initially shared the webxdc application in the chat.
|
||||
is_app_sender: bool,
|
||||
/// Define if the app runs in a broadcasting context.
|
||||
is_broadcast: bool,
|
||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
||||
send_update_interval: usize,
|
||||
@@ -64,8 +60,6 @@ impl WebxdcMessageInfo {
|
||||
request_integration: _,
|
||||
internet_access,
|
||||
self_addr,
|
||||
is_app_sender,
|
||||
is_broadcast,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
@@ -78,8 +72,6 @@ impl WebxdcMessageInfo {
|
||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||
internet_access,
|
||||
self_addr,
|
||||
is_app_sender,
|
||||
is_broadcast,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,59 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from imap_tools import AND, U
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client import Contact, EventType, Message
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
"""When a batch of messages is moved from Inbox to another folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
assert ac2.is_configured()
|
||||
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's movebox folder in the reverse order")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("Movebox")
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "Movebox")
|
||||
|
||||
logging.info("moving messages back")
|
||||
ac2_direct_imap.select_folder("Movebox")
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()]):
|
||||
ac2_direct_imap.conn.move(uid, "INBOX")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory, direct_imap, log):
|
||||
|
||||
@@ -18,8 +18,6 @@ def test_webxdc(acfactory) -> None:
|
||||
"sourceCodeUrl": None,
|
||||
"summary": None,
|
||||
"selfAddr": webxdc_info["selfAddr"],
|
||||
"isAppSender": False,
|
||||
"isBroadcast": False,
|
||||
"sendUpdateInterval": 1000,
|
||||
"sendUpdateMaxSize": 18874368,
|
||||
}
|
||||
|
||||
10
deny.toml
10
deny.toml
@@ -27,7 +27,15 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||
"RUSTSEC-2026-0049",
|
||||
"RUSTSEC-2026-0098",
|
||||
"RUSTSEC-2026-0099"
|
||||
"RUSTSEC-2026-0099",
|
||||
|
||||
# rand 0.8.x
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0097>
|
||||
# We already use rand 0.9,
|
||||
# version 0.8 that cannot be upgraded
|
||||
# is a dependency of iroh 0.35.0 and rPGP.
|
||||
# rPGP upgrade is waiting for <https://github.com/rpgp/rpgp/pull/573>
|
||||
"RUSTSEC-2026-0097"
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -48,3 +48,19 @@ the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type
|
||||
|
||||
This will **rsync** your current checkout to the remote build machine
|
||||
(no need to commit before) and then run either rust or python tests.
|
||||
|
||||
# coredeps Dockerfile
|
||||
|
||||
`coredeps/Dockerfile` specifies an image that contains all
|
||||
of Delta Chat's core dependencies. It is used to
|
||||
build python wheels (binary packages for Python).
|
||||
|
||||
You can build the docker images yourself locally
|
||||
to avoid the relatively large download:
|
||||
|
||||
cd scripts # where all CI things are
|
||||
docker build -t deltachat/coredeps coredeps
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
|
||||
|
||||
26
scripts/concourse/README.md
Normal file
26
scripts/concourse/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Concourse CI pipeline
|
||||
|
||||
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
|
||||
that builds C documentation, Python documentation, Python wheels for `x86_64`
|
||||
and `aarch64` and Python source packages, and uploads them.
|
||||
|
||||
To setup the pipeline run
|
||||
```
|
||||
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
|
||||
```
|
||||
where `secret.yml` contains the following secrets:
|
||||
```
|
||||
c.delta.chat:
|
||||
private_key: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
devpi:
|
||||
login: dc
|
||||
password: ...
|
||||
```
|
||||
|
||||
Secrets can be read from the password manager:
|
||||
```
|
||||
fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml)
|
||||
```
|
||||
305
scripts/concourse/docs_wheels.yml
Normal file
305
scripts/concourse/docs_wheels.yml
Normal file
@@ -0,0 +1,305 @@
|
||||
resources:
|
||||
- name: deltachat-core-rust
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/chatmail/core.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/chatmail/core.git
|
||||
tag_filter: "v*"
|
||||
|
||||
jobs:
|
||||
- name: python-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_x86_64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
# Binary wheels
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload x86_64 wheels and source packages
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_aarch64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-musl-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_x86_64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload musl x86_64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64*
|
||||
|
||||
- name: python-musl-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_aarch64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload musl aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64*
|
||||
9
scripts/coredeps/Dockerfile
Normal file
9
scripts/coredeps/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG BASEIMAGE=quay.io/pypa/manylinux2014_x86_64
|
||||
#ARG BASEIMAGE=quay.io/pypa/musllinux_1_1_x86_64
|
||||
#ARG BASEIMAGE=quay.io/pypa/manylinux2014_aarch64
|
||||
|
||||
FROM $BASEIMAGE
|
||||
RUN pipx install tox
|
||||
COPY install-rust.sh /scripts/
|
||||
RUN /scripts/install-rust.sh
|
||||
RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi
|
||||
20
scripts/coredeps/install-rust.sh
Executable file
20
scripts/coredeps/install-rust.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.94.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$ARCH-unknown-linux-$LIBC"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
|
||||
14
src/blob.rs
14
src/blob.rs
@@ -416,6 +416,17 @@ impl<'a> BlobObject<'a> {
|
||||
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
|
||||
// images.
|
||||
let do_scale = exceeds_max_bytes
|
||||
// Don't recode huge JPEGs w/o resizing:
|
||||
// - It may be huge because of high JPEG quality, but we don't know that.
|
||||
// - We don't want extra CPU work for most photos.
|
||||
&& (fmt == ImageFormat::Jpeg
|
||||
|| encoded_img_exceeds_bytes(
|
||||
context,
|
||||
&img,
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)?)
|
||||
|| is_avatar
|
||||
&& (exceeds_wh
|
||||
|| exif.is_some() && {
|
||||
@@ -477,8 +488,7 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if do_scale || exif.is_some() {
|
||||
if !encoded.is_empty() || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, ImageFormat::Jpeg)
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
|
||||
@@ -462,6 +462,26 @@ async fn test_recode_image_balanced_png() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_balanced_png_huge() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot-huge.png");
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
original_width: 1618,
|
||||
original_height: 949,
|
||||
compressed_width: 1618,
|
||||
compressed_height: 949,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_with_exif() {
|
||||
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
|
||||
|
||||
@@ -1188,6 +1188,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
/// prefer plaintext emails.
|
||||
///
|
||||
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if !chat.is_encrypted(context).await? {
|
||||
@@ -1751,6 +1752,7 @@ impl Chat {
|
||||
///
|
||||
/// If `update_msg_id` is set, that record is reused;
|
||||
/// if `update_msg_id` is None, a new record is created.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn prepare_msg_raw(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -3021,6 +3023,7 @@ pub async fn send_text_msg(
|
||||
}
|
||||
|
||||
/// Sends chat members a request to edit the given message's text.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
|
||||
let mut original_msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
@@ -4609,6 +4612,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
/// the copy contains a reference to the original message
|
||||
/// as well as to the original chat in case the original message gets deleted.
|
||||
/// Returns data needed to add a `SaveMessage` sync item.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn save_copy_in_self_talk(
|
||||
context: &Context,
|
||||
src_msg_id: MsgId,
|
||||
|
||||
@@ -2068,6 +2068,7 @@ pub(crate) async fn mark_contact_id_as_verified(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
|
||||
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::key::self_fingerprint;
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::net::tls::{SpkiHashStore, TlsSessionStore};
|
||||
use crate::net::tls::TlsSessionStore;
|
||||
use crate::peer_channels::Iroh;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::quota::QuotaInfo;
|
||||
@@ -308,13 +308,6 @@ pub struct InnerContext {
|
||||
/// TLS session resumption cache.
|
||||
pub(crate) tls_session_store: TlsSessionStore,
|
||||
|
||||
/// Store for TLS SPKI hashes.
|
||||
///
|
||||
/// Used to remember public keys
|
||||
/// of TLS certificates to accept them
|
||||
/// even after they expire.
|
||||
pub(crate) spki_hash_store: SpkiHashStore,
|
||||
|
||||
/// Iroh for realtime peer channels.
|
||||
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
|
||||
|
||||
@@ -518,7 +511,6 @@ impl Context {
|
||||
push_subscriber,
|
||||
push_subscribed: AtomicBool::new(false),
|
||||
tls_session_store: TlsSessionStore::new(),
|
||||
spki_hash_store: SpkiHashStore::new(),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
self_fingerprint: OnceLock::new(),
|
||||
self_public_key: Mutex::new(None),
|
||||
@@ -568,15 +560,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests deletion of all messages from chatmail relays.
|
||||
///
|
||||
/// Non-chatmail relays are excluded
|
||||
/// to avoid accidentally deleting emails
|
||||
/// from shared inboxes.
|
||||
pub async fn clear_all_relay_storage(&self) -> Result<()> {
|
||||
self.scheduler.clear_all_relay_storage().await
|
||||
}
|
||||
|
||||
/// Restarts the IO scheduler if it was running before
|
||||
/// when it is not running this is an no-op
|
||||
pub async fn restart_io_if_running(&self) {
|
||||
|
||||
@@ -89,9 +89,13 @@ mod test_chatlist_events {
|
||||
.get_matching(|evt| match evt {
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(ev_chat_id),
|
||||
} if ev_chat_id == &chat_id => {
|
||||
first_event_is_item.store(true, Ordering::Relaxed);
|
||||
true
|
||||
} => {
|
||||
if ev_chat_id == &chat_id {
|
||||
first_event_is_item.store(true, Ordering::Relaxed);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
EventType::ChatlistChanged => true,
|
||||
_ => false,
|
||||
|
||||
@@ -86,6 +86,7 @@ impl HtmlMsgParser {
|
||||
/// Function takes a raw mime-message string,
|
||||
/// searches for the main-text part
|
||||
/// and returns that as parser.html
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn from_bytes<'a>(
|
||||
context: &Context,
|
||||
rawmime: &'a [u8],
|
||||
@@ -119,6 +120,7 @@ impl HtmlMsgParser {
|
||||
/// Usually, there is at most one plain-text and one HTML-text part,
|
||||
/// multiple plain-text parts might be used for mailinglist-footers,
|
||||
/// therefore we use the first one.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn collect_texts_recursive<'a>(
|
||||
&'a mut self,
|
||||
context: &'a Context,
|
||||
|
||||
23
src/imap.rs
23
src/imap.rs
@@ -945,29 +945,6 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes all messages from IMAP folder.
|
||||
pub(crate) async fn delete_all_messages(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
if self.select_with_uidvalidity(context, folder).await? {
|
||||
self.add_flag_finalized_with_set("1:*", "\\Deleted").await?;
|
||||
self.selected_folder_needs_expunge = true;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves batch of messages identified by their UID from the currently
|
||||
/// selected folder to the target folder.
|
||||
async fn move_message_batch(
|
||||
|
||||
@@ -220,8 +220,6 @@ impl Client {
|
||||
alpn(addr.port()),
|
||||
logging_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
@@ -284,8 +282,6 @@ impl Client {
|
||||
"",
|
||||
tcp_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
@@ -314,8 +310,6 @@ impl Client {
|
||||
alpn(port),
|
||||
proxy_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
@@ -379,8 +373,6 @@ impl Client {
|
||||
"",
|
||||
proxy_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::net::session::SessionStream;
|
||||
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
||||
/// not necessarily sent by Delta Chat.
|
||||
/// - Chat-Is-Post-Message to skip it in background fetch or when it is > `DownloadLimit`.
|
||||
const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
||||
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
||||
MESSAGE-ID \
|
||||
DATE \
|
||||
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
|
||||
@@ -124,7 +124,7 @@ impl Session {
|
||||
}
|
||||
|
||||
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
|
||||
/// order of ascending UIDs.
|
||||
/// order of ascending delivery time to the server (INTERNALDATE).
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn prefetch(
|
||||
&mut self,
|
||||
@@ -142,10 +142,10 @@ impl Session {
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
msgs.insert(msg_uid, msg);
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Vec::from_iter(msgs))
|
||||
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
|
||||
}
|
||||
|
||||
/// Returns detailed message information in a multi-line text form.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn get_info(self, context: &Context) -> Result<String> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
|
||||
@@ -824,6 +825,7 @@ impl Message {
|
||||
///
|
||||
/// Currently this includes `additional_text`, but this may change in future, when the UIs show
|
||||
/// the necessary info themselves.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn get_text(&self) -> String {
|
||||
self.text.clone() + &self.additional_text
|
||||
}
|
||||
|
||||
@@ -1887,6 +1887,7 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
/// Render an MDN
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn render_mdn(&mut self) -> Result<MimePart<'static>> {
|
||||
// RFC 6522, this also requires the `report-type` parameter which is equal
|
||||
// to the MIME subtype of the second body part of the multipart/report
|
||||
|
||||
@@ -269,6 +269,7 @@ impl MimeMessage {
|
||||
///
|
||||
/// This method has some side-effects,
|
||||
/// such as saving blobs and saving found public keys to the database.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
|
||||
let mail = mailparse::parse_mail(body)?;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::{SpkiHashStore, TlsSessionStore};
|
||||
use crate::net::tls::TlsSessionStore;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -130,8 +130,6 @@ pub(crate) async fn connect_tls_inner(
|
||||
strict_tls: bool,
|
||||
alpn: &str,
|
||||
tls_session_store: &TlsSessionStore,
|
||||
spki_hash_store: &SpkiHashStore,
|
||||
sql: &Sql,
|
||||
) -> Result<impl SessionStream + 'static> {
|
||||
let use_sni = true;
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
@@ -143,8 +141,6 @@ pub(crate) async fn connect_tls_inner(
|
||||
alpn,
|
||||
tcp_stream,
|
||||
tls_session_store,
|
||||
spki_hash_store,
|
||||
sql,
|
||||
)
|
||||
.await?;
|
||||
Ok(tls_stream)
|
||||
|
||||
@@ -87,8 +87,6 @@ where
|
||||
"",
|
||||
proxy_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await?;
|
||||
Box::new(tls_stream)
|
||||
@@ -101,8 +99,6 @@ where
|
||||
"",
|
||||
tcp_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await?;
|
||||
Box::new(tls_stream)
|
||||
|
||||
@@ -171,6 +171,7 @@ pub enum ProxyConfig {
|
||||
}
|
||||
|
||||
/// Constructs HTTP/1.1 `CONNECT` request for HTTP(S) proxy.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
|
||||
// According to <https://datatracker.ietf.org/doc/html/rfc7230#section-5.4>
|
||||
// clients MUST send `Host:` header in HTTP/1.1 requests,
|
||||
@@ -319,6 +320,7 @@ impl ProxyConfig {
|
||||
/// config into `proxy_url` if `proxy_url` is unset or empty.
|
||||
///
|
||||
/// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn migrate_socks_config(sql: &Sql) -> Result<()> {
|
||||
if sql.get_raw_config("proxy_url").await?.is_none() {
|
||||
// Load legacy SOCKS5 settings.
|
||||
@@ -434,8 +436,6 @@ impl ProxyConfig {
|
||||
"",
|
||||
tcp_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await?;
|
||||
let auth = if let Some((username, password)) = &https_config.user_password {
|
||||
|
||||
@@ -6,20 +6,13 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::time;
|
||||
|
||||
use tokio_rustls::rustls;
|
||||
use tokio_rustls::rustls::client::ClientSessionStore;
|
||||
use tokio_rustls::rustls::server::ParsedCertificate;
|
||||
|
||||
mod danger;
|
||||
use danger::CustomCertificateVerifier;
|
||||
use danger::NoCertificateVerification;
|
||||
|
||||
mod spki;
|
||||
pub use spki::SpkiHashStore;
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub async fn wrap_tls<'a>(
|
||||
strict_tls: bool,
|
||||
hostname: &str,
|
||||
@@ -28,21 +21,10 @@ pub async fn wrap_tls<'a>(
|
||||
alpn: &str,
|
||||
stream: impl SessionStream + 'static,
|
||||
tls_session_store: &TlsSessionStore,
|
||||
spki_hash_store: &SpkiHashStore,
|
||||
sql: &Sql,
|
||||
) -> Result<impl SessionStream + 'a> {
|
||||
if strict_tls {
|
||||
let tls_stream = wrap_rustls(
|
||||
hostname,
|
||||
port,
|
||||
use_sni,
|
||||
alpn,
|
||||
stream,
|
||||
tls_session_store,
|
||||
spki_hash_store,
|
||||
sql,
|
||||
)
|
||||
.await?;
|
||||
let tls_stream =
|
||||
wrap_rustls(hostname, port, use_sni, alpn, stream, tls_session_store).await?;
|
||||
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
|
||||
Ok(boxed_stream)
|
||||
} else {
|
||||
@@ -112,7 +94,6 @@ impl TlsSessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub async fn wrap_rustls<'a>(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
@@ -120,11 +101,9 @@ pub async fn wrap_rustls<'a>(
|
||||
alpn: &str,
|
||||
stream: impl SessionStream + 'a,
|
||||
tls_session_store: &TlsSessionStore,
|
||||
spki_hash_store: &SpkiHashStore,
|
||||
sql: &Sql,
|
||||
) -> Result<impl SessionStream + 'a> {
|
||||
let root_cert_store =
|
||||
rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
let mut config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(root_cert_store)
|
||||
@@ -148,28 +127,20 @@ pub async fn wrap_rustls<'a>(
|
||||
config.resumption = resumption;
|
||||
config.enable_sni = use_sni;
|
||||
|
||||
config
|
||||
.dangerous()
|
||||
.set_certificate_verifier(Arc::new(CustomCertificateVerifier::new(
|
||||
spki_hash_store.get_spki_hash(hostname, sql).await?,
|
||||
)));
|
||||
// Do not verify certificates for hostnames starting with `_`.
|
||||
// They are used for servers with self-signed certificates, e.g. for local testing.
|
||||
// Hostnames starting with `_` can have only self-signed TLS certificates or wildcard certificates.
|
||||
// It is not possible to get valid non-wildcard TLS certificates because CA/Browser Forum requirements
|
||||
// explicitly state that domains should start with a letter, digit or hyphen:
|
||||
// https://github.com/cabforum/servercert/blob/24f38fd4765e019db8bb1a8c56bf63c7115ce0b0/docs/BR.md
|
||||
if hostname.starts_with("_") {
|
||||
config
|
||||
.dangerous()
|
||||
.set_certificate_verifier(Arc::new(NoCertificateVerification::new()));
|
||||
}
|
||||
|
||||
let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
|
||||
let name = tokio_rustls::rustls::pki_types::ServerName::try_from(hostname)?.to_owned();
|
||||
let tls_stream = tls.connect(name, stream).await?;
|
||||
|
||||
// Successfully connected.
|
||||
// Remember SPKI hash to accept it later if certificate expires.
|
||||
let (_io, client_connection) = tls_stream.get_ref();
|
||||
if let Some(end_entity) = client_connection
|
||||
.peer_certificates()
|
||||
.and_then(|certs| certs.first())
|
||||
{
|
||||
let now = time();
|
||||
let parsed_certificate = ParsedCertificate::try_from(end_entity)?;
|
||||
let spki = parsed_certificate.subject_public_key_info();
|
||||
spki_hash_store.save_spki(hostname, &spki, sql, now).await?;
|
||||
}
|
||||
|
||||
Ok(tls_stream)
|
||||
}
|
||||
|
||||
@@ -1,93 +1,26 @@
|
||||
//! Custom TLS verification.
|
||||
//!
|
||||
//! We want to accept expired certificates.
|
||||
//! Dangerous TLS implementation of accepting invalid certificates for Rustls.
|
||||
|
||||
use rustls::RootCertStore;
|
||||
use rustls::client::{verify_server_cert_signed_by_trust_anchor, verify_server_name};
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::server::ParsedCertificate;
|
||||
use tokio_rustls::rustls;
|
||||
|
||||
use crate::net::tls::spki::spki_hash;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct CustomCertificateVerifier {
|
||||
/// Root certificates.
|
||||
root_cert_store: RootCertStore,
|
||||
pub(super) struct NoCertificateVerification();
|
||||
|
||||
/// Expected SPKI hash as a base64 of SHA-256.
|
||||
spki_hash: Option<String>,
|
||||
}
|
||||
|
||||
impl CustomCertificateVerifier {
|
||||
pub(super) fn new(spki_hash: Option<String>) -> Self {
|
||||
let root_cert_store =
|
||||
RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
Self {
|
||||
root_cert_store,
|
||||
spki_hash,
|
||||
}
|
||||
impl NoCertificateVerification {
|
||||
pub(super) fn new() -> Self {
|
||||
Self()
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for CustomCertificateVerifier {
|
||||
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
intermediates: &[CertificateDer<'_>],
|
||||
server_name: &ServerName<'_>,
|
||||
// OCSP is a certificate revocation mechanism that is intentionally ignored.
|
||||
// It is practically not used and is essentially deprecated
|
||||
// in favor of Certificate Revocation Lists.
|
||||
// Let's Encrypt has disabled OCSP responders in 2025:
|
||||
// <https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life>.
|
||||
// Theoretically checking of stapled OCSP responses could be implemented,
|
||||
// but it is not interesting to implement it because it is not used
|
||||
// by the servers: <https://github.com/rustls/webpki/issues/217>.
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
now: UnixTime,
|
||||
_now: UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
let parsed_certificate = ParsedCertificate::try_from(end_entity)?;
|
||||
|
||||
let spki = parsed_certificate.subject_public_key_info();
|
||||
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
|
||||
if let ServerName::DnsName(dns_name) = server_name
|
||||
&& dns_name.as_ref().starts_with("_")
|
||||
{
|
||||
// Do not verify certificates for hostnames starting with `_`.
|
||||
// They are used for servers with self-signed certificates, e.g. for local testing.
|
||||
// Hostnames starting with `_` can have only self-signed TLS certificates or wildcard certificates.
|
||||
// It is not possible to get valid non-wildcard TLS certificates because CA/Browser Forum requirements
|
||||
// explicitly state that domains should start with a letter, digit or hyphen:
|
||||
// https://github.com/cabforum/servercert/blob/24f38fd4765e019db8bb1a8c56bf63c7115ce0b0/docs/BR.md
|
||||
} else if let Some(hash) = &self.spki_hash
|
||||
&& spki_hash(&spki) == *hash
|
||||
{
|
||||
// Last time we successfully connected to this hostname with TLS checks,
|
||||
// SPKI had this hash.
|
||||
// It does not matter if certificate has now expired.
|
||||
} else {
|
||||
// verify_server_cert_signed_by_trust_anchor does no revocation checking:
|
||||
// <https://docs.rs/rustls/0.23.37/rustls/client/fn.verify_server_cert_signed_by_trust_anchor.html>
|
||||
// We don't do it either.
|
||||
verify_server_cert_signed_by_trust_anchor(
|
||||
&parsed_certificate,
|
||||
&self.root_cert_store,
|
||||
intermediates,
|
||||
now,
|
||||
provider.signature_verification_algorithms.all,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Verify server name unconditionally.
|
||||
//
|
||||
// We do this even for self-signed certificates when hostname starts with `_`
|
||||
// so we don't try to connect to captive portals
|
||||
// and fail on MITM certificates if they are generated once
|
||||
// and reused for all hostnames.
|
||||
verify_server_name(&parsed_certificate, server_name)?;
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
//! SPKI hash storage.
|
||||
//!
|
||||
//! We store hashes of Subject Public Key Info from TLS certificates
|
||||
//! after successful connection to allow connecting when
|
||||
//! server certificate expires as long as the key is not changed.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use parking_lot::RwLock;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio_rustls::rustls::pki_types::SubjectPublicKeyInfoDer;
|
||||
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::time;
|
||||
|
||||
/// Calculates Subject Public Key Info SHA-256 hash and returns it as base64.
|
||||
///
|
||||
/// This is the same format as used in <https://www.rfc-editor.org/rfc/rfc7469>.
|
||||
/// You can calculate the same hash for any remote host with
|
||||
/// ```sh
|
||||
/// openssl s_client -connect "$HOST:993" -servername "$HOST" </dev/null 2>/dev/null |
|
||||
/// openssl x509 -pubkey -noout |
|
||||
/// openssl pkey -pubin -outform der |
|
||||
/// openssl dgst -sha256 -binary |
|
||||
/// openssl enc -base64
|
||||
/// ```
|
||||
pub fn spki_hash(spki: &SubjectPublicKeyInfoDer) -> String {
|
||||
let spki_hash = Sha256::digest(spki);
|
||||
base64::engine::general_purpose::STANDARD.encode(spki_hash)
|
||||
}
|
||||
|
||||
/// Write-through cache for SPKI hashes.
|
||||
#[derive(Debug)]
|
||||
pub struct SpkiHashStore {
|
||||
/// Map from hostnames to base64 of SHA-256 hashes.
|
||||
pub hash_store: RwLock<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
impl SpkiHashStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hash_store: RwLock::new(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns base64 of SPKI hash if we have previously successfully connected to given hostname.
|
||||
pub async fn get_spki_hash(&self, hostname: &str, sql: &Sql) -> Result<Option<String>> {
|
||||
if let Some(hash) = self.hash_store.read().get(hostname).cloned() {
|
||||
return Ok(Some(hash));
|
||||
}
|
||||
|
||||
match sql
|
||||
.query_row_optional(
|
||||
"SELECT spki_hash FROM tls_spki WHERE host=?",
|
||||
(hostname,),
|
||||
|row| {
|
||||
let spki_hash: String = row.get(0)?;
|
||||
Ok(spki_hash)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(hash) => {
|
||||
self.hash_store
|
||||
.write()
|
||||
.insert(hostname.to_string(), hash.clone());
|
||||
Ok(Some(hash))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves SPKI hash after successful connection.
|
||||
pub async fn save_spki(
|
||||
&self,
|
||||
hostname: &str,
|
||||
spki: &SubjectPublicKeyInfoDer<'_>,
|
||||
sql: &Sql,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
let hash = spki_hash(spki);
|
||||
self.hash_store
|
||||
.write()
|
||||
.insert(hostname.to_string(), hash.clone());
|
||||
sql.execute(
|
||||
"INSERT OR REPLACE INTO tls_spki (host, spki_hash, timestamp) VALUES (?, ?, ?)",
|
||||
(hostname, hash, timestamp),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes stale entries from SPKI storage.
|
||||
pub async fn cleanup(&self, sql: &Sql) -> Result<()> {
|
||||
let now = time();
|
||||
let removed_hosts = sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction
|
||||
.prepare("DELETE FROM tls_spki WHERE ? > timestamp + ? RETURNING host")?;
|
||||
let mut res = Vec::new();
|
||||
for row in stmt.query_map((now, 30 * 24 * 60 * 60), |row| {
|
||||
let host: String = row.get(0)?;
|
||||
Ok(host)
|
||||
})? {
|
||||
res.push(row?);
|
||||
}
|
||||
|
||||
// Fix timestamps that happen to be in the future
|
||||
// if we had clock set incorrectly when the timestamp was stored.
|
||||
// Otherwise entry may take more than 30 days to expire.
|
||||
transaction.execute(
|
||||
"UPDATE tls_spki SET timestamp = ?1 WHERE timestamp > ?1",
|
||||
(now,),
|
||||
)?;
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut lock = self.hash_store.write();
|
||||
for host in removed_hosts {
|
||||
// We may accidentally remove a host that was added
|
||||
// to the cache after SQL query but before we got
|
||||
// the write lock on `hash_store`.
|
||||
// It is unlikely and will only result
|
||||
// in additional SQL query next time.
|
||||
lock.remove(&host);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ pub struct PlainText {
|
||||
impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
@@ -714,6 +714,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
}
|
||||
|
||||
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::securejoin;
|
||||
use crate::stock_str::{self, backup_transfer_qr};
|
||||
|
||||
/// Create a QR code from any input data.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
|
||||
let all_size = 512.0;
|
||||
let qr_code_size = 416.0;
|
||||
@@ -175,6 +176,7 @@ async fn self_info(context: &Context) -> Result<(Option<Vec<u8>>, String, String
|
||||
Ok((avatar, displayname, addr, color))
|
||||
}
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn inner_generate_secure_join_qr_code(
|
||||
qrcode_description: &str,
|
||||
qrcode_content: &str,
|
||||
|
||||
@@ -857,6 +857,8 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
if transport_changed {
|
||||
info!(context, "Primary transport changed to {from_addr:?}.");
|
||||
context.sql.uncache_raw_config("configured_addr").await;
|
||||
|
||||
// Regenerate User ID in V4 keys.
|
||||
context.self_public_key.lock().await.take();
|
||||
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
|
||||
@@ -250,16 +250,6 @@ impl SchedulerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_all_relay_storage(&self) -> Result<()> {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.clear_all_relay_storage();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("IO is not started");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
@@ -358,7 +348,6 @@ async fn inbox_loop(
|
||||
let ImapConnectionHandlers {
|
||||
mut connection,
|
||||
stop_token,
|
||||
clear_storage_request_receiver,
|
||||
} = inbox_handlers;
|
||||
|
||||
let transport_id = connection.transport_id();
|
||||
@@ -397,14 +386,7 @@ async fn inbox_loop(
|
||||
}
|
||||
};
|
||||
|
||||
match inbox_fetch_idle(
|
||||
&ctx,
|
||||
&mut connection,
|
||||
session,
|
||||
&clear_storage_request_receiver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match inbox_fetch_idle(&ctx, &mut connection, session).await {
|
||||
Err(err) => warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed inbox fetch_idle: {err:#}."
|
||||
@@ -425,29 +407,11 @@ async fn inbox_loop(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn inbox_fetch_idle(
|
||||
ctx: &Context,
|
||||
imap: &mut Imap,
|
||||
mut session: Session,
|
||||
clear_storage_request_receiver: &Receiver<()>,
|
||||
) -> Result<Session> {
|
||||
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
// Clear IMAP storage on request.
|
||||
//
|
||||
// Only doing this for chatmail relays to avoid
|
||||
// accidentally deleting all emails in a shared mailbox.
|
||||
let should_clear_imap_storage =
|
||||
clear_storage_request_receiver.try_recv().is_ok() && session.is_chatmail();
|
||||
if should_clear_imap_storage {
|
||||
info!(ctx, "Transport {transport_id}: Clearing IMAP storage.");
|
||||
session.delete_all_messages(ctx, &imap.folder).await?;
|
||||
}
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
//
|
||||
// Always update if we just cleared IMAP storage.
|
||||
if (ctx.quota_needs_update(session.transport_id(), 60).await || should_clear_imap_storage)
|
||||
if ctx.quota_needs_update(session.transport_id(), 60).await
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session, &imap.folder).await
|
||||
{
|
||||
warn!(
|
||||
@@ -773,12 +737,6 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_all_relay_storage(&self) {
|
||||
for b in &self.inboxes {
|
||||
b.conn_state.clear_relay_storage();
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_smtp(&self) {
|
||||
self.smtp.interrupt();
|
||||
}
|
||||
@@ -912,13 +870,6 @@ struct SmtpConnectionHandlers {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ImapConnectionState {
|
||||
state: ConnectionState,
|
||||
|
||||
/// Channel to request clearing the folder.
|
||||
///
|
||||
/// IMAP loop receiving this should clear the folder
|
||||
/// on the next iteration if IMAP server is a chatmail relay
|
||||
/// and otherwise ignore the request.
|
||||
clear_storage_request_sender: Sender<()>,
|
||||
}
|
||||
|
||||
impl ImapConnectionState {
|
||||
@@ -930,13 +881,11 @@ impl ImapConnectionState {
|
||||
) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
let stop_token = CancellationToken::new();
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
let (clear_storage_request_sender, clear_storage_request_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = ImapConnectionHandlers {
|
||||
connection: Imap::new(context, transport_id, login_param, idle_interrupt_receiver)
|
||||
.await?,
|
||||
stop_token: stop_token.clone(),
|
||||
clear_storage_request_receiver,
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
@@ -945,10 +894,7 @@ impl ImapConnectionState {
|
||||
connectivity: handlers.connection.connectivity.clone(),
|
||||
};
|
||||
|
||||
let conn = ImapConnectionState {
|
||||
state,
|
||||
clear_storage_request_sender,
|
||||
};
|
||||
let conn = ImapConnectionState { state };
|
||||
|
||||
Ok((conn, handlers))
|
||||
}
|
||||
@@ -962,19 +908,10 @@ impl ImapConnectionState {
|
||||
fn stop(&self) {
|
||||
self.state.stop();
|
||||
}
|
||||
|
||||
/// Requests clearing relay storage and interrupts the inbox.
|
||||
fn clear_relay_storage(&self) {
|
||||
self.clear_storage_request_sender.try_send(()).ok();
|
||||
self.state.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ImapConnectionHandlers {
|
||||
connection: Imap,
|
||||
stop_token: CancellationToken,
|
||||
|
||||
/// Channel receiver to get requests to clear IMAP storage.
|
||||
pub(crate) clear_storage_request_receiver: Receiver<()>,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::tools::IsNoneOrEmpty;
|
||||
/// This escapes a bit more than actually needed by delta (e.g. also lines as "-- footer"),
|
||||
/// but for non-delta-compatibility, that seems to be better.
|
||||
/// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
|
||||
@@ -11,12 +11,11 @@ use crate::log::warn;
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::net::tls::{SpkiHashStore, TlsSessionStore, wrap_tls};
|
||||
use crate::net::tls::{TlsSessionStore, wrap_tls};
|
||||
use crate::net::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::time;
|
||||
use crate::transport::ConnectionCandidate;
|
||||
use crate::transport::ConnectionSecurity;
|
||||
@@ -112,26 +111,10 @@ async fn connection_attempt(
|
||||
);
|
||||
let res = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
connect_secure(
|
||||
resolved_addr,
|
||||
host,
|
||||
strict_tls,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await
|
||||
connect_secure(resolved_addr, host, strict_tls, &context.tls_session_store).await
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
connect_starttls(
|
||||
resolved_addr,
|
||||
host,
|
||||
strict_tls,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await
|
||||
connect_starttls(resolved_addr, host, strict_tls, &context.tls_session_store).await
|
||||
}
|
||||
ConnectionSecurity::Plain => connect_insecure(resolved_addr).await,
|
||||
};
|
||||
@@ -257,8 +240,6 @@ async fn connect_secure_proxy(
|
||||
alpn(port),
|
||||
proxy_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await?;
|
||||
let mut buffered_stream = BufStream::new(tls_stream);
|
||||
@@ -292,8 +273,6 @@ async fn connect_starttls_proxy(
|
||||
"",
|
||||
tcp_stream,
|
||||
&context.tls_session_store,
|
||||
&context.spki_hash_store,
|
||||
&context.sql,
|
||||
)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
@@ -320,8 +299,6 @@ async fn connect_secure(
|
||||
hostname: &str,
|
||||
strict_tls: bool,
|
||||
tls_session_store: &TlsSessionStore,
|
||||
spki_hash_store: &SpkiHashStore,
|
||||
sql: &Sql,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let tls_stream = connect_tls_inner(
|
||||
addr,
|
||||
@@ -329,8 +306,6 @@ async fn connect_secure(
|
||||
strict_tls,
|
||||
alpn(addr.port()),
|
||||
tls_session_store,
|
||||
spki_hash_store,
|
||||
sql,
|
||||
)
|
||||
.await?;
|
||||
let mut buffered_stream = BufStream::new(tls_stream);
|
||||
@@ -344,8 +319,6 @@ async fn connect_starttls(
|
||||
host: &str,
|
||||
strict_tls: bool,
|
||||
tls_session_store: &TlsSessionStore,
|
||||
spki_hash_store: &SpkiHashStore,
|
||||
sql: &Sql,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let use_sni = false;
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
@@ -363,8 +336,6 @@ async fn connect_starttls(
|
||||
"",
|
||||
tcp_stream,
|
||||
tls_session_store,
|
||||
spki_hash_store,
|
||||
sql,
|
||||
)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
@@ -874,14 +874,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
context
|
||||
.spki_hash_store
|
||||
.cleanup(&context.sql)
|
||||
.await
|
||||
.context("Failed to prune SPKI store")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
// Cleanup `imap` and `imap_sync` entries for deleted transports.
|
||||
//
|
||||
// Transports may be deleted directly or via sync messages,
|
||||
|
||||
@@ -2360,22 +2360,6 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 151)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE tls_spki (
|
||||
host TEXT NOT NULL UNIQUE,
|
||||
spki_hash TEXT NOT NULL, -- base64 of SPKI SHA-256 hash
|
||||
timestamp INTEGER NOT NULL -- timestamp of the last time we have seen this key
|
||||
) STRICT;
|
||||
-- Index on host column is created implicitly because of UNIQUE constraint.
|
||||
CREATE INDEX tls_spki_index_timestamp ON tls_spki (timestamp);
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -34,7 +34,8 @@ async fn test_additional_text_on_different_viewtypes() -> Result<()> {
|
||||
let (pre_message, _, _) = send_large_image_message(alice, a_group_id).await?;
|
||||
let msg = bob.recv_msg(&pre_message).await;
|
||||
assert_eq!(msg.text, "test".to_owned());
|
||||
assert_eq!(msg.get_text(), "test [Image – 146.12 KiB]".to_owned());
|
||||
assert!(msg.get_text().starts_with("test [Image – "));
|
||||
assert!(msg.get_text().ends_with(" KiB]"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::EventType;
|
||||
use crate::chat;
|
||||
use crate::chat::send_msg;
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact;
|
||||
use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PostMsgMetadata};
|
||||
use crate::message::{Message, MessageState, Viewtype, delete_msgs, markseen_msgs};
|
||||
@@ -402,9 +403,11 @@ async fn test_receive_pre_message_image() -> Result<()> {
|
||||
// test that metadata is correctly returned by methods
|
||||
assert_eq!(msg.get_post_message_viewtype(), Some(Viewtype::Image));
|
||||
// recoded image dimensions
|
||||
assert_eq!(msg.get_filebytes(bob).await?, Some(149632));
|
||||
assert_eq!(msg.get_height(), 1280);
|
||||
assert_eq!(msg.get_width(), 720);
|
||||
let n_bytes: usize = msg.get_filebytes(bob).await?.unwrap().try_into().unwrap();
|
||||
assert!(100_000 < n_bytes);
|
||||
assert!(n_bytes <= constants::BALANCED_IMAGE_BYTES);
|
||||
assert_eq!(msg.get_height(), 1920);
|
||||
assert_eq!(msg.get_width(), 1080);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -687,6 +687,7 @@ fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Opti
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) fn parse_receive_header(header: &str) -> String {
|
||||
let header = header.replace(&['\r', '\n'][..], "");
|
||||
let mut hop_info = String::from("Hop: ");
|
||||
|
||||
@@ -791,18 +791,7 @@ pub(crate) async fn sync_transports(
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let configured_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
for RemovedTransportData { addr, timestamp } in removed_transports {
|
||||
if *addr == configured_addr {
|
||||
continue;
|
||||
}
|
||||
modified |= transaction.execute(
|
||||
"DELETE FROM transports
|
||||
WHERE addr=? AND add_timestamp<=?",
|
||||
|
||||
@@ -111,12 +111,6 @@ pub struct WebxdcInfo {
|
||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
pub self_addr: String,
|
||||
|
||||
/// Define if the local user is the one who initially shared the webxdc application in the chat.
|
||||
pub is_app_sender: bool,
|
||||
|
||||
/// Define if the app runs in a broadcasting context.
|
||||
pub is_broadcast: bool,
|
||||
|
||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
||||
pub send_update_interval: usize,
|
||||
@@ -929,11 +923,6 @@ impl Message {
|
||||
let internet_access = is_integrated;
|
||||
|
||||
let self_addr = self.get_webxdc_self_addr(context).await?;
|
||||
let is_app_sender = self.from_id == ContactId::SELF;
|
||||
let chat = Chat::load_from_db(context, self.chat_id)
|
||||
.await
|
||||
.with_context(|| "Failed to load chat from the database")?;
|
||||
let is_broadcast = chat.typ == Chattype::InBroadcast || chat.typ == Chattype::OutBroadcast;
|
||||
|
||||
Ok(WebxdcInfo {
|
||||
name: if let Some(name) = manifest.name {
|
||||
@@ -972,8 +961,6 @@ impl Message {
|
||||
request_integration,
|
||||
internet_access,
|
||||
self_addr,
|
||||
is_app_sender,
|
||||
is_broadcast,
|
||||
send_update_interval: context.ratelimit.read().await.update_interval(),
|
||||
send_update_max_size: RECOMMENDED_FILE_SIZE as usize,
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::ephemeral;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::tools::{self, SystemTime};
|
||||
use crate::{message, sql};
|
||||
@@ -2196,42 +2195,3 @@ async fn test_self_addr_consistency() -> Result<()> {
|
||||
assert_eq!(db_msg.get_webxdc_self_addr(alice).await?, self_addr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_info_app_sender() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// Alice sends webxdc in a group chat
|
||||
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let alice_instance = send_webxdc_instance(alice, alice_chat_id).await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let alice_info = alice_instance.get_webxdc_info(alice).await?;
|
||||
assert!(alice_info.is_app_sender);
|
||||
assert!(!alice_info.is_broadcast);
|
||||
|
||||
// Bob receives group webxdc
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
let bob_info = bob_instance.get_webxdc_info(bob).await?;
|
||||
assert!(!bob_info.is_app_sender);
|
||||
assert!(!bob_info.is_broadcast);
|
||||
|
||||
// Alice sends webxdc to broadcast channel
|
||||
let alice_chat_id = create_broadcast(alice, "Broadcast".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let alice_instance = send_webxdc_instance(alice, alice_chat_id).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
let alice_info = alice_instance.get_webxdc_info(alice).await?;
|
||||
assert!(alice_info.is_app_sender);
|
||||
assert!(alice_info.is_broadcast);
|
||||
|
||||
// Bob receives broadcast webxdc
|
||||
let bob_instance = bob.recv_msg(&sent2).await;
|
||||
let bob_info = bob_instance.get_webxdc_info(bob).await?;
|
||||
assert!(!bob_info.is_app_sender);
|
||||
assert!(bob_info.is_broadcast);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
BIN
test-data/image/screenshot-huge.png
Normal file
BIN
test-data/image/screenshot-huge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 792 KiB |
Reference in New Issue
Block a user