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
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:
name: cargo deny
runs-on: ubuntu-latest

View File

@@ -26,35 +26,26 @@ jobs:
steps:
- 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
- name: Upload x86_64 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-x86_64
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Build deltachat-rpc-server Python wheels and source package
run: scripts/wheel-rpc-server.py
- name: Upload i686 binary
- name: Upload dist directory
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-i686
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
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
name: dist
path: dist/
if-no-files-found: error
build_windows:
@@ -116,15 +107,29 @@ jobs:
contents: write
runs-on: "ubuntu-latest"
steps:
- name: Download built binaries
uses: "actions/download-artifact@v3"
- name: Download Linux binaries
uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Compose dist/ directory
run: |
mkdir dist
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
done
- name: Download win32 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-win32.exe
path: dist/deltachat-rpc-server-win32.exe
- 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
run: ls -l dist/

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.125.0"
version = "1.126.0"
description = "Deltachat FFI"
edition = "2018"
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
* is added as needed.
* @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
* are returned. Give 0 for no filtering.
* @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.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
* however, recipients get the messages in a read-only chat
* and will see who the other members are.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in 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.
* For historical reasons, this function does not take a name directly,
* instead you have to set the name using dc_set_chat_name()
* after creating the broadcast list.
*
* @memberof dc_context_t
* @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 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 name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
* the format is `delta-chat-<day>-<number>.tar`
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
*
* - **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
@@ -3973,7 +3961,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
* Get the message time used for sorting.
* This function returns the timestamp that is used for sorting the message
* 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 sending time, use dc_msg_get_timestamp().

View File

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

View File

