Merge tag 'v1.126.0'

This commit is contained in:
link2xt
2023-10-22 15:16:11 +00:00
38 changed files with 561 additions and 321 deletions

View File

@@ -39,10 +39,6 @@ jobs:
- name: Check - name: Check
run: cargo check --workspace --all-targets --all-features run: cargo check --workspace --all-targets --all-features
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
- name: Check musl
run: scripts/zig-musl-check.sh
cargo_deny: cargo_deny:
name: cargo deny name: cargo deny
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -26,35 +26,26 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Build # Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install ziglang and wheel
run: pip install wheel ziglang==0.11.0
- name: Build deltachat-rpc-server binaries
run: sh scripts/zig-rpc-server.sh run: sh scripts/zig-rpc-server.sh
- name: Upload x86_64 binary - name: Build deltachat-rpc-server Python wheels and source package
uses: actions/upload-artifact@v3 run: scripts/wheel-rpc-server.py
with:
name: deltachat-rpc-server-x86_64
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload i686 binary - name: Upload dist directory
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: deltachat-rpc-server-i686 name: dist
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server path: dist/
if-no-files-found: error
- name: Upload aarch64 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-aarch64
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload armv7 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-armv7
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
if-no-files-found: error if-no-files-found: error
build_windows: build_windows:
@@ -116,15 +107,29 @@ jobs:
contents: write contents: write
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download built binaries - name: Download Linux binaries
uses: "actions/download-artifact@v3" uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Compose dist/ directory - name: Download win32 binary
run: | uses: actions/download-artifact@v3
mkdir dist with:
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do name: deltachat-rpc-server-win32.exe
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x" path: dist/deltachat-rpc-server-win32.exe
done
- name: Download win64 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-win64.exe
path: dist/deltachat-rpc-server-win32.exe
- name: Download macOS binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: dist/deltachat-rpc-server-x86_64-macos
- name: List downloaded artifacts - name: List downloaded artifacts
run: ls -l dist/ run: ls -l dist/

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/target /target
**/*.rs.bk **/*.rs.bk
/build /build
/dist
# ignore vi temporaries # ignore vi temporaries
*~ *~

View File

