mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 17:36:29 +03:00
Merge tag 'v1.126.0'
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -39,10 +39,6 @@ jobs:
|
|||||||
- name: Check
|
- name: Check
|
||||||
run: cargo check --workspace --all-targets --all-features
|
run: cargo check --workspace --all-targets --all-features
|
||||||
|
|
||||||
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
|
|
||||||
- name: Check musl
|
|
||||||
run: scripts/zig-musl-check.sh
|
|
||||||
|
|
||||||
cargo_deny:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
69
.github/workflows/deltachat-rpc-server.yml
vendored
69
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -26,35 +26,26 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build
|
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||||
|
- name: Install python 3.12
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
|
- name: Install ziglang and wheel
|
||||||
|
run: pip install wheel ziglang==0.11.0
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: sh scripts/zig-rpc-server.sh
|
run: sh scripts/zig-rpc-server.sh
|
||||||
|
|
||||||
- name: Upload x86_64 binary
|
- name: Build deltachat-rpc-server Python wheels and source package
|
||||||
uses: actions/upload-artifact@v3
|
run: scripts/wheel-rpc-server.py
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64
|
|
||||||
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload i686 binary
|
- name: Upload dist directory
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686
|
name: dist
|
||||||
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
|
path: dist/
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload aarch64 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64
|
|
||||||
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload armv7 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7
|
|
||||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
@@ -116,15 +107,29 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- name: Download built binaries
|
- name: Download Linux binaries
|
||||||
uses: "actions/download-artifact@v3"
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/
|
||||||
|
|
||||||
- name: Compose dist/ directory
|
- name: Download win32 binary
|
||||||
run: |
|
uses: actions/download-artifact@v3
|
||||||
mkdir dist
|
with:
|
||||||
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
|
name: deltachat-rpc-server-win32.exe
|
||||||
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
|
path: dist/deltachat-rpc-server-win32.exe
|
||||||
done
|
|
||||||
|
- name: Download win64 binary
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64.exe
|
||||||
|
path: dist/deltachat-rpc-server-win32.exe
|
||||||
|
|
||||||
|
- name: Download macOS binary
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
|
path: dist/deltachat-rpc-server-x86_64-macos
|
||||||
|
|
||||||
- name: List downloaded artifacts
|
- name: List downloaded artifacts
|
||||||
run: ls -l dist/
|
run: ls -l dist/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
|
|||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.126.0] - 2023-10-22
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
|
||||||
|
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
|
||||||
|
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
|
||||||
|
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Build Python wheels for deltachat-rpc-server.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Strip release binaries.
|
||||||
|
- Workaround OpenSSL crate expecting libatomic to be available.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Set `soft_heap_limit` on SQLite database.
|
||||||
|
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
|
||||||
|
|
||||||
## [1.125.0] - 2023-10-14
|
## [1.125.0] - 2023-10-14
|
||||||
|
|
||||||
### API-Changes
|
### API-Changes
|
||||||
@@ -2915,3 +2942,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
|||||||
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
||||||
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
|
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
|
||||||
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
|
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
|
||||||
|
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
|
||||||
|
|||||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -210,9 +210,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-imap"
|
name = "async-imap"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
|
checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"base64 0.21.3",
|
"base64 0.21.3",
|
||||||
@@ -1085,7 +1085,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1162,7 +1162,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
@@ -1186,7 +1186,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1201,7 +1201,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"deltachat",
|
"deltachat",
|
||||||
@@ -1226,7 +1226,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"deltachat",
|
"deltachat",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.67"
|
rust-version = "1.67"
|
||||||
@@ -24,6 +24,7 @@ lto = true
|
|||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -891,7 +891,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
|||||||
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||||
* is added as needed.
|
* is added as needed.
|
||||||
* @param query_str An optional query for filtering the list. Only chats matching this query
|
* @param query_str An optional query for filtering the list. Only chats matching this query
|
||||||
* are returned. Give NULL for no filtering.
|
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
|
||||||
|
* the chatlist is filtered such that only chats with unread messages show up.
|
||||||
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
||||||
* are returned. Give 0 for no filtering.
|
* are returned. Give 0 for no filtering.
|
||||||
* @return A chatlist as an dc_chatlist_t object.
|
* @return A chatlist as an dc_chatlist_t object.
|
||||||
@@ -1706,24 +1707,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
|
|||||||
* Create a new broadcast list.
|
* Create a new broadcast list.
|
||||||
*
|
*
|
||||||
* Broadcast lists are similar to groups on the sending device,
|
* Broadcast lists are similar to groups on the sending device,
|
||||||
* however, recipients get the messages in normal one-to-one chats
|
* however, recipients get the messages in a read-only chat
|
||||||
* and will not be aware of other members.
|
* and will see who the other members are.
|
||||||
*
|
*
|
||||||
* Replies to broadcasts go only to the sender
|
* For historical reasons, this function does not take a name directly,
|
||||||
* and not to all broadcast recipients.
|
* instead you have to set the name using dc_set_chat_name()
|
||||||
* Moreover, replies will not appear in the broadcast list
|
* after creating the broadcast list.
|
||||||
* but in the one-to-one chat with the person answering.
|
|
||||||
*
|
|
||||||
* The name and the image of the broadcast list is set automatically
|
|
||||||
* and is visible to the sender only.
|
|
||||||
* Not asking for these data allows more focused creation
|
|
||||||
* and we bypass the question who will get which data.
|
|
||||||
* Also, many users will have at most one broadcast list
|
|
||||||
* so, a generic name and image is sufficient at the first place.
|
|
||||||
*
|
|
||||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
* All in all, this is also what other messengers are doing here.
|
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
@@ -2266,8 +2255,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
|||||||
* the backup is not encrypted.
|
* the backup is not encrypted.
|
||||||
* The backup contains all contacts, chats, images and other data and device independent settings.
|
* The backup contains all contacts, chats, images and other data and device independent settings.
|
||||||
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||||
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
|
||||||
* the format is `delta-chat-<day>-<number>.tar`
|
|
||||||
*
|
*
|
||||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
||||||
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||||
@@ -3973,7 +3961,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
|
|||||||
* Get the message time used for sorting.
|
* Get the message time used for sorting.
|
||||||
* This function returns the timestamp that is used for sorting the message
|
* This function returns the timestamp that is used for sorting the message
|
||||||
* into lists as returned e.g. by dc_get_chat_msgs().
|
* into lists as returned e.g. by dc_get_chat_msgs().
|
||||||
* This may be the reveived time, the sending time or another time.
|
* This may be the received time, the sending time or another time.
|
||||||
*
|
*
|
||||||
* To get the receiving time, use dc_msg_get_received_timestamp().
|
* To get the receiving time, use dc_msg_get_received_timestamp().
|
||||||
* To get the sending time, use dc_msg_get_timestamp().
|
* To get the sending time, use dc_msg_get_timestamp().
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "deltachat-jsonrpc-server"
|
default-run = "deltachat-jsonrpc-server"
|
||||||
|
|||||||
@@ -815,24 +815,12 @@ impl CommandApi {
|
|||||||
/// Create a new broadcast list.
|
/// Create a new broadcast list.
|
||||||
///
|
///
|
||||||
/// Broadcast lists are similar to groups on the sending device,
|
/// Broadcast lists are similar to groups on the sending device,
|
||||||
/// however, recipients get the messages in normal one-to-one chats
|
/// however, recipients get the messages in a read-only chat
|
||||||
/// and will not be aware of other members.
|
/// and will see who the other members are.
|
||||||
///
|
///
|
||||||
/// Replies to broadcasts go only to the sender
|
/// For historical reasons, this function does not take a name directly,
|
||||||
/// and not to all broadcast recipients.
|
/// instead you have to set the name using dc_set_chat_name()
|
||||||
/// Moreover, replies will not appear in the broadcast list
|
/// after creating the broadcast list.
|
||||||
/// but in the one-to-one chat with the person answering.
|
|
||||||
///
|
|
||||||
/// The name and the image of the broadcast list is set automatically
|
|
||||||
/// and is visible to the sender only.
|
|
||||||
/// Not asking for these data allows more focused creation
|
|
||||||
/// and we bypass the question who will get which data.
|
|
||||||
/// Also, many users will have at most one broadcast list
|
|
||||||
/// so, a generic name and image is sufficient at the first place.
|
|
||||||
///
|
|
||||||
/// Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
/// All in all, this is also what other messengers are doing here.
|
|
||||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
chat::create_broadcast_list(&ctx)
|
chat::create_broadcast_list(&ctx)
|
||||||
@@ -2071,6 +2059,23 @@ impl CommandApi {
|
|||||||
|
|
||||||
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send the chat's current set draft
|
||||||
|
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||||
|
let mut draft = draft;
|
||||||
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
|
||||||
|
.await?
|
||||||
|
.to_u32();
|
||||||
|
Ok(msg_id)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"chat with id {} doesn't have draft message",
|
||||||
|
chat_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions (to prevent code duplication)
|
// Helper functions (to prevent code duplication)
|
||||||
|
|||||||
@@ -55,5 +55,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "1.125.0"
|
"version": "1.126.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe("online tests", function () {
|
|||||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||||
]);
|
]);
|
||||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||||
// Check if answer arives at A and if it is encrypted
|
// Check if answer arrives at A and if it is encrypted
|
||||||
await eventPromise2;
|
await eventPromise2;
|
||||||
|
|
||||||
const messageId = (
|
const messageId = (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
|
||||||
Dict,
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -92,7 +91,7 @@ class Client:
|
|||||||
"""Process events forever."""
|
"""Process events forever."""
|
||||||
self.run_until(lambda _: False)
|
self.run_until(lambda _: False)
|
||||||
|
|
||||||
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||||
"""Process events until the given callable evaluates to True.
|
"""Process events until the given callable evaluates to True.
|
||||||
|
|
||||||
The callable should accept an AttrDict object representing the
|
The callable should accept an AttrDict object representing the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "1.125.0"
|
version = "1.126.0"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
|
|||||||
#### Upsides:
|
#### Upsides:
|
||||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
|
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
||||||
|
|
||||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opens a stanalone context (without an account manager)
|
/** Opens a standalone context (without an account manager)
|
||||||
* automatically starts the event handler */
|
* automatically starts the event handler */
|
||||||
static open(cwd: string): Context {
|
static open(cwd: string): Context {
|
||||||
const dbFile = join(cwd, 'db.sqlite')
|
const dbFile = join(cwd, 'db.sqlite')
|
||||||
|
|||||||
@@ -60,5 +60,5 @@
|
|||||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||||
},
|
},
|
||||||
"types": "node/dist/index.d.ts",
|
"types": "node/dist/index.d.ts",
|
||||||
"version": "1.125.0"
|
"version": "1.126.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2023-10-14
|
2023-10-22
|
||||||
@@ -10,6 +10,8 @@ and an own build machine.
|
|||||||
|
|
||||||
- `deny.sh` runs `cargo deny` for all Rust code in the project.
|
- `deny.sh` runs `cargo deny` for all Rust code in the project.
|
||||||
|
|
||||||
|
- `codespell.sh` spellchecks the source code using `codespell` tool.
|
||||||
|
|
||||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
- `../.github/workflows` contains jobs run by GitHub Actions.
|
||||||
|
|
||||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ export DCC_RS_DEV="$PWD"
|
|||||||
cargo build -p deltachat_ffi --features jsonrpc
|
cargo build -p deltachat_ffi --features jsonrpc
|
||||||
|
|
||||||
tox -c python -e py --devenv venv
|
tox -c python -e py --devenv venv
|
||||||
env/bin/pip install --upgrade pip
|
venv/bin/pip install --upgrade pip
|
||||||
|
|||||||
162
scripts/wheel-rpc-server.py
Executable file
162
scripts/wheel-rpc-server.py
Executable 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()
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# /// pyproject
|
||||||
|
# [run]
|
||||||
|
# dependencies = [
|
||||||
|
# "ziglang==0.11.0"
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def flag_filter(flag: str) -> bool:
|
def flag_filter(flag: str) -> bool:
|
||||||
|
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
|
||||||
|
if flag == "-latomic":
|
||||||
|
return False
|
||||||
|
|
||||||
if flag == "-lc":
|
if flag == "-lc":
|
||||||
return False
|
return False
|
||||||
if flag == "-Wl,-melf_i386":
|
if flag == "-Wl,-melf_i386":
|
||||||
@@ -24,8 +34,23 @@ def main():
|
|||||||
else:
|
else:
|
||||||
zig_cpu_args = []
|
zig_cpu_args = []
|
||||||
|
|
||||||
|
# Disable atomics and use locks instead in OpenSSL.
|
||||||
|
# Zig toolchains do not provide atomics.
|
||||||
|
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
|
||||||
|
args += ["-DBROKEN_CLANG_ATOMICS"]
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"ziglang",
|
||||||
|
"cc",
|
||||||
|
"-target",
|
||||||
|
zig_target,
|
||||||
|
*zig_cpu_args,
|
||||||
|
*args,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -10,14 +10,6 @@ unset RUSTFLAGS
|
|||||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||||
export RUSTUP_TOOLCHAIN=1.72.0
|
export RUSTUP_TOOLCHAIN=1.72.0
|
||||||
|
|
||||||
ZIG_VERSION=0.11.0
|
|
||||||
|
|
||||||
# Download Zig
|
|
||||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
|
||||||
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
|
||||||
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
|
||||||
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
|
|
||||||
|
|
||||||
rustup target add i686-unknown-linux-musl
|
rustup target add i686-unknown-linux-musl
|
||||||
CC="$PWD/scripts/zig-cc" \
|
CC="$PWD/scripts/zig-cc" \
|
||||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||||
@@ -50,3 +42,9 @@ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
|||||||
LD="$PWD/scripts/zig-cc" \
|
LD="$PWD/scripts/zig-cc" \
|
||||||
ZIG_TARGET="aarch64-linux-musl" \
|
ZIG_TARGET="aarch64-linux-musl" \
|
||||||
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
|
||||||
|
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
|
||||||
|
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
|
||||||
|
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux
|
||||||
|
|||||||
2
spec.md
2
spec.md
@@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from <sender name>`.
|
|||||||
Replies to messages MAY follow the typical `Re:`-format.
|
Replies to messages MAY follow the typical `Re:`-format.
|
||||||
|
|
||||||
The body MAY contain text which MUST have the content type `text/plain`
|
The body MAY contain text which MUST have the content type `text/plain`
|
||||||
or `mulipart/alternative` containing `text/plain`.
|
or `multipart/alternative` containing `text/plain`.
|
||||||
|
|
||||||
The text MAY be divided into a user-text-part and a footer-part using the
|
The text MAY be divided into a user-text-part and a footer-part using the
|
||||||
line `-- ` (minus, minus, space, lineend).
|
line `-- ` (minus, minus, space, lineend).
|
||||||
|
|||||||
24
src/chat.rs
24
src/chat.rs
@@ -6126,22 +6126,40 @@ mod tests {
|
|||||||
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
|
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
|
||||||
|
{
|
||||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
assert_eq!(chat.typ, Chattype::Broadcast);
|
||||||
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
|
assert_eq!(chat.name, "Broadcast list");
|
||||||
assert!(!chat.is_self_talk());
|
assert!(!chat.is_self_talk());
|
||||||
|
|
||||||
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
||||||
let msg = alice.get_last_msg().await;
|
let msg = alice.get_last_msg().await;
|
||||||
assert_eq!(msg.chat_id, chat.id);
|
assert_eq!(msg.chat_id, chat.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||||
assert_eq!(msg.get_text(), "ola!");
|
assert_eq!(msg.get_text(), "ola!");
|
||||||
|
assert_eq!(msg.subject, "Broadcast list");
|
||||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||||
assert_eq!(chat.typ, Chattype::Single);
|
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||||
assert_eq!(chat.id, chat_bob.id);
|
assert_ne!(chat.id, chat_bob.id);
|
||||||
|
assert_eq!(chat.name, "Broadcast list");
|
||||||
assert!(!chat.is_self_talk());
|
assert!(!chat.is_self_talk());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Alice changes the name:
|
||||||
|
set_chat_name(&alice, broadcast_id, "My great broadcast").await?;
|
||||||
|
let sent = alice.send_text(broadcast_id, "I changed the title!").await;
|
||||||
|
|
||||||
|
let msg = bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(msg.subject, "Re: My great broadcast");
|
||||||
|
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||||
|
assert_eq!(bob_chat.name, "My great broadcast");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! # Chat list module.
|
//! # Chat list module.
|
||||||
|
|
||||||
use anyhow::{ensure, Context as _, Result};
|
use anyhow::{ensure, Context as _, Result};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
@@ -15,6 +16,10 @@ use crate::stock_str;
|
|||||||
use crate::summary::Summary;
|
use crate::summary::Summary;
|
||||||
use crate::tools::IsNoneOrEmpty;
|
use crate::tools::IsNoneOrEmpty;
|
||||||
|
|
||||||
|
/// Regex to find out if a query should filter by unread messages.
|
||||||
|
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
|
||||||
|
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
|
||||||
|
|
||||||
/// An object representing a single chatlist in memory.
|
/// An object representing a single chatlist in memory.
|
||||||
///
|
///
|
||||||
/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
|
/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
|
||||||
@@ -78,7 +83,8 @@ impl Chatlist {
|
|||||||
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||||
/// is added as needed.
|
/// is added as needed.
|
||||||
/// `query`: An optional query for filtering the list. Only chats matching this query
|
/// `query`: An optional query for filtering the list. Only chats matching this query
|
||||||
/// are returned.
|
/// are returned. When `is:unread` is contained in the query, the chatlist is
|
||||||
|
/// filtered such that only chats with unread messages show up.
|
||||||
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
|
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
|
||||||
/// are returned.
|
/// are returned.
|
||||||
pub async fn try_load(
|
pub async fn try_load(
|
||||||
@@ -172,8 +178,10 @@ impl Chatlist {
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if let Some(query) = query {
|
} else if let Some(query) = query {
|
||||||
let query = query.trim().to_string();
|
let mut query = query.trim().to_string();
|
||||||
ensure!(!query.is_empty(), "missing query");
|
ensure!(!query.is_empty(), "query mustn't be empty");
|
||||||
|
let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
|
||||||
|
query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
|
||||||
|
|
||||||
// allow searching over special names that may change at any time
|
// allow searching over special names that may change at any time
|
||||||
// when the ui calls set_stock_translation()
|
// when the ui calls set_stock_translation()
|
||||||
@@ -198,9 +206,10 @@ impl Chatlist {
|
|||||||
WHERE c.id>9 AND c.id!=?2
|
WHERE c.id>9 AND c.id!=?2
|
||||||
AND c.blocked!=1
|
AND c.blocked!=1
|
||||||
AND c.name LIKE ?3
|
AND c.name LIKE ?3
|
||||||
|
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
(MessageState::OutDraft, skip_id, str_like_cmd),
|
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
)
|
)
|
||||||
@@ -462,7 +471,8 @@ pub async fn get_last_message_for_chat(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chat::{
|
use crate::chat::{
|
||||||
create_group_chat, get_chat_contacts, remove_contact_from_chat, ProtectionStatus,
|
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||||
|
send_text_msg, ProtectionStatus,
|
||||||
};
|
};
|
||||||
use crate::message::Viewtype;
|
use crate::message::Viewtype;
|
||||||
use crate::receive_imf::receive_imf;
|
use crate::receive_imf::receive_imf;
|
||||||
@@ -471,7 +481,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_try_load() {
|
async fn test_try_load() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new_bob().await;
|
||||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -510,6 +520,31 @@ mod tests {
|
|||||||
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
|
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
|
||||||
assert_eq!(chats.len(), 1);
|
assert_eq!(chats.len(), 1);
|
||||||
|
|
||||||
|
// receive a message from alice
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
add_contact_to_chat(
|
||||||
|
&alice,
|
||||||
|
alice_chat_id,
|
||||||
|
Contact::create(&alice, "bob", "bob@example.net")
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
send_text_msg(&alice, alice_chat_id, "hi".into())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let sent_msg = alice.pop_sent_msg().await;
|
||||||
|
|
||||||
|
t.recv_msg(&sent_msg).await;
|
||||||
|
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(chats.len() == 1);
|
||||||
|
|
||||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use crate::tools::{duration_to_str, time};
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// Creating a new unecrypted database:
|
/// Creating a new unencrypted database:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|||||||
55
src/imap.rs
55
src/imap.rs
@@ -82,7 +82,6 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
|||||||
MESSAGE-ID \
|
MESSAGE-ID \
|
||||||
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
|
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
|
||||||
)])";
|
)])";
|
||||||
const JUST_UID: &str = "(UID)";
|
|
||||||
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
|
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
|
||||||
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
|
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
|
||||||
|
|
||||||
@@ -627,18 +626,6 @@ impl Imap {
|
|||||||
// UIDVALIDITY is modified, reset highest seen MODSEQ.
|
// UIDVALIDITY is modified, reset highest seen MODSEQ.
|
||||||
set_modseq(context, folder, 0).await?;
|
set_modseq(context, folder, 0).await?;
|
||||||
|
|
||||||
if mailbox.exists == 0 {
|
|
||||||
info!(context, "Folder {folder:?} is empty.");
|
|
||||||
|
|
||||||
// set uid_next=1 for empty folders.
|
|
||||||
// If we do not do this here, we'll miss the first message
|
|
||||||
// as we will get in here again and fetch from uid_next then.
|
|
||||||
// Also, the "fall back to fetching" below would need a non-zero mailbox.exists to work.
|
|
||||||
set_uid_next(context, folder, 1).await?;
|
|
||||||
set_uidvalidity(context, folder, new_uid_validity).await?;
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== uid_validity has changed or is being set the first time. ==============
|
// ============== uid_validity has changed or is being set the first time. ==============
|
||||||
|
|
||||||
let new_uid_next = match mailbox.uid_next {
|
let new_uid_next = match mailbox.uid_next {
|
||||||
@@ -646,26 +633,36 @@ impl Imap {
|
|||||||
None => {
|
None => {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"IMAP folder {folder:?} has no uid_next, fall back to fetching."
|
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
|
||||||
);
|
);
|
||||||
// note that we use fetch by sequence number
|
|
||||||
// and thus we only need to get exactly the
|
|
||||||
// last-index message.
|
|
||||||
let set = format!("{}", mailbox.exists);
|
|
||||||
let mut list = session
|
|
||||||
.inner
|
|
||||||
.fetch(set, JUST_UID)
|
|
||||||
.await
|
|
||||||
.context("Error fetching UID")?;
|
|
||||||
|
|
||||||
let mut new_last_seen_uid = None;
|
// RFC 3501 says STATUS command SHOULD NOT be used
|
||||||
while let Some(fetch) = list.try_next().await? {
|
// on the currently selected mailbox because the same
|
||||||
if fetch.message == mailbox.exists && fetch.uid.is_some() {
|
// information can be obtained by other means,
|
||||||
new_last_seen_uid = fetch.uid;
|
// such as reading SELECT response.
|
||||||
|
//
|
||||||
|
// However, it also says that UIDNEXT is REQUIRED
|
||||||
|
// in the SELECT response and if we are here,
|
||||||
|
// it is actually not returned.
|
||||||
|
//
|
||||||
|
// In particular, Winmail Pro Mail Server 5.1.0616
|
||||||
|
// never returns UIDNEXT in SELECT response,
|
||||||
|
// but responds to "SELECT INBOX (UIDNEXT)" command.
|
||||||
|
let status = session
|
||||||
|
.inner
|
||||||
|
.status(folder, "(UIDNEXT)")
|
||||||
|
.await
|
||||||
|
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||||
|
|
||||||
|
if let Some(uid_next) = status.uid_next {
|
||||||
|
uid_next
|
||||||
|
} else {
|
||||||
|
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
|
||||||
|
|
||||||
|
// Set UIDNEXT to 1 as a last resort fallback.
|
||||||
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_last_seen_uid.context("select: failed to fetch")? + 1
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
set_uid_next(context, folder, new_uid_next).await?;
|
set_uid_next(context, folder, new_uid_next).await?;
|
||||||
|
|||||||
20
src/imex.rs
20
src/imex.rs
@@ -60,8 +60,7 @@ pub enum ImexMode {
|
|||||||
/// Export a backup to the directory given as `path` with the given `passphrase`.
|
/// Export a backup to the directory given as `path` with the given `passphrase`.
|
||||||
/// The backup contains all contacts, chats, images and other data and device independent settings.
|
/// The backup contains all contacts, chats, images and other data and device independent settings.
|
||||||
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
|
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||||
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
/// The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
|
||||||
/// the format is `delta-chat-<day>-<number>.tar`
|
|
||||||
ExportBackup = 11,
|
ExportBackup = 11,
|
||||||
|
|
||||||
/// `path` is the file (not: directory) to import. The file is normally
|
/// `path` is the file (not: directory) to import. The file is normally
|
||||||
@@ -130,7 +129,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
|||||||
&& (newest_backup_name.is_empty() || name > newest_backup_name)
|
&& (newest_backup_name.is_empty() || name > newest_backup_name)
|
||||||
{
|
{
|
||||||
// We just use string comparison to determine which backup is newer.
|
// We just use string comparison to determine which backup is newer.
|
||||||
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
|
// This works fine because the filenames have the form `delta-chat-backup-2023-10-18-00-foo@example.com.tar`
|
||||||
newest_backup_path = Some(path);
|
newest_backup_path = Some(path);
|
||||||
newest_backup_name = name;
|
newest_backup_name = name;
|
||||||
}
|
}
|
||||||
@@ -486,7 +485,11 @@ async fn import_backup(
|
|||||||
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
|
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
|
||||||
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
|
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
|
||||||
/// it can be renamed to dest_path. This guarantees that the backup is complete.
|
/// it can be renamed to dest_path. This guarantees that the backup is complete.
|
||||||
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> {
|
fn get_next_backup_path(
|
||||||
|
folder: &Path,
|
||||||
|
addr: &str,
|
||||||
|
backup_time: i64,
|
||||||
|
) -> Result<(PathBuf, PathBuf, PathBuf)> {
|
||||||
let folder = PathBuf::from(folder);
|
let folder = PathBuf::from(folder);
|
||||||
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
|
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
|
||||||
.context("can't get next backup path")?
|
.context("can't get next backup path")?
|
||||||
@@ -497,13 +500,13 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
|
|||||||
// 64 backup files per day should be enough for everyone
|
// 64 backup files per day should be enough for everyone
|
||||||
for i in 0..64 {
|
for i in 0..64 {
|
||||||
let mut tempdbfile = folder.clone();
|
let mut tempdbfile = folder.clone();
|
||||||
tempdbfile.push(format!("{stem}-{i:02}.db"));
|
tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
|
||||||
|
|
||||||
let mut tempfile = folder.clone();
|
let mut tempfile = folder.clone();
|
||||||
tempfile.push(format!("{stem}-{i:02}.tar.part"));
|
tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
|
||||||
|
|
||||||
let mut destfile = folder.clone();
|
let mut destfile = folder.clone();
|
||||||
destfile.push(format!("{stem}-{i:02}.tar"));
|
destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
|
||||||
|
|
||||||
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
|
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
|
||||||
return Ok((tempdbfile, tempfile, destfile));
|
return Ok((tempdbfile, tempfile, destfile));
|
||||||
@@ -518,7 +521,8 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
|
|||||||
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
|
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
|
||||||
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
||||||
let now = time();
|
let now = time();
|
||||||
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now)?;
|
let self_addr = context.get_primary_self_addr().await?;
|
||||||
|
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
|
||||||
let _d1 = DeleteOnDrop(temp_db_path.clone());
|
let _d1 = DeleteOnDrop(temp_db_path.clone());
|
||||||
let _d2 = DeleteOnDrop(temp_path.clone());
|
let _d2 = DeleteOnDrop(temp_path.clone());
|
||||||
|
|
||||||
|
|||||||
@@ -415,7 +415,9 @@ impl<'a> MimeFactory<'a> {
|
|||||||
return Ok(self.msg.subject.clone());
|
return Ok(self.msg.subject.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if chat.typ == Chattype::Group && quoted_msg_subject.is_none_or_empty() {
|
if (chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast)
|
||||||
|
&& quoted_msg_subject.is_none_or_empty()
|
||||||
|
{
|
||||||
let re = if self.in_reply_to.is_empty() {
|
let re = if self.in_reply_to.is_empty() {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
@@ -424,7 +426,6 @@ impl<'a> MimeFactory<'a> {
|
|||||||
return Ok(format!("{}{}", re, chat.name));
|
return Ok(format!("{}{}", re, chat.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if chat.typ != Chattype::Broadcast {
|
|
||||||
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
|
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
|
||||||
chat.param.get(Param::LastSubject)
|
chat.param.get(Param::LastSubject)
|
||||||
} else {
|
} else {
|
||||||
@@ -433,7 +434,6 @@ impl<'a> MimeFactory<'a> {
|
|||||||
if let Some(last_subject) = parent_subject {
|
if let Some(last_subject) = parent_subject {
|
||||||
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
|
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let self_name = &match context.get_config(Config::Displayname).await? {
|
let self_name = &match context.get_config(Config::Displayname).await? {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
@@ -594,6 +594,15 @@ impl<'a> MimeFactory<'a> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Loaded::Message { chat } = &self.loaded {
|
||||||
|
if chat.typ == Chattype::Broadcast {
|
||||||
|
headers.protected.push(Header::new(
|
||||||
|
"List-ID".into(),
|
||||||
|
format!("{} <{}>", chat.name, chat.grpid),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Non-standard headers.
|
// Non-standard headers.
|
||||||
headers
|
headers
|
||||||
.unprotected
|
.unprotected
|
||||||
@@ -2341,7 +2350,7 @@ mod tests {
|
|||||||
// Now Bob can send an encrypted message to Alice.
|
// Now Bob can send an encrypted message to Alice.
|
||||||
let mut msg = Message::new(Viewtype::File);
|
let mut msg = Message::new(Viewtype::File);
|
||||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||||
// decoded_data to check presense of the necessary headers.
|
// decoded_data to check presence of the necessary headers.
|
||||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
|
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -126,23 +126,6 @@ pub(crate) enum AvatarAction {
|
|||||||
Change(String),
|
Change(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub(crate) enum MailinglistType {
|
|
||||||
/// The message belongs to a mailing list and has a `ListId:`-header
|
|
||||||
/// that should be used to get a unique id.
|
|
||||||
ListIdBased,
|
|
||||||
|
|
||||||
/// The message belongs to a mailing list, but there is no `ListId:`-header;
|
|
||||||
/// `Sender:`-header should be used to get a unique id.
|
|
||||||
/// This method is used by implementations as Majordomo.
|
|
||||||
/// Note, that the `Sender:` header alone is not sufficient to detect these lists,
|
|
||||||
/// `get_mailinglist_type()` check additional conditions therefore.
|
|
||||||
SenderBased,
|
|
||||||
|
|
||||||
/// The message does not belong to a mailing list.
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// System message type.
|
/// System message type.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||||
@@ -1348,26 +1331,28 @@ impl MimeMessage {
|
|||||||
self.parts.push(part);
|
self.parts.push(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType {
|
pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
|
||||||
if self.get_header(HeaderDef::ListId).is_some() {
|
if let Some(list_id) = self.get_header(HeaderDef::ListId) {
|
||||||
return MailinglistType::ListIdBased;
|
// The message belongs to a mailing list and has a `ListId:`-header
|
||||||
} else if self.get_header(HeaderDef::Sender).is_some() {
|
// that should be used to get a unique id.
|
||||||
|
return Some(list_id);
|
||||||
|
} else if let Some(sender) = self.get_header(HeaderDef::Sender) {
|
||||||
// the `Sender:`-header alone is no indicator for mailing list
|
// the `Sender:`-header alone is no indicator for mailing list
|
||||||
// as also used for bot-impersonation via `set_override_sender_name()`
|
// as also used for bot-impersonation via `set_override_sender_name()`
|
||||||
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
|
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
|
||||||
if precedence == "list" || precedence == "bulk" {
|
if precedence == "list" || precedence == "bulk" {
|
||||||
return MailinglistType::SenderBased;
|
// The message belongs to a mailing list, but there is no `ListId:`-header;
|
||||||
|
// `Sender:`-header is be used to get a unique id.
|
||||||
|
// This method is used by implementations as Majordomo.
|
||||||
|
return Some(sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MailinglistType::None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_mailinglist_message(&self) -> bool {
|
pub(crate) fn is_mailinglist_message(&self) -> bool {
|
||||||
match self.get_mailinglist_type() {
|
self.get_mailinglist_header().is_some()
|
||||||
MailinglistType::ListIdBased | MailinglistType::SenderBased => true,
|
|
||||||
MailinglistType::None => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repl_msg_by_error(&mut self, error_msg: &str) {
|
pub fn repl_msg_by_error(&mut self, error_msg: &str) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::stock_str;
|
|||||||
/// Type of the public key stored inside the peerstate.
|
/// Type of the public key stored inside the peerstate.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PeerstateKeyType {
|
pub enum PeerstateKeyType {
|
||||||
/// Pubilc key sent in the `Autocrypt-Gossip` header.
|
/// Public key sent in the `Autocrypt-Gossip` header.
|
||||||
GossipKey,
|
GossipKey,
|
||||||
|
|
||||||
/// Public key sent in the `Autocrypt` header.
|
/// Public key sent in the `Autocrypt` header.
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ use crate::log::LogExt;
|
|||||||
use crate::message::{
|
use crate::message::{
|
||||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||||
};
|
};
|
||||||
use crate::mimeparser::{
|
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||||
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
|
|
||||||
};
|
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||||
use crate::reaction::{set_msg_reaction, Reaction};
|
use crate::reaction::{set_msg_reaction, Reaction};
|
||||||
@@ -665,14 +663,11 @@ async fn add_parts(
|
|||||||
|
|
||||||
if chat_id.is_none() {
|
if chat_id.is_none() {
|
||||||
// check if the message belongs to a mailing list
|
// check if the message belongs to a mailing list
|
||||||
match mime_parser.get_mailinglist_type() {
|
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||||
MailinglistType::ListIdBased => {
|
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
|
||||||
if let Some(list_id) = mime_parser.get_header(HeaderDef::ListId) {
|
|
||||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
|
||||||
create_or_lookup_mailinglist(
|
|
||||||
context,
|
context,
|
||||||
allow_creation,
|
allow_creation,
|
||||||
list_id,
|
mailinglist_header,
|
||||||
mime_parser,
|
mime_parser,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -682,28 +677,9 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MailinglistType::SenderBased => {
|
|
||||||
if let Some(sender) = mime_parser.get_header(HeaderDef::Sender) {
|
|
||||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
|
||||||
create_or_lookup_mailinglist(
|
|
||||||
context,
|
|
||||||
allow_creation,
|
|
||||||
sender,
|
|
||||||
mime_parser,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
chat_id = Some(new_chat_id);
|
|
||||||
chat_id_blocked = new_chat_id_blocked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MailinglistType::None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(chat_id) = chat_id {
|
if let Some(chat_id) = chat_id {
|
||||||
apply_mailinglist_changes(context, mime_parser, chat_id).await?;
|
apply_mailinglist_changes(context, mime_parser, sent_timestamp, chat_id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if contact renaming is prevented (for mailinglists and bots),
|
// if contact renaming is prevented (for mailinglists and bots),
|
||||||
@@ -1972,6 +1948,8 @@ async fn apply_group_changes(
|
|||||||
Ok(better_msg)
|
Ok(better_msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||||
|
|
||||||
/// Create or lookup a mailing list chat.
|
/// Create or lookup a mailing list chat.
|
||||||
///
|
///
|
||||||
/// `list_id_header` contains the Id that must be used for the mailing list
|
/// `list_id_header` contains the Id that must be used for the mailing list
|
||||||
@@ -1988,23 +1966,71 @@ async fn create_or_lookup_mailinglist(
|
|||||||
list_id_header: &str,
|
list_id_header: &str,
|
||||||
mime_parser: &MimeMessage,
|
mime_parser: &MimeMessage,
|
||||||
) -> Result<Option<(ChatId, Blocked)>> {
|
) -> Result<Option<(ChatId, Blocked)>> {
|
||||||
static LIST_ID: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
let listid = match LIST_ID_REGEX.captures(list_id_header) {
|
||||||
let (mut name, listid) = match LIST_ID.captures(list_id_header) {
|
Some(cap) => cap[2].trim().to_string(),
|
||||||
Some(cap) => (cap[1].trim().to_string(), cap[2].trim().to_string()),
|
None => list_id_header
|
||||||
None => (
|
|
||||||
"".to_string(),
|
|
||||||
list_id_header
|
|
||||||
.trim()
|
.trim()
|
||||||
.trim_start_matches('<')
|
.trim_start_matches('<')
|
||||||
.trim_end_matches('>')
|
.trim_end_matches('>')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
|
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
|
||||||
return Ok(Some((chat_id, blocked)));
|
return Ok(Some((chat_id, blocked)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
|
||||||
|
|
||||||
|
if allow_creation {
|
||||||
|
// list does not exist but should be created
|
||||||
|
let param = mime_parser.list_post.as_ref().map(|list_post| {
|
||||||
|
let mut p = Params::new();
|
||||||
|
p.set(Param::ListPost, list_post);
|
||||||
|
p.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||||
|
let blocked = if is_bot {
|
||||||
|
Blocked::Not
|
||||||
|
} else {
|
||||||
|
Blocked::Request
|
||||||
|
};
|
||||||
|
let chat_id = ChatId::create_multiuser_record(
|
||||||
|
context,
|
||||||
|
Chattype::Mailinglist,
|
||||||
|
&listid,
|
||||||
|
&name,
|
||||||
|
blocked,
|
||||||
|
ProtectionStatus::Unprotected,
|
||||||
|
param,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to create mailinglist '{}' for grpid={}",
|
||||||
|
&name, &listid
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
|
||||||
|
Ok(Some((chat_id, blocked)))
|
||||||
|
} else {
|
||||||
|
info!(context, "Creating list forbidden by caller.");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::indexing_slicing)]
|
||||||
|
fn compute_mailinglist_name(
|
||||||
|
list_id_header: &str,
|
||||||
|
listid: &str,
|
||||||
|
mime_parser: &MimeMessage,
|
||||||
|
) -> String {
|
||||||
|
let mut name = match LIST_ID_REGEX.captures(list_id_header) {
|
||||||
|
Some(cap) => cap[1].trim().to_string(),
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
// for mailchimp lists, the name in `ListId` is just a long number.
|
// for mailchimp lists, the name in `ListId` is just a long number.
|
||||||
// a usable name for these lists is in the `From` header
|
// a usable name for these lists is in the `From` header
|
||||||
// and we can detect these lists by a unique `ListId`-suffix.
|
// and we can detect these lists by a unique `ListId`-suffix.
|
||||||
@@ -2048,50 +2074,14 @@ async fn create_or_lookup_mailinglist(
|
|||||||
// 51231231231231231231231232869f58.xing.com -> xing.com
|
// 51231231231231231231231232869f58.xing.com -> xing.com
|
||||||
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
|
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
|
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
|
||||||
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(&listid) {
|
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(listid) {
|
||||||
name = cap[2].to_string();
|
name = cap[2].to_string();
|
||||||
} else {
|
} else {
|
||||||
name = listid.clone();
|
name = listid.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if allow_creation {
|
strip_rtlo_characters(&name)
|
||||||
// list does not exist but should be created
|
|
||||||
let param = mime_parser.list_post.as_ref().map(|list_post| {
|
|
||||||
let mut p = Params::new();
|
|
||||||
p.set(Param::ListPost, list_post);
|
|
||||||
p.to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
|
||||||
let blocked = if is_bot {
|
|
||||||
Blocked::Not
|
|
||||||
} else {
|
|
||||||
Blocked::Request
|
|
||||||
};
|
|
||||||
let chat_id = ChatId::create_multiuser_record(
|
|
||||||
context,
|
|
||||||
Chattype::Mailinglist,
|
|
||||||
&listid,
|
|
||||||
&name,
|
|
||||||
blocked,
|
|
||||||
ProtectionStatus::Unprotected,
|
|
||||||
param,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to create mailinglist '{}' for grpid={}",
|
|
||||||
&name, &listid
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
|
|
||||||
Ok(Some((chat_id, blocked)))
|
|
||||||
} else {
|
|
||||||
info!(context, "Creating list forbidden by caller.");
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set ListId param on the contact and ListPost param the chat.
|
/// Set ListId param on the contact and ListPost param the chat.
|
||||||
@@ -2100,9 +2090,10 @@ async fn create_or_lookup_mailinglist(
|
|||||||
async fn apply_mailinglist_changes(
|
async fn apply_mailinglist_changes(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mime_parser: &MimeMessage,
|
mime_parser: &MimeMessage,
|
||||||
|
sent_timestamp: i64,
|
||||||
chat_id: ChatId,
|
chat_id: ChatId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let Some(list_post) = &mime_parser.list_post else {
|
let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2112,6 +2103,24 @@ async fn apply_mailinglist_changes(
|
|||||||
}
|
}
|
||||||
let listid = &chat.grpid;
|
let listid = &chat.grpid;
|
||||||
|
|
||||||
|
let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
|
||||||
|
if chat.name != new_name
|
||||||
|
&& chat_id
|
||||||
|
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
info!(context, "Updating listname for chat {chat_id}.");
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
|
||||||
|
.await?;
|
||||||
|
context.emit_event(EventType::ChatModified(chat_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(list_post) = &mime_parser.list_post else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
let list_post = match ContactAddress::new(list_post) {
|
let list_post = match ContactAddress::new(list_post) {
|
||||||
Ok(list_post) => list_post,
|
Ok(list_post) => list_post,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -686,6 +686,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
|||||||
PRAGMA secure_delete=on;
|
PRAGMA secure_delete=on;
|
||||||
PRAGMA busy_timeout = 0; -- fail immediately
|
PRAGMA busy_timeout = 0; -- fail immediately
|
||||||
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
||||||
|
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
|
||||||
PRAGMA foreign_keys=on;
|
PRAGMA foreign_keys=on;
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -1546,7 +1546,7 @@ mod tests {
|
|||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
assert_eq!(chats.len(), 0);
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
// a subsequent call to update_device_chats() must not re-add manally deleted messages or chats
|
// a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
|
||||||
t.update_device_chats().await.unwrap();
|
t.update_device_chats().await.unwrap();
|
||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
assert_eq!(chats.len(), 0);
|
assert_eq!(chats.len(), 0);
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ pub(crate) fn create_id() -> String {
|
|||||||
/// Function generates a Message-ID that can be used for a new outgoing message.
|
/// Function generates a Message-ID that can be used for a new outgoing message.
|
||||||
/// - this function is called for all outgoing messages.
|
/// - this function is called for all outgoing messages.
|
||||||
/// - the message ID should be globally unique
|
/// - the message ID should be globally unique
|
||||||
/// - do not add a counter or any private data as this leaks information unncessarily
|
/// - do not add a counter or any private data as this leaks information unnecessarily
|
||||||
pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
|
pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
|
||||||
let hostname = from_addr
|
let hostname = from_addr
|
||||||
.find('@')
|
.find('@')
|
||||||
|
|||||||
Reference in New Issue
Block a user