@@ -815,24 +815,12 @@ impl CommandApi {
/// Create a new broadcast list.
///
/// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in normal one-to-one chats
/// and will not be aware of other members.
/// however, recipients get the messages in a read-only chat
/// and will see who the other members are.
///
/// Replies to broadcasts go only to the sender
/// and not to all broadcast recipients.
/// Moreover, replies will not appear in 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.
/// For historical reasons, this function does not take a name directly,
/// instead you have to set the name using dc_set_chat_name()
/// after creating the broadcast list.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
@@ -2071,6 +2059,23 @@ impl CommandApi {
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)

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"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),
]);
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;
const messageId = (

View File

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

View File

@@ -3,7 +3,6 @@ import logging
from typing import (
TYPE_CHECKING,
Callable,
Coroutine,
Dict,
Iterable,
Optional,
@@ -92,7 +91,7 @@ class Client:
"""Process events forever."""
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.
The callable should accept an AttrDict object representing the

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.125.0"
version = "1.126.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
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:
- 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.)
- 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)

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 */
static open(cwd: string): Context {
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"
},
"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.
- `codespell.sh` spellchecks the source code using `codespell` tool.
- `../.github/workflows` contains jobs run by GitHub Actions.
- `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
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
# /// pyproject
# [run]
# dependencies = [
# "ziglang==0.11.0"
# ]
# ///
import os
import subprocess
import sys
import os
def flag_filter(flag: str) -> bool:
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
if flag == "-latomic":
return False
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
@@ -24,8 +34,23 @@ def main():
else:
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(
["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.
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
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" \
ZIG_TARGET="aarch64-linux-musl" \
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.
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
line `-- ` (minus, minus, space, lineend).

View File

@@ -6126,22 +6126,40 @@ mod tests {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
assert_eq!(chat.typ, Chattype::Broadcast);
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
assert!(!chat.is_self_talk());
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
{
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
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?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, chat.id);
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, chat.id);
}
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "ola!");
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(chat.id, chat_bob.id);
assert!(!chat.is_self_talk());
{
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "ola!");
assert_eq!(msg.subject, "Broadcast list");
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
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(())
}

View File

@@ -1,6 +1,7 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
use once_cell::sync::Lazy;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -15,6 +16,10 @@ use crate::stock_str;
use crate::summary::Summary;
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.
///
/// 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
/// is added as needed.
/// `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
/// are returned.
pub async fn try_load(
@@ -172,8 +178,10 @@ impl Chatlist {
)
.await?
} else if let Some(query) = query {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
let mut query = query.trim().to_string();
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
// when the ui calls set_stock_translation()
@@ -198,9 +206,10 @@ impl Chatlist {
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
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
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_rows,
)
@@ -462,7 +471,8 @@ pub async fn get_last_message_for_chat(
mod tests {
use super::*;
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::receive_imf::receive_imf;
@@ -471,7 +481,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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")
.await
.unwrap();
@@ -510,6 +520,31 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
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)
.await
.unwrap();

View File

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

View File

@@ -82,7 +82,6 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])";
const JUST_UID: &str = "(UID)";
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
@@ -627,18 +626,6 @@ impl Imap {
// UIDVALIDITY is modified, reset highest seen MODSEQ.
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. ==============
let new_uid_next = match mailbox.uid_next {
@@ -646,25 +633,35 @@ impl Imap {
None => {
warn!(
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;
while let Some(fetch) = list.try_next().await? {
if fetch.message == mailbox.exists && fetch.uid.is_some() {
new_last_seen_uid = fetch.uid;
}
// RFC 3501 says STATUS command SHOULD NOT be used
// on the currently selected mailbox because the same
// information can be obtained by other means,
// 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`.
/// 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 name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
/// the format is `delta-chat-<day>-<number>.tar`
/// The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
ExportBackup = 11,
/// `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)
{
// 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_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
/// 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.
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 stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
.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
for i in 0..64 {
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();
tempfile.push(format!("{stem}-{i:02}.tar.part"));
tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
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() {
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<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
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 _d2 = DeleteOnDrop(temp_path.clone());

View File

@@ -415,7 +415,9 @@ impl<'a> MimeFactory<'a> {
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() {
""
} else {
@@ -424,15 +426,13 @@ impl<'a> MimeFactory<'a> {
return Ok(format!("{}{}", re, chat.name));
}
if chat.typ != Chattype::Broadcast {
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
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.
headers
.unprotected
@@ -2341,7 +2350,7 @@ mod tests {
// Now Bob can send an encrypted message to Alice.
let mut msg = Message::new(Viewtype::File);
// 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_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
.await?;

View File

@@ -126,23 +126,6 @@ pub(crate) enum AvatarAction {
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.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
@@ -1348,26 +1331,28 @@ impl MimeMessage {
self.parts.push(part);
}
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType {
if self.get_header(HeaderDef::ListId).is_some() {
return MailinglistType::ListIdBased;
} else if self.get_header(HeaderDef::Sender).is_some() {
pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
if let Some(list_id) = self.get_header(HeaderDef::ListId) {
// The message belongs to a mailing list and has a `ListId:`-header
// 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
// as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
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 {
match self.get_mailinglist_type() {
MailinglistType::ListIdBased | MailinglistType::SenderBased => true,
MailinglistType::None => false,
}
self.get_mailinglist_header().is_some()
}
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.
#[derive(Debug)]
pub enum PeerstateKeyType {
/// Pubilc key sent in the `Autocrypt-Gossip` header.
/// Public key sent in the `Autocrypt-Gossip` header.
GossipKey,
/// Public key sent in the `Autocrypt` header.

View File

@@ -28,9 +28,7 @@ use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
};
use crate::mimeparser::{
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
};
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
@@ -665,45 +663,23 @@ async fn add_parts(
if chat_id.is_none() {
// check if the message belongs to a mailing list
match mime_parser.get_mailinglist_type() {
MailinglistType::ListIdBased => {
if let Some(list_id) = mime_parser.get_header(HeaderDef::ListId) {
if let Some((new_chat_id, new_chat_id_blocked)) =
create_or_lookup_mailinglist(
context,
allow_creation,
list_id,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
context,
allow_creation,
mailinglist_header,
mime_parser,
)
.await?
{
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 {
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),
@@ -1972,6 +1948,8 @@ async fn apply_group_changes(
Ok(better_msg)
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
/// Create or lookup a mailing list chat.
///
/// `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,
mime_parser: &MimeMessage,
) -> Result<Option<(ChatId, Blocked)>> {
static LIST_ID: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
let (mut name, listid) = match LIST_ID.captures(list_id_header) {
Some(cap) => (cap[1].trim().to_string(), cap[2].trim().to_string()),
None => (
"".to_string(),
list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
),
let listid = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[2].trim().to_string(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
};
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
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.
// a usable name for these lists is in the `From` header
// 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
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
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();
} else {
name = listid.clone();
name = listid.to_string();
}
}
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)
}
strip_rtlo_characters(&name)
}
/// 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(
context: &Context,
mime_parser: &MimeMessage,
sent_timestamp: i64,
chat_id: ChatId,
) -> Result<()> {
let Some(list_post) = &mime_parser.list_post else {
let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
return Ok(());
};
@@ -2112,6 +2103,24 @@ async fn apply_mailinglist_changes(
}
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) {
Ok(list_post) => list_post,
Err(err) => {

View File

@@ -686,6 +686,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
PRAGMA secure_delete=on;
PRAGMA busy_timeout = 0; -- fail immediately
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;
",
)?;

View File

@@ -1546,7 +1546,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
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();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
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.
/// - this function is called for all outgoing messages.
/// - 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 {
let hostname = from_addr
.find('@')