@@ -1,5 +1,32 @@
# Changelog # Changelog
## [1.126.0] - 2023-10-22
### API-Changes
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
### Features / Changes
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
### CI
- Build Python wheels for deltachat-rpc-server.
### Build system
- Strip release binaries.
- Workaround OpenSSL crate expecting libatomic to be available.
### Fixes
- Set `soft_heap_limit` on SQLite database.
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
## [1.125.0] - 2023-10-14 ## [1.125.0] - 2023-10-14
### API-Changes ### API-Changes
@@ -2915,3 +2942,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0 [1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1 [1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0 [1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0

14
Cargo.lock generated
View File

@@ -210,9 +210,9 @@ dependencies = [
[[package]] [[package]]
name = "async-imap" name = "async-imap"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8" checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"base64 0.21.3", "base64 0.21.3",
@@ -1085,7 +1085,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat" name = "deltachat"
version = "1.125.0" version = "1.126.0"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",
@@ -1162,7 +1162,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat-jsonrpc" name = "deltachat-jsonrpc"
version = "1.125.0" version = "1.126.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel", "async-channel",
@@ -1186,7 +1186,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat-repl" name = "deltachat-repl"
version = "1.125.0" version = "1.126.0"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",
@@ -1201,7 +1201,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat-rpc-server" name = "deltachat-rpc-server"
version = "1.125.0" version = "1.126.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"deltachat", "deltachat",
@@ -1226,7 +1226,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat_ffi" name = "deltachat_ffi"
version = "1.125.0" version = "1.126.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"deltachat", "deltachat",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat" name = "deltachat"
version = "1.125.0" version = "1.126.0"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
rust-version = "1.67" rust-version = "1.67"
@@ -24,6 +24,7 @@ lto = true
panic = 'abort' panic = 'abort'
opt-level = "z" opt-level = "z"
codegen-units = 1 codegen-units = 1
strip = true
[patch.crates-io] [patch.crates-io]
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" } quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }

View File

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

View File

@@ -891,7 +891,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT * - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
* is added as needed. * is added as needed.
* @param query_str An optional query for filtering the list. Only chats matching this query * @param query_str An optional query for filtering the list. Only chats matching this query
* are returned. Give NULL for no filtering. * are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
* the chatlist is filtered such that only chats with unread messages show up.
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID * @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
* are returned. Give 0 for no filtering. * are returned. Give 0 for no filtering.
* @return A chatlist as an dc_chatlist_t object. * @return A chatlist as an dc_chatlist_t object.
@@ -1706,24 +1707,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
* Create a new broadcast list. * Create a new broadcast list.
* *
* Broadcast lists are similar to groups on the sending device, * Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats * however, recipients get the messages in a read-only chat
* and will not be aware of other members. * and will see who the other members are.
* *
* Replies to broadcasts go only to the sender * For historical reasons, this function does not take a name directly,
* and not to all broadcast recipients. * instead you have to set the name using dc_set_chat_name()
* Moreover, replies will not appear in the broadcast list * after creating the broadcast list.
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
* *
* @memberof dc_context_t * @memberof dc_context_t
* @param context The context object. * @param context The context object.
@@ -2266,8 +2255,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
* the backup is not encrypted. * the backup is not encrypted.
* The backup contains all contacts, chats, images and other data and device independent settings. * The backup contains all contacts, chats, images and other data and device independent settings.
* The backup does not contain device dependent settings as ringtones or LED notification settings. * The backup does not contain device dependent settings as ringtones or LED notification settings.
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day, * The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
* the format is `delta-chat-<day>-<number>.tar`
* *
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase. * - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup * The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
@@ -3973,7 +3961,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
* Get the message time used for sorting. * Get the message time used for sorting.
* This function returns the timestamp that is used for sorting the message * This function returns the timestamp that is used for sorting the message
* into lists as returned e.g. by dc_get_chat_msgs(). * into lists as returned e.g. by dc_get_chat_msgs().
* This may be the reveived time, the sending time or another time. * This may be the received time, the sending time or another time.
* *
* To get the receiving time, use dc_msg_get_received_timestamp(). * To get the receiving time, use dc_msg_get_received_timestamp().
* To get the sending time, use dc_msg_get_timestamp(). * To get the sending time, use dc_msg_get_timestamp().

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat-jsonrpc" name = "deltachat-jsonrpc"
version = "1.125.0" version = "1.126.0"
description = "DeltaChat JSON-RPC API" description = "DeltaChat JSON-RPC API"
edition = "2021" edition = "2021"
default-run = "deltachat-jsonrpc-server" default-run = "deltachat-jsonrpc-server"

View File

@@ -815,24 +815,12 @@ impl CommandApi {
/// Create a new broadcast list. /// Create a new broadcast list.
/// ///
/// Broadcast lists are similar to groups on the sending device, /// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in normal one-to-one chats /// however, recipients get the messages in a read-only chat
/// and will not be aware of other members. /// and will see who the other members are.
/// ///
/// Replies to broadcasts go only to the sender /// For historical reasons, this function does not take a name directly,
/// and not to all broadcast recipients. /// instead you have to set the name using dc_set_chat_name()
/// Moreover, replies will not appear in the broadcast list /// after creating the broadcast list.
/// but in the one-to-one chat with the person answering.
///
/// The name and the image of the broadcast list is set automatically
/// and is visible to the sender only.
/// Not asking for these data allows more focused creation
/// and we bypass the question who will get which data.
/// Also, many users will have at most one broadcast list
/// so, a generic name and image is sufficient at the first place.
///
/// Later on, however, the name can be changed using dc_set_chat_name().
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
/// All in all, this is also what other messengers are doing here.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> { async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx) chat::create_broadcast_list(&ctx)
@@ -2071,6 +2059,23 @@ impl CommandApi {
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
} }
// send the chat's current set draft
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
let mut draft = draft;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
.await?
.to_u32();
Ok(msg_id)
} else {
Err(anyhow!(
"chat with id {} doesn't have draft message",
chat_id
))
}
}
} }
// Helper functions (to prevent code duplication) // Helper functions (to prevent code duplication)

View File

@@ -55,5 +55,5 @@
}, },
"type": "module", "type": "module",
"types": "dist/deltachat.d.ts", "types": "dist/deltachat.d.ts",
"version": "1.125.0" "version": "1.126.0"
} }

View File

@@ -148,7 +148,7 @@ describe("online tests", function () {
waitForEvent(dc, "IncomingMsg", accountId1), waitForEvent(dc, "IncomingMsg", accountId1),
]); ]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message"); dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arives at A and if it is encrypted // Check if answer arrives at A and if it is encrypted
await eventPromise2; await eventPromise2;
const messageId = ( const messageId = (

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat-repl" name = "deltachat-repl"
version = "1.125.0" version = "1.126.0"
license = "MPL-2.0" license = "MPL-2.0"
edition = "2021" edition = "2021"

View File

@@ -3,7 +3,6 @@ import logging
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Callable, Callable,
Coroutine,
Dict, Dict,
Iterable, Iterable,
Optional, Optional,
@@ -92,7 +91,7 @@ class Client:
"""Process events forever.""" """Process events forever."""
self.run_until(lambda _: False) self.run_until(lambda _: False)
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict: def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
"""Process events until the given callable evaluates to True. """Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the The callable should accept an AttrDict object representing the

View File

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

View File

@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
#### Upsides: #### Upsides:
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally. - With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.) - (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.) - It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161) [full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)

