diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65a83793f..dfef71fce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/deltachat-rpc-server.yml b/.github/workflows/deltachat-rpc-server.yml index 59530701c..356534fe3 100644 --- a/.github/workflows/deltachat-rpc-server.yml +++ b/.github/workflows/deltachat-rpc-server.yml @@ -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/ diff --git a/.gitignore b/.gitignore index 1f574be4a..aed04dd95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target **/*.rs.bk /build +/dist # ignore vi temporaries *~ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b88db8a2..479472882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 . +- 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 diff --git a/Cargo.lock b/Cargo.lock index 5e910fb96..144871585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 341ae158a..1c225a5b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index e53f1d96f..253c59bf3 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.125.0" +version = "1.126.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index fb6051117..a2cbb72dc 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -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-.tar`, if more than one backup is create on a day, - * the format is `delta-chat--.tar` + * The name of the backup is `delta-chat-backup---.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(). diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 49744eb46..59c53b404 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -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" diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index e58adef60..b67c8e595 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -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 { 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 { + 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) diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 4f3dd7b4d..c2c0b4ad5 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.125.0" + "version": "1.126.0" } diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index 889b1e22e..c85376b09 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -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 = ( diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index c916b6a2c..e13a104c0 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.125.0" +version = "1.126.0" license = "MPL-2.0" edition = "2021" diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index 9486fcab4..31b628ffa 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -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 diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index a609ecc9c..55b3fb7b6 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -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" diff --git a/draft/aeap-mvp.md b/draft/aeap-mvp.md index 94ac3c03e..040125f57 100644 --- a/draft/aeap-mvp.md +++ b/draft/aeap-mvp.md @@ -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) diff --git a/node/lib/context.ts b/node/lib/context.ts index a219881a1..b292e9e35 100644 --- a/node/lib/context.ts +++ b/node/lib/context.ts @@ -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') diff --git a/package.json b/package.json index 05486b8fe..eb738e36f 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/release-date.in b/release-date.in index ac787018e..1a26377ca 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2023-10-14 \ No newline at end of file +2023-10-22 \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index 18dc3bd0c..551d7e77a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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 diff --git a/scripts/make-python-testenv.sh b/scripts/make-python-testenv.sh index 64f959a68..ce3d0b71e 100755 --- a/scripts/make-python-testenv.sh +++ b/scripts/make-python-testenv.sh @@ -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 diff --git a/scripts/wheel-rpc-server.py b/scripts/wheel-rpc-server.py new file mode 100755 index 000000000..9dc481847 --- /dev/null +++ b/scripts/wheel-rpc-server.py @@ -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() diff --git a/scripts/zig-cc b/scripts/zig-cc index 4b1293dde..ffa3278b4 100755 --- a/scripts/zig-cc +++ b/scripts/zig-cc @@ -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 . + 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 + 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, ) diff --git a/scripts/zig-musl-check.sh b/scripts/zig-musl-check.sh deleted file mode 100755 index 89c9b5bf6..000000000 --- a/scripts/zig-musl-check.sh +++ /dev/null @@ -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 diff --git a/scripts/zig-rpc-server.sh b/scripts/zig-rpc-server.sh index 1f35b3fd1..455c0f150 100755 --- a/scripts/zig-rpc-server.sh +++ b/scripts/zig-rpc-server.sh @@ -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 diff --git a/spec.md b/spec.md index b12a75464..ea34862aa 100644 --- a/spec.md +++ b/spec.md @@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from `. 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). diff --git a/src/chat.rs b/src/chat.rs index fe5cde406..b6d593822 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -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(()) } diff --git a/src/chatlist.rs b/src/chatlist.rs index d809b42d6..883ca780c 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -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 = + 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(); diff --git a/src/context.rs b/src/context.rs index f4d6e9fb6..cc1e88b9f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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(); diff --git a/src/imap.rs b/src/imap.rs index 8f009f8ba..6c41244aa 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -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 } }; diff --git a/src/imex.rs b/src/imex.rs index 0b61de71c..236f5e6fc 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -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-.tar`, if more than one backup is create on a day, - /// the format is `delta-chat--.tar` + /// The name of the backup is `delta-chat-backup---.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 { && (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()); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 2ac272afc..f54b2fa38 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -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?; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index b07e067df..ae715e0a1 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -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) { diff --git a/src/peerstate.rs b/src/peerstate.rs index cbe4227f5..6a2a83380 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -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. diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 59442a118..1ca7323c9 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -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 = 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> { - static LIST_ID: Lazy = 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 = 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) => { diff --git a/src/sql.rs b/src/sql.rs index ceb38067e..521280f62 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -686,6 +686,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result { 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; ", )?; diff --git a/src/stock_str.rs b/src/stock_str.rs index e4d43701b..888c474c4 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -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); diff --git a/src/tools.rs b/src/tools.rs index 43dd51f01..338570f7a 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -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('@')