View File

@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
} }
} }
/** Opens a stanalone context (without an account manager) /** Opens a standalone context (without an account manager)
* automatically starts the event handler */ * automatically starts the event handler */
static open(cwd: string): Context { static open(cwd: string): Context {
const dbFile = join(cwd, 'db.sqlite') const dbFile = join(cwd, 'db.sqlite')

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
}, },
"types": "node/dist/index.d.ts", "types": "node/dist/index.d.ts",
"version": "1.125.0" "version": "1.126.0"
} }

View File

@@ -1 +1 @@
2023-10-14 2023-10-22

View File

@@ -10,6 +10,8 @@ and an own build machine.
- `deny.sh` runs `cargo deny` for all Rust code in the project. - `deny.sh` runs `cargo deny` for all Rust code in the project.
- `codespell.sh` spellchecks the source code using `codespell` tool.
- `../.github/workflows` contains jobs run by GitHub Actions. - `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs - `remote_tests_python.sh` rsyncs to a build machine and runs

View File

@@ -14,4 +14,4 @@ export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc cargo build -p deltachat_ffi --features jsonrpc
tox -c python -e py --devenv venv tox -c python -e py --devenv venv
env/bin/pip install --upgrade pip venv/bin/pip install --upgrade pip

162
scripts/wheel-rpc-server.py Executable file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Build Python wheels for deltachat-rpc-server.
Run scripts/zig-rpc-server.sh first."""
from pathlib import Path
from wheel.wheelfile import WheelFile
import tomllib
import tarfile
from io import BytesIO
def metadata_contents(version):
return f"""Metadata-Version: 2.1
Name: deltachat-rpc-server
Version: {version}
Summary: Delta Chat JSON-RPC server
"""
SETUP_PY = """
import sys
from setuptools import setup, find_packages
from distutils.cmd import Command
from setuptools.command.install import install
from setuptools.command.build import build
import subprocess
import platform
import tempfile
from zipfile import ZipFile
from pathlib import Path
import shutil
class BuildCommand(build):
def run(self):
tmpdir = tempfile.mkdtemp()
subprocess.run(
[
sys.executable,
"-m",
"pip",
"download",
"--no-input",
"--timeout",
"1000",
"--platform",
"musllinux_1_1_" + platform.machine(),
"--only-binary=:all:",
"deltachat-rpc-server",
],
cwd=tmpdir,
)
wheel_path = next(Path(tmpdir).glob("*.whl"))
with ZipFile(wheel_path, "r") as wheel:
exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
Path(exe_path).chmod(0o700)
wheel.extract("deltachat_rpc_server/__init__.py", "src")
shutil.rmtree(tmpdir)
return super().run()
setup(
cmdclass={"build": BuildCommand},
package_data={"deltachat_rpc_server": ["deltachat-rpc-server"]},
)
"""
def build_source_package(version):
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
contents = contents.encode()
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
tar_info.mode = 0o644
tar_info.size = len(contents)
pkg.addfile(tar_info, BytesIO(contents))
pack("PKG-INFO", metadata_contents(version))
pack(
"pyproject.toml",
"""[build-system]
requires = ["setuptools==68.2.2", "pip"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-server"
version = "1.125.0"
[project.scripts]
deltachat-rpc-server = "deltachat_rpc_server:main"
""",
)
pack("setup.py", SETUP_PY)
pack("src/deltachat_rpc_server/__init__.py", "")
def build_wheel(version, binary, tag):
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
with WheelFile(filename, "w") as wheel:
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
wheel.write("deltachat-rpc-server/README.md", "deltachat_rpc_server/README.md")
wheel.writestr(
"deltachat_rpc_server/__init__.py",
"""import os, sys
def main():
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server"), *sys.argv[1:]]
os.execv(argv[0], argv)
""",
)
wheel.write(
binary,
"deltachat_rpc_server/deltachat-rpc-server",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/METADATA",
metadata_contents(version),
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/WHEEL",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/entry_points.txt",
"[console_scripts]\ndeltachat-rpc-server = deltachat_rpc_server:main",
)
def main():
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
cargo_toml = tomllib.load(f)
version = cargo_toml["package"]["version"]
Path("dist").mkdir(exist_ok=True)
build_source_package(version)
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-linux",
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv7-linux",
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-linux",
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-i686-linux",
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
)
main()

View File

@@ -1,10 +1,20 @@
#!/usr/bin/env python #!/usr/bin/env python
# /// pyproject
# [run]
# dependencies = [
# "ziglang==0.11.0"
# ]
# ///
import os
import subprocess import subprocess
import sys import sys
import os
def flag_filter(flag: str) -> bool: def flag_filter(flag: str) -> bool:
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
if flag == "-latomic":
return False
if flag == "-lc": if flag == "-lc":
return False return False
if flag == "-Wl,-melf_i386": if flag == "-Wl,-melf_i386":
@@ -24,8 +34,23 @@ def main():
else: else:
zig_cpu_args = [] zig_cpu_args = []
# Disable atomics and use locks instead in OpenSSL.
# Zig toolchains do not provide atomics.
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
args += ["-DBROKEN_CLANG_ATOMICS"]
subprocess.run( subprocess.run(
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True [
sys.executable,
"-m",
"ziglang",
"cc",
"-target",
zig_target,
*zig_cpu_args,
*args,
],
check=True,
) )

View File

@@ -1,28 +0,0 @@
#!/bin/sh
#
# Run `cargo check` with musl libc.
# This requires `zig` to compile vendored openssl.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
ZIG_VERSION=0.11.0
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo check --release --target x86_64-unknown-linux-musl -p deltachat_ffi --features jsonrpc

View File

@@ -10,14 +10,6 @@ unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags. # Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0 export RUSTUP_TOOLCHAIN=1.72.0
ZIG_VERSION=0.11.0
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add i686-unknown-linux-musl rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \ CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \ TARGET_CC="$PWD/scripts/zig-cc" \
@@ -50,3 +42,9 @@ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \ LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \ ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
mkdir -p dist
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux

View File

@@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from <sender name>`.
Replies to messages MAY follow the typical `Re:`-format. Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain` The body MAY contain text which MUST have the content type `text/plain`
or `mulipart/alternative` containing `text/plain`. or `multipart/alternative` containing `text/plain`.
The text MAY be divided into a user-text-part and a footer-part using the The text MAY be divided into a user-text-part and a footer-part using the
line `-- ` (minus, minus, space, lineend). line `-- ` (minus, minus, space, lineend).

View File

@@ -6126,22 +6126,40 @@ mod tests {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(), get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
) )
.await?; .await?;
let chat = Chat::load_from_db(&alice, broadcast_id).await?; set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
assert_eq!(chat.typ, Chattype::Broadcast); {
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await); let chat = Chat::load_from_db(&alice, broadcast_id).await?;
assert!(!chat.is_self_talk()); assert_eq!(chat.typ, Chattype::Broadcast);
assert_eq!(chat.name, "Broadcast list");
assert!(!chat.is_self_talk());
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?; send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
let msg = alice.get_last_msg().await; let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, chat.id); assert_eq!(msg.chat_id, chat.id);
}
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; {
assert_eq!(msg.get_text(), "ola!"); let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data assert_eq!(msg.get_text(), "ola!");
let chat = Chat::load_from_db(&bob, msg.chat_id).await?; assert_eq!(msg.subject, "Broadcast list");
assert_eq!(chat.typ, Chattype::Single); assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
assert_eq!(chat.id, chat_bob.id); let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert!(!chat.is_self_talk()); assert_eq!(chat.typ, Chattype::Mailinglist);
assert_ne!(chat.id, chat_bob.id);
assert_eq!(chat.name, "Broadcast list");
assert!(!chat.is_self_talk());
}
{
// Alice changes the name:
set_chat_name(&alice, broadcast_id, "My great broadcast").await?;
let sent = alice.send_text(broadcast_id, "I changed the title!").await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.subject, "Re: My great broadcast");
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(bob_chat.name, "My great broadcast");
}
Ok(()) Ok(())
} }

View File

@@ -1,6 +1,7 @@
//! # Chat list module. //! # Chat list module.
use anyhow::{ensure, Context as _, Result}; use anyhow::{ensure, Context as _, Result};
use once_cell::sync::Lazy;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{ use crate::constants::{
@@ -15,6 +16,10 @@ use crate::stock_str;
use crate::summary::Summary; use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty; use crate::tools::IsNoneOrEmpty;
/// Regex to find out if a query should filter by unread messages.
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
/// An object representing a single chatlist in memory. /// An object representing a single chatlist in memory.
/// ///
/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them. /// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
@@ -78,7 +83,8 @@ impl Chatlist {
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT /// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
/// is added as needed. /// is added as needed.
/// `query`: An optional query for filtering the list. Only chats matching this query /// `query`: An optional query for filtering the list. Only chats matching this query
/// are returned. /// are returned. When `is:unread` is contained in the query, the chatlist is
/// filtered such that only chats with unread messages show up.
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned. /// are returned.
pub async fn try_load( pub async fn try_load(
@@ -172,8 +178,10 @@ impl Chatlist {
) )
.await? .await?
} else if let Some(query) = query { } else if let Some(query) = query {
let query = query.trim().to_string(); let mut query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query"); ensure!(!query.is_empty(), "query mustn't be empty");
let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
// allow searching over special names that may change at any time // allow searching over special names that may change at any time
// when the ui calls set_stock_translation() // when the ui calls set_stock_translation()
@@ -198,9 +206,10 @@ impl Chatlist {
WHERE c.id>9 AND c.id!=?2 WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1 AND c.blocked!=1
AND c.name LIKE ?3 AND c.name LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd), (MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row, process_row,
process_rows, process_rows,
) )
@@ -462,7 +471,8 @@ pub async fn get_last_message_for_chat(
mod tests { mod tests {
use super::*; use super::*;
use crate::chat::{ use crate::chat::{
create_group_chat, get_chat_contacts, remove_contact_from_chat, ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
}; };
use crate::message::Viewtype; use crate::message::Viewtype;
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
@@ -471,7 +481,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() { async fn test_try_load() {
let t = TestContext::new().await; let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await .await
.unwrap(); .unwrap();
@@ -510,6 +520,31 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
// receive a message from alice
let alice = TestContext::new_alice().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", "bob@example.net")
.await
.unwrap(),
)
.await
.unwrap();
send_text_msg(&alice, alice_chat_id, "hi".into())
.await
.unwrap();
let sent_msg = alice.pop_sent_msg().await;
t.recv_msg(&sent_msg).await;
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
.await
.unwrap();
assert!(chats.len() == 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None) let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await .await
.unwrap(); .unwrap();

View File

@@ -38,7 +38,7 @@ use crate::tools::{duration_to_str, time};
/// ///
/// # Examples /// # Examples
/// ///
/// Creating a new unecrypted database: /// Creating a new unencrypted database:
/// ///
/// ``` /// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap(); /// # let rt = tokio::runtime::Runtime::new().unwrap();

View File

@@ -82,7 +82,6 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \ MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\ X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])"; )])";
const JUST_UID: &str = "(UID)";
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])"; const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])"; const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
@@ -627,18 +626,6 @@ impl Imap {
// UIDVALIDITY is modified, reset highest seen MODSEQ. // UIDVALIDITY is modified, reset highest seen MODSEQ.
set_modseq(context, folder, 0).await?; set_modseq(context, folder, 0).await?;
if mailbox.exists == 0 {
info!(context, "Folder {folder:?} is empty.");
// set uid_next=1 for empty folders.
// If we do not do this here, we'll miss the first message
// as we will get in here again and fetch from uid_next then.
// Also, the "fall back to fetching" below would need a non-zero mailbox.exists to work.
set_uid_next(context, folder, 1).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
return Ok(false);
}
// ============== uid_validity has changed or is being set the first time. ============== // ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = match mailbox.uid_next { let new_uid_next = match mailbox.uid_next {
@@ -646,25 +633,35 @@ impl Imap {
None => { None => {
warn!( warn!(
context, context,
"IMAP folder {folder:?} has no uid_next, fall back to fetching." "SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
); );
// note that we use fetch by sequence number
// and thus we only need to get exactly the
// last-index message.
let set = format!("{}", mailbox.exists);
let mut list = session
.inner
.fetch(set, JUST_UID)
.await
.context("Error fetching UID")?;
let mut new_last_seen_uid = None; // RFC 3501 says STATUS command SHOULD NOT be used
while let Some(fetch) = list.try_next().await? { // on the currently selected mailbox because the same
if fetch.message == mailbox.exists && fetch.uid.is_some() { // information can be obtained by other means,
new_last_seen_uid = fetch.uid; // such as reading SELECT response.
} //
// However, it also says that UIDNEXT is REQUIRED
// in the SELECT response and if we are here,
// it is actually not returned.
//
// In particular, Winmail Pro Mail Server 5.1.0616
// never returns UIDNEXT in SELECT response,
// but responds to "SELECT INBOX (UIDNEXT)" command.
let status = session
.inner
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
if let Some(uid_next) = status.uid_next {
uid_next
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Set UIDNEXT to 1 as a last resort fallback.
1
} }
new_last_seen_uid.context("select: failed to fetch")? + 1
} }
}; };

View File

@@ -60,8 +60,7 @@ pub enum ImexMode {
/// Export a backup to the directory given as `path` with the given `passphrase`. /// Export a backup to the directory given as `path` with the given `passphrase`.
/// The backup contains all contacts, chats, images and other data and device independent settings. /// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings. /// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day, /// The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
/// the format is `delta-chat-<day>-<number>.tar`
ExportBackup = 11, ExportBackup = 11,
/// `path` is the file (not: directory) to import. The file is normally /// `path` is the file (not: directory) to import. The file is normally
@@ -130,7 +129,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
&& (newest_backup_name.is_empty() || name > newest_backup_name) && (newest_backup_name.is_empty() || name > newest_backup_name)
{ {
// We just use string comparison to determine which backup is newer. // We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar // This works fine because the filenames have the form `delta-chat-backup-2023-10-18-00-foo@example.com.tar`
newest_backup_path = Some(path); newest_backup_path = Some(path);
newest_backup_name = name; newest_backup_name = name;
} }
@@ -486,7 +485,11 @@ async fn import_backup(
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be /// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded, /// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete. /// it can be renamed to dest_path. This guarantees that the backup is complete.
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> { fn get_next_backup_path(
folder: &Path,
addr: &str,
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder); let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0) let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
.context("can't get next backup path")? .context("can't get next backup path")?
@@ -497,13 +500,13 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
// 64 backup files per day should be enough for everyone // 64 backup files per day should be enough for everyone
for i in 0..64 { for i in 0..64 {
let mut tempdbfile = folder.clone(); let mut tempdbfile = folder.clone();
tempdbfile.push(format!("{stem}-{i:02}.db")); tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
let mut tempfile = folder.clone(); let mut tempfile = folder.clone();
tempfile.push(format!("{stem}-{i:02}.tar.part")); tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
let mut destfile = folder.clone(); let mut destfile = folder.clone();
destfile.push(format!("{stem}-{i:02}.tar")); destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() { if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
return Ok((tempdbfile, tempfile, destfile)); return Ok((tempdbfile, tempfile, destfile));
@@ -518,7 +521,8 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> { async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible) // get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time(); let now = time();
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now)?; let self_addr = context.get_primary_self_addr().await?;
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
let _d1 = DeleteOnDrop(temp_db_path.clone()); let _d1 = DeleteOnDrop(temp_db_path.clone());
let _d2 = DeleteOnDrop(temp_path.clone()); let _d2 = DeleteOnDrop(temp_path.clone());

View File

@@ -415,7 +415,9 @@ impl<'a> MimeFactory<'a> {
return Ok(self.msg.subject.clone()); return Ok(self.msg.subject.clone());
} }
if chat.typ == Chattype::Group && quoted_msg_subject.is_none_or_empty() { if (chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast)
&& quoted_msg_subject.is_none_or_empty()
{
let re = if self.in_reply_to.is_empty() { let re = if self.in_reply_to.is_empty() {
"" ""
} else { } else {
@@ -424,15 +426,13 @@ impl<'a> MimeFactory<'a> {
return Ok(format!("{}{}", re, chat.name)); return Ok(format!("{}{}", re, chat.name));
} }
if chat.typ != Chattype::Broadcast { let parent_subject = if quoted_msg_subject.is_none_or_empty() {
let parent_subject = if quoted_msg_subject.is_none_or_empty() { chat.param.get(Param::LastSubject)
chat.param.get(Param::LastSubject) } else {
} else { quoted_msg_subject.as_deref()
quoted_msg_subject.as_deref() };
}; if let Some(last_subject) = parent_subject {
if let Some(last_subject) = parent_subject { return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
} }
let self_name = &match context.get_config(Config::Displayname).await? { let self_name = &match context.get_config(Config::Displayname).await? {
@@ -594,6 +594,15 @@ impl<'a> MimeFactory<'a> {
)); ));
} }
if let Loaded::Message { chat } = &self.loaded {
if chat.typ == Chattype::Broadcast {
headers.protected.push(Header::new(
"List-ID".into(),
format!("{} <{}>", chat.name, chat.grpid),
));
}
}
// Non-standard headers. // Non-standard headers.
headers headers
.unprotected .unprotected
@@ -2341,7 +2350,7 @@ mod tests {
// Now Bob can send an encrypted message to Alice. // Now Bob can send an encrypted message to Alice.
let mut msg = Message::new(Viewtype::File); let mut msg = Message::new(Viewtype::File);
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need // Long messages are truncated and MimeMessage::decoded_data is set for them. We need
// decoded_data to check presense of the necessary headers. // decoded_data to check presence of the necessary headers.
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1)); msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None) msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
.await?; .await?;

View File

@@ -126,23 +126,6 @@ pub(crate) enum AvatarAction {
Change(String), Change(String),
} }
#[derive(Debug, PartialEq)]
pub(crate) enum MailinglistType {
/// The message belongs to a mailing list and has a `ListId:`-header
/// that should be used to get a unique id.
ListIdBased,
/// The message belongs to a mailing list, but there is no `ListId:`-header;
/// `Sender:`-header should be used to get a unique id.
/// This method is used by implementations as Majordomo.
/// Note, that the `Sender:` header alone is not sufficient to detect these lists,
/// `get_mailinglist_type()` check additional conditions therefore.
SenderBased,
/// The message does not belong to a mailing list.
None,
}
/// System message type. /// System message type.
#[derive( #[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
@@ -1348,26 +1331,28 @@ impl MimeMessage {
self.parts.push(part); self.parts.push(part);
} }
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType { pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
if self.get_header(HeaderDef::ListId).is_some() { if let Some(list_id) = self.get_header(HeaderDef::ListId) {
return MailinglistType::ListIdBased; // The message belongs to a mailing list and has a `ListId:`-header
} else if self.get_header(HeaderDef::Sender).is_some() { // that should be used to get a unique id.
return Some(list_id);
} else if let Some(sender) = self.get_header(HeaderDef::Sender) {
// the `Sender:`-header alone is no indicator for mailing list // the `Sender:`-header alone is no indicator for mailing list
// as also used for bot-impersonation via `set_override_sender_name()` // as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get_header(HeaderDef::Precedence) { if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
if precedence == "list" || precedence == "bulk" { if precedence == "list" || precedence == "bulk" {
return MailinglistType::SenderBased; // The message belongs to a mailing list, but there is no `ListId:`-header;
// `Sender:`-header is be used to get a unique id.
// This method is used by implementations as Majordomo.
return Some(sender);
} }
} }
} }
MailinglistType::None None
} }
pub(crate) fn is_mailinglist_message(&self) -> bool { pub(crate) fn is_mailinglist_message(&self) -> bool {
match self.get_mailinglist_type() { self.get_mailinglist_header().is_some()
MailinglistType::ListIdBased | MailinglistType::SenderBased => true,
MailinglistType::None => false,
}
} }
pub fn repl_msg_by_error(&mut self, error_msg: &str) { pub fn repl_msg_by_error(&mut self, error_msg: &str) {

View File

@@ -21,7 +21,7 @@ use crate::stock_str;
/// Type of the public key stored inside the peerstate. /// Type of the public key stored inside the peerstate.
#[derive(Debug)] #[derive(Debug)]
pub enum PeerstateKeyType { pub enum PeerstateKeyType {
/// Pubilc key sent in the `Autocrypt-Gossip` header. /// Public key sent in the `Autocrypt-Gossip` header.
GossipKey, GossipKey,
/// Public key sent in the `Autocrypt` header. /// Public key sent in the `Autocrypt` header.

View File

@@ -28,9 +28,7 @@ use crate::log::LogExt;
use crate::message::{ use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype, self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
}; };
use crate::mimeparser::{ use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
};
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus}; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction}; use crate::reaction::{set_msg_reaction, Reaction};
@@ -665,45 +663,23 @@ async fn add_parts(
if chat_id.is_none() { if chat_id.is_none() {
// check if the message belongs to a mailing list // check if the message belongs to a mailing list
match mime_parser.get_mailinglist_type() { if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
MailinglistType::ListIdBased => { if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
if let Some(list_id) = mime_parser.get_header(HeaderDef::ListId) { context,
if let Some((new_chat_id, new_chat_id_blocked)) = allow_creation,
create_or_lookup_mailinglist( mailinglist_header,
context, mime_parser,
allow_creation, )
list_id, .await?
mime_parser, {
) chat_id = Some(new_chat_id);
.await? chat_id_blocked = new_chat_id_blocked;
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
} }
MailinglistType::SenderBased => {
if let Some(sender) = mime_parser.get_header(HeaderDef::Sender) {
if let Some((new_chat_id, new_chat_id_blocked)) =
create_or_lookup_mailinglist(
context,
allow_creation,
sender,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
MailinglistType::None => {}
} }
} }
if let Some(chat_id) = chat_id { if let Some(chat_id) = chat_id {
apply_mailinglist_changes(context, mime_parser, chat_id).await?; apply_mailinglist_changes(context, mime_parser, sent_timestamp, chat_id).await?;
} }
// if contact renaming is prevented (for mailinglists and bots), // if contact renaming is prevented (for mailinglists and bots),
@@ -1972,6 +1948,8 @@ async fn apply_group_changes(
Ok(better_msg) Ok(better_msg)
} }
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
/// Create or lookup a mailing list chat. /// Create or lookup a mailing list chat.
/// ///
/// `list_id_header` contains the Id that must be used for the mailing list /// `list_id_header` contains the Id that must be used for the mailing list
@@ -1988,23 +1966,71 @@ async fn create_or_lookup_mailinglist(
list_id_header: &str, list_id_header: &str,
mime_parser: &MimeMessage, mime_parser: &MimeMessage,
) -> Result<Option<(ChatId, Blocked)>> { ) -> Result<Option<(ChatId, Blocked)>> {
static LIST_ID: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap()); let listid = match LIST_ID_REGEX.captures(list_id_header) {
let (mut name, listid) = match LIST_ID.captures(list_id_header) { Some(cap) => cap[2].trim().to_string(),
Some(cap) => (cap[1].trim().to_string(), cap[2].trim().to_string()), None => list_id_header
None => ( .trim()
"".to_string(), .trim_start_matches('<')
list_id_header .trim_end_matches('>')
.trim() .to_string(),
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
),
}; };
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? { if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked))); return Ok(Some((chat_id, blocked)));
} }
let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
if allow_creation {
// list does not exist but should be created
let param = mime_parser.list_post.as_ref().map(|list_post| {
let mut p = Params::new();
p.set(Param::ListPost, list_post);
p.to_string()
});
let is_bot = context.get_config_bool(Config::Bot).await?;
let blocked = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let chat_id = ChatId::create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
&name,
blocked,
ProtectionStatus::Unprotected,
param,
)
.await
.with_context(|| {
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
} else {
info!(context, "Creating list forbidden by caller.");
Ok(None)
}
}
#[allow(clippy::indexing_slicing)]
fn compute_mailinglist_name(
list_id_header: &str,
listid: &str,
mime_parser: &MimeMessage,
) -> String {
let mut name = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[1].trim().to_string(),
None => "".to_string(),
};
// for mailchimp lists, the name in `ListId` is just a long number. // for mailchimp lists, the name in `ListId` is just a long number.
// a usable name for these lists is in the `From` header // a usable name for these lists is in the `From` header
// and we can detect these lists by a unique `ListId`-suffix. // and we can detect these lists by a unique `ListId`-suffix.
@@ -2048,50 +2074,14 @@ async fn create_or_lookup_mailinglist(
// 51231231231231231231231232869f58.xing.com -> xing.com // 51231231231231231231231232869f58.xing.com -> xing.com
static PREFIX_32_CHARS_HEX: Lazy<Regex> = static PREFIX_32_CHARS_HEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap()); Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(&listid) { if let Some(cap) = PREFIX_32_CHARS_HEX.captures(listid) {
name = cap[2].to_string(); name = cap[2].to_string();
} else { } else {
name = listid.clone(); name = listid.to_string();
} }
} }
if allow_creation { strip_rtlo_characters(&name)
// list does not exist but should be created
let param = mime_parser.list_post.as_ref().map(|list_post| {
let mut p = Params::new();
p.set(Param::ListPost, list_post);
p.to_string()
});
let is_bot = context.get_config_bool(Config::Bot).await?;
let blocked = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let chat_id = ChatId::create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
&name,
blocked,
ProtectionStatus::Unprotected,
param,
)
.await
.with_context(|| {
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
} else {
info!(context, "Creating list forbidden by caller.");
Ok(None)
}
} }
/// Set ListId param on the contact and ListPost param the chat. /// Set ListId param on the contact and ListPost param the chat.
@@ -2100,9 +2090,10 @@ async fn create_or_lookup_mailinglist(
async fn apply_mailinglist_changes( async fn apply_mailinglist_changes(
context: &Context, context: &Context,
mime_parser: &MimeMessage, mime_parser: &MimeMessage,
sent_timestamp: i64,
chat_id: ChatId, chat_id: ChatId,
) -> Result<()> { ) -> Result<()> {
let Some(list_post) = &mime_parser.list_post else { let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
return Ok(()); return Ok(());
}; };
@@ -2112,6 +2103,24 @@ async fn apply_mailinglist_changes(
} }
let listid = &chat.grpid; let listid = &chat.grpid;
let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
if chat.name != new_name
&& chat_id
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
.await?
{
info!(context, "Updating listname for chat {chat_id}.");
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}
let Some(list_post) = &mime_parser.list_post else {
return Ok(());
};
let list_post = match ContactAddress::new(list_post) { let list_post = match ContactAddress::new(list_post) {
Ok(list_post) => list_post, Ok(list_post) => list_post,
Err(err) => { Err(err) => {

View File

@@ -686,6 +686,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
PRAGMA secure_delete=on; PRAGMA secure_delete=on;
PRAGMA busy_timeout = 0; -- fail immediately PRAGMA busy_timeout = 0; -- fail immediately
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
PRAGMA foreign_keys=on; PRAGMA foreign_keys=on;
", ",
)?; )?;

View File

@@ -1546,7 +1546,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
// a subsequent call to update_device_chats() must not re-add manally deleted messages or chats // a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
t.update_device_chats().await.unwrap(); t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);

View File

@@ -268,7 +268,7 @@ pub(crate) fn create_id() -> String {
/// Function generates a Message-ID that can be used for a new outgoing message. /// Function generates a Message-ID that can be used for a new outgoing message.
/// - this function is called for all outgoing messages. /// - this function is called for all outgoing messages.
/// - the message ID should be globally unique /// - the message ID should be globally unique
/// - do not add a counter or any private data as this leaks information unncessarily /// - do not add a counter or any private data as this leaks information unnecessarily
pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String { pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr let hostname = from_addr
.find('@') .find('@')