Compare commits

...

28 Commits

Author SHA1 Message Date
link2xt
6077499f07 chore(release): prepare for 2.28.0 2025-11-23 17:08:42 +00:00
Simon Laux
94d2d8cfd7 feat: add api to get information about diskspace usage of database. (jsonrpc method: get_storage_usage_report_string) (#7486)
new jsonrpc api: `get_storage_usage_report_string(accountId)`
new rust API: `get_storage_usage(&context)`
2025-11-23 15:18:00 +00:00
iequidoo
ba3cad6ad6 docs: Mark db encryption support as deprecated (#7403)
- Db encryption does nothing with blobs, so fs/disk encryption is recommended.
- Isolation from other apps is needed anyway.
- Experimental database encryption was removed on iOS and Android.
- Delta Touch is using CFFI API with a manually entered password because Ubuntu Touch does not offer
  filesystem or disk encryption, but we don't want new users of these APIs, such as bot developers.
2025-11-22 18:36:40 -03:00
link2xt
c9c362d5ff api: get_existing_msg_ids()
This API allows to check if the message with
given ID exists and distinguish between
message not existing and database error.
It might also be faster than
checking messages one by one
if multiple messages need to be checked
because of using a single SQL transaction.
2025-11-22 18:19:44 +00:00
iequidoo
6514b4ca7f fix: Look up or create ad-hoc group if there are duplicate addresses in "To"
Fix `test_unencrypted_doesnt_goto_self_chat` as well, it was only testing the first message because
of using the same Message-ID for all messages.
2025-11-22 02:48:27 -03:00
link2xt
e7e31d7914 ci: do not use --encoding option for rst-lint
It was removed in rst-lint 2.0:
7b43036b4d
2025-11-22 05:26:03 +00:00
B. Petersen
51d6855e0d fix: add missing stock strings 2025-11-21 14:42:50 +01:00
Hocuri
2f90b55309 feat: Stock string for joining a channel (#7480)
Add a stock string `%1$s invited you to join this channel.\n\nWaiting
for the device of %2$s to reply…`, which is shown when a user starts to
join a channel.

I did _not_ add an equivalent to `%1$s replied, waiting for being added
to the group…`, which is shown when vg-auth-required was received. I
don't think that this would add any information that's interesting to
the user, other than 'something is happening, hang on'. And the more
text on the screen, the less likely that anyone reads it. But if others
think differently, we can also add it.

With this PR, joining a channel looks like this:

```
Msg#2003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#2004: info (Contact#Contact#Info): Alice invited you to join this channel.

Waiting for the device of Alice to reply… [NOTICED][INFO]
Msg#2007🔒:  (Contact#Contact#2001): You joined the channel. [FRESH][INFO]
```
2025-11-20 21:13:04 +00:00
link2xt
be3e202470 feat: allow adding second transport 2025-11-20 15:51:19 +00:00
link2xt
57aadfbbf6 chore: update preloaded DNS cache 2025-11-19 19:55:53 +00:00
link2xt
849cde9757 refactor: remove some easy to remove unwrap() calls 2025-11-19 17:38:58 +00:00
link2xt
b4cd99fc56 docs: replace some references to Delta Chat with chatmail 2025-11-19 04:11:12 +00:00
iequidoo
9305a0676c fix: Assign outgoing self-sent unencrypted messages to ad-hoc groups with only SELF (#7409)
Before, outgoing self-sent unencrypted messages were assigned to the self-chat. Now we assign them
to ad-hoc groups with only SELF instead of 1:1 chats with address contacts corresponding to our own
addresses because we don't want to create such address contacts; we still use SELF for `from_id` of
such messages. Not assigning such messages to the encrypted chat should be safe enough and such
messages can actually be sent by the user from another MUA.
2025-11-18 20:34:56 -03:00
B. Petersen
39c9ba19ef docs: add missing RFC 9788, link 'Header Protection for Cryptographically Protected Email' as other RFC 2025-11-18 23:16:10 +01:00
link2xt
af574279fd docs: remove unsupported RFC 3503 ($MDNSent flag) from the list of standards 2025-11-18 21:44:41 +00:00
Hocuri
713c929e03 refactor: Rename add_encrypted_msg -> add_e2ee_notice 2025-11-18 18:58:26 +01:00
Hocuri
c83c131a37 feat: Rephrase "Establishing end-to-end encryption" -> "Establishing connection" 2025-11-18 18:58:26 +01:00
Hocuri
0d0602a4a5 fix: Sort system messages to the bottom of the chat
Fix #7435

For most messages, `calc_sort_timestamp()` makes sure that they are at the correct place; esp. that they are not above system messages or other noticed/seen messages.

Most callers of `add_info_msg()`, however, didn't call `calc_sort_timestamp()`, and just used `time()` or `smeared_time()` to get the sort timestamp. Because of this, system messages could sometimes wrongly be sorted above other messages.

This PR fixes this by making the sort timestamp optional in `add_info_msg*()`. If the sort timestamp isn't passed, then the message is sorted to the bottom of the chat. `sent_rcvd_timestamp` is not optional anymore, because we need _some_ timestamp that can be shown to the user (most callers just pass `time()` there).
2025-11-18 18:58:26 +01:00
link2xt
abfb556377 fix: set SQLite busy timeout to 1 minute on iOS
Closes <https://github.com/chatmail/core/issues/7464>
2025-11-18 17:07:27 +00:00
link2xt
72788daca0 refactor: use HashMap::extract_if() stabilized in Rust 1.88.0 2025-11-18 13:16:44 +00:00
iequidoo
16bd87c78f test: Contact shalln't be verified by another having unknown verifier
It must be verified by "unknown verifier" instead. But if the verifier has known verifier in turn,
it must reverify contacts having unknown verifier. Add a check for this also.
2025-11-18 05:42:46 -03:00
iequidoo
d44e2420bc fix: ContactId::set_name_ex(): Emit ContactsChanged when transaction is completed
This fixes flaky JSON-RPC's `test_rename_synchronization()`.
2025-11-18 02:17:31 -03:00
dependabot[bot]
88d213fcdb chore(deps): bump astral-sh/setup-uv from 7.1.2 to 7.1.3
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.2 to 7.1.3.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](85856786d1...5a7eac68fb)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 02:12:53 +00:00
link2xt
fb14acb0fb fix: limit the range of Date to up to 6 days in the past
Previous value (1000000 seconds) was slightly more than 11.5 days.
2025-11-17 23:17:55 +00:00
link2xt
a5c470fbae build(nix): update fenix and use it for all Rust builds
`fenix` input provides updated Rust packages.
Updating it is needed since current version is 1.86.0
and MSRV has been increased to 1.88.0.
2025-11-17 15:23:58 +00:00
link2xt
6bdba33d32 build: update rPGP from 0.17.0 to 0.18.0 2025-11-16 15:18:55 +00:00
link2xt
c6ace749e3 build: increase MSRV to 1.88.0
It is required by rPGP 0.18.0.

All the changes in `.rs` files are made automatically with `clippy --fix`.
2025-11-16 14:48:50 +00:00
link2xt
22ebd6436f feat: default bcc_self to 0 for new accounts 2025-11-16 10:00:00 +00:00
78 changed files with 2312 additions and 1532 deletions

View File

@@ -23,7 +23,7 @@ env:
RUST_VERSION: 1.91.0
# Minimum Supported Rust Version
MSRV: 1.85.0
MSRV: 1.88.0
jobs:
lint_rust:

View File

@@ -19,7 +19,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

View File

@@ -1,5 +1,55 @@
# Changelog
## [2.28.0] - 2025-11-23
### API-Changes
- New API `get_existing_msg_ids()` to check if the messages with given IDs exist.
- Add API to get storage usage information. (JSON-RPC method: `get_storage_usage_report_string`) ([#7486](https://github.com/chatmail/core/pull/7486)).
### Features / Changes
- Experimentaly allow adding second transport.
There is no synchronization yet, so UIs should not allow the user to change the address manually and only expose the ability to add transports if `bcc_self` is disabled.
- Default `bcc_self` to 0 for all new accounts.
- Rephrase "Establishing end-to-end encryption" -> "Establishing connection".
- Stock string for joining a channel ([#7480](https://github.com/chatmail/core/pull/7480)).
### Fixes
- Limit the range of `Date` to up to 6 days in the past.
- `ContactId::set_name_ex()`: Emit ContactsChanged when transaction is completed.
- Set SQLite busy timeout to 1 minute on iOS.
- Sort system messages to the bottom of the chat.
- Assign outgoing self-sent unencrypted messages to ad-hoc groups with only SELF ([#7409](https://github.com/chatmail/core/pull/7409)).
- Add missing stock strings.
- Look up or create ad-hoc group if there are duplicate addresses in "To".
### Documentation
- Add missing RFC 9788, link 'Header Protection for Cryptographically Protected Email' as other RFC.
- Remove unsupported RFC 3503 (`$MDNSent` flag) from the list of standards.
- Mark database encryption support as deprecated ([#7403](https://github.com/chatmail/core/pull/7403)).
### Build system
- Increase Minimum Supported Rust Version to 1.88.0.
- Update rPGP from 0.17.0 to 0.18.0.
- nix: Update `fenix` and use it for all Rust builds.
### CI
- Do not use --encoding option for rst-lint.
### Refactor
- Use `HashMap::extract_if()` stabilized in Rust 1.88.0.
- Remove some easy to remove unwrap() calls.
### Tests
- Contact shalln't be verified by another having unknown verifier.
## [2.27.0] - 2025-11-16
### API-Changes
@@ -7185,3 +7235,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.25.0]: https://github.com/chatmail/core/compare/v2.24.0..v2.25.0
[2.26.0]: https://github.com/chatmail/core/compare/v2.25.0..v2.26.0
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0

View File

@@ -1,4 +1,4 @@
# Contributing to Delta Chat
# Contributing to chatmail core
## Bug reports

18
Cargo.lock generated
View File

@@ -1304,7 +1304,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.27.0"
version = "2.28.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1413,7 +1413,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.27.0"
version = "2.28.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1435,7 +1435,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.27.0"
version = "2.28.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1451,7 +1451,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.27.0"
version = "2.28.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1480,7 +1480,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.27.0"
version = "2.28.0"
dependencies = [
"anyhow",
"deltachat",
@@ -4142,9 +4142,9 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.17.0"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d918d5da2ce943e4c6088d7694f33f47c19374d6f0f2080a0c5e8010afdfd29"
checksum = "66d4a27a4d5cfd4e185ddd3eff94dee0f611c4c3e776422254237c54c336c160"
dependencies = [
"aead",
"aes",
@@ -5053,9 +5053,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.7"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "2.27.0"
version = "2.28.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
rust-version = "1.88"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -78,7 +78,7 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.17.0", default-features = false }
pgp = { version = "0.18.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.38", features = ["escape-html"] }

View File

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

View File

@@ -247,7 +247,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
// create/open/config/information
/**
* Create a new context object and try to open it without passphrase. If
* Create a new context object and try to open it. If
* database is encrypted, the result is the same as using
* dc_context_new_closed() and the database should be opened with
* dc_context_open() before using.
@@ -283,8 +283,13 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
/**
* Opens the database with the given passphrase. This can only be used on
* closed context, such as created by dc_context_new_closed(). If the database
* Opens the database with the given passphrase.
* NB: Nonempty passphrase (db encryption) is deprecated 2025-11:
* - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
* - Isolation from other apps is needed anyway.
*
* This can only be used on closed context, such as
* created by dc_context_new_closed(). If the database
* is new, this operation sets the database passphrase. For existing databases
* the passphrase should be the one used to encrypt the database the first
* time.
@@ -301,6 +306,8 @@ int dc_context_open (dc_context_t *context, const char*
/**
* Changes the passphrase on the open database.
* Deprecated 2025-11, see `dc_context_open()` for reasoning.
*
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
* It is impossible to encrypt unencrypted database with this method and vice versa.
*
@@ -7712,7 +7719,12 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/// "Establishing guaranteed end-to-end encryption, please wait…"
/// "Member %1$s removed."
///
/// `%1$s` will be replaced by name of the removed contact.
#define DC_STR_REMOVE_MEMBER 178
/// "Establishing connection, please wait…"
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT 190
@@ -7756,7 +7768,22 @@ void dc_event_unref(dc_event_t* event);
/// Subtitle for channel join qrcode svg image generated by the core.
///
/// `%1$s` will be replaced with the channel name.
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
/// "You joined the channel."
#define DC_STR_MSG_YOU_JOINED_CHANNEL 202
/// "%1$s invited you to join this channel. Waiting for the device of %2$s to reply…"
///
/// Added as an info-message directly after scanning a QR code for joining a broadcast channel.
///
/// `%1$s` and `%2$s` will both be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_CHANNEL_STARTED 203
/// "The attachment contains anonymous usage statistics, which help us improve Delta Chat. Thank you!"
///
/// Used as the message body for statistics sent out.
#define DC_STR_STATS_MSG_BODY 210
/// "Proxy Enabled"
///

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.27.0"
version = "2.28.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -21,9 +21,9 @@ use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipts, markseen_msgs, Message,
MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -34,6 +34,7 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::storage_usage::get_storage_usage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
@@ -366,6 +367,13 @@ impl CommandApi {
ctx.get_info().await
}
/// Get storage usage report as formatted string
async fn get_storage_usage_report_string(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let storage_usage = get_storage_usage(&ctx).await?;
Ok(storage_usage.to_string())
}
/// Get the blob dir.
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
@@ -1295,6 +1303,19 @@ impl CommandApi {
.collect())
}
/// Checks if the messages with given IDs exist.
///
/// Returns IDs of existing messages.
async fn get_existing_msg_ids(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<Vec<u32>> {
let context = self.get_context(account_id).await?;
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
Ok(existing_msg_ids
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect())
}
async fn get_message_list_items(
&self,
account_id: u32,

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.27.0"
"version": "2.28.0"
}

View File

@@ -64,6 +64,7 @@ describe("online tests", function () {
await dc.rpc.setConfig(accountId1, "addr", account1.email);
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
await dc.rpc.configure(accountId1);
await waitForEvent(dc, "ImapInboxIdle", accountId1);
accountId2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(accountId2, {
@@ -71,6 +72,7 @@ describe("online tests", function () {
mail_pw: account2.password,
});
await dc.rpc.configure(accountId2);
await waitForEvent(dc, "ImapInboxIdle", accountId2);
accountsConfigured = true;
});

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.27.0"
version = "2.28.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.27.0"
version = "2.28.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -130,6 +130,10 @@ class Account:
"""Add a new transport using a QR code."""
yield self._rpc.add_transport_from_qr.future(self.id, qr)
def delete_transport(self, addr: str):
"""Delete a transport."""
self._rpc.delete_transport(self.id, addr)
@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""

View File

@@ -40,12 +40,17 @@ class ACFactory:
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
return f"{username}@{domain}", f"{username}${username}"
def get_account_qr(self):
"""Return "dcaccount:" QR code for testing chatmail relay."""
domain = os.getenv("CHATMAIL_DOMAIN")
return f"dcaccount:{domain}"
@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
account = self.get_unconfigured_account()
domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
qr = self.get_account_qr()
yield account.add_transport_from_qr.future(qr)
assert account.is_configured()
return account
@@ -77,6 +82,7 @@ class ACFactory:
ac_clone = self.get_unconfigured_account()
for transport in transports:
ac_clone.add_or_update_transport(transport)
ac_clone.bring_online()
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:

View File

@@ -143,7 +143,7 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
if event.kind == EventType.INFO and "transport 1: UID validity for folder DeltaChat changed from " in event.msg:
break
ac2 = acfactory.get_online_account()

View File

@@ -0,0 +1,158 @@
import pytest
from deltachat_rpc_client.rpc import JsonRpcError
def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
# When the first transport is created,
# mvbox_move and only_fetch_mvbox should be disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 3
first_addr = account.list_transports()[0]["addr"]
second_addr = account.list_transports()[1]["addr"]
# Cannot delete the first address.
with pytest.raises(JsonRpcError):
account.delete_transport(first_addr)
account.delete_transport(second_addr)
assert len(account.list_transports()) == 2
# Enabling mvbox_move or only_fetch_mvbox
# is not allowed when multi-transport is enabled.
for option in ["mvbox_move", "only_fetch_mvbox"]:
with pytest.raises(JsonRpcError):
account.set_config(option, "1")
with pytest.raises(JsonRpcError):
account.set_config("show_emails", "0")
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
"""Test that second transport cannot be configured if mvbox is used."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
qr = acfactory.get_account_qr()
account.set_config(key, "1")
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_no_second_transport_without_classic_emails(acfactory) -> None:
"""Test that second transport cannot be configured if classic emails are not fetched."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr()
account.set_config("show_emails", "0")
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello!")
msg1 = bob.wait_for_incoming_msg().get_snapshot()
sender_addr1 = msg1.sender.get_snapshot().address
alice.stop_io()
old_alice_addr = alice.get_config("configured_addr")
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr in alice_vcard
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
with pytest.raises(JsonRpcError):
# Cannot use the address that is not
# configured for any transport.
alice.set_config("configured_addr", bob_addr)
# Load old address so it is cached.
assert alice.get_config("configured_addr") == old_alice_addr
alice.set_config("configured_addr", new_alice_addr)
# Make sure that setting `configured_addr` invalidated the cache.
assert alice.get_config("configured_addr") == new_alice_addr
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr not in alice_vcard
assert new_alice_addr in alice_vcard
with pytest.raises(JsonRpcError):
alice.delete_transport(new_alice_addr)
alice.start_io()
alice_chat_bob.send_text("Hello again!")
msg2 = bob.wait_for_incoming_msg().get_snapshot()
sender_addr2 = msg2.sender.get_snapshot().address
assert msg1.sender == msg2.sender
assert sender_addr1 != sender_addr2
assert sender_addr1 == old_alice_addr
assert sender_addr2 == new_alice_addr
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
Disabling mvbox_move is required to be able to setup a second transport.
"""
account = acfactory.get_unconfigured_account()
account.set_config("fix_is_chatmail", "1")
account.set_config("is_chatmail", is_chatmail)
# The default value when the setting is unset is "1".
# This is not changed for compatibility with old databases
# imported from backups.
assert account.get_config("mvbox_move") == "1"
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
# Once the first transport is set up,
# mvbox_move is disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("is_chatmail") == is_chatmail
def test_reconfigure_transport(acfactory) -> None:
"""Test that reconfiguring the transport works
even if settings not supported for multi-transport
like mvbox_move are enabled."""
account = acfactory.get_online_account()
account.set_config("mvbox_move", "1")
[transport] = account.list_transports()
account.add_or_update_transport(transport)
# Reconfiguring the transport should not reset
# the settings as if when configuring the first transport.
assert account.get_config("mvbox_move") == "1"

View File

@@ -158,29 +158,29 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs[0].get_snapshot()
encrypted_msg = chat_msgs.pop(0).get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs[1].get_snapshot()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert "invited you to join this channel" in first_msg.text
assert first_msg.is_info
member_added_msg = chat_msgs.pop(0).get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == "You joined the channel."
assert member_added_msg.is_info
hello_msg = chat_msgs[2].get_snapshot()
hello_msg = chat_msgs.pop(0).get_snapshot()
assert hello_msg.text == "Hello everyone!"
assert not hello_msg.is_info
assert hello_msg.show_padlock
assert hello_msg.error is None
assert len(chat_msgs) == 3
assert len(chat_msgs) == 0
chat_snapshot = chat.get_full_snapshot()
assert chat_snapshot.is_encrypted

View File

@@ -467,7 +467,7 @@ def test_bot(acfactory) -> None:
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
alice = acfactory.get_online_account()
# Create a bot account so it does not receive device messages in the beginning.
addr, password = acfactory.get_credentials()
@@ -475,6 +475,7 @@ def test_wait_next_messages(acfactory) -> None:
bot.set_config("bot", "1")
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
bot.bring_online()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
@@ -867,15 +868,15 @@ def test_leave_broadcast(acfactory, all_devices_online):
contact_snapshot = contact.get_snapshot()
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs.pop(0).get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert "invited you to join this channel" in first_msg.text
assert first_msg.is_info
member_added_msg = chat_msgs.pop(0).get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."

View File

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

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.27.0"
"version": "2.28.0"
}

18
flake.lock generated
View File

@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1747291057,
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
"lastModified": 1763361733,
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
"owner": "nix-community",
"repo": "fenix",
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"type": "github"
},
"original": {
@@ -202,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1746889290,
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
"lastModified": 1762860488,
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
"type": "github"
},
"original": {

View File

@@ -1,5 +1,5 @@
{
description = "Delta Chat core";
description = "Chatmail core";
inputs = {
fenix.url = "github:nix-community/fenix";
flake-utils.url = "github:numtide/flake-utils";
@@ -14,7 +14,15 @@
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
fenixPkgs = fenix.packages.${system};
naersk' = pkgs.callPackage naersk { };
fenixToolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.stable.rust-std
];
naersk' = pkgs.callPackage naersk {
cargo = fenixToolchain;
rustc = fenixToolchain;
};
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
@@ -470,6 +478,12 @@
};
libdeltachat =
let
rustPlatform = (pkgs.makeRustPlatform {
cargo = fenixToolchain;
rustc = fenixToolchain;
});
in
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
@@ -479,8 +493,9 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
rustPlatform.cargoSetupHook
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
];
postInstall = ''

View File

@@ -14,6 +14,7 @@ def datadir():
return None
@pytest.mark.skip("The test is flaky in CI and crashes the interpreter as of 2025-11-12")
def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating one echo_and_quit bot")
botproc = acfactory.run_bot_process(echo_and_quit)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.27.0"
version = "2.28.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -35,7 +35,7 @@ class TestOfflineAccountBasic:
d = ac1.get_info()
assert d["arch"]
assert d["number_of_chats"] == "0"
assert d["bcc_self"] == "1"
assert d["bcc_self"] == "0"
def test_is_not_configured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -69,7 +69,7 @@ class TestOfflineAccountBasic:
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
assert ac1.get_config("bcc_self") == "1"
assert ac1.get_config("bcc_self") == "0"
def test_selfcontact_if_unconfigured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -46,7 +46,7 @@ deps =
commands =
ruff format --diff setup.py src/deltachat examples/ tests/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
rst-lint README.rst
[testenv:mypy]
deps =

View File

@@ -1 +1 @@
2025-11-16
2025-11-23

View File

@@ -678,13 +678,12 @@ impl Config {
// Convert them to relative paths.
let mut modified = false;
for account in &mut config.inner.accounts {
if account.dir.is_absolute() {
if let Some(old_path_parent) = account.dir.parent() {
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
account.dir = new_path.to_path_buf();
modified = true;
}
}
if account.dir.is_absolute()
&& let Some(old_path_parent) = account.dir.parent()
&& let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
{
account.dir = new_path.to_path_buf();
modified = true;
}
}
if modified && writable {

View File

@@ -301,7 +301,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_encrypted(context).await? {
chat_id.add_encrypted_msg(context, timestamp).await?;
chat_id.add_e2ee_notice(context, timestamp).await?;
}
info!(
@@ -462,19 +462,15 @@ impl ChatId {
}
/// Adds message "Messages are end-to-end encrypted".
pub(crate) async fn add_encrypted_msg(
self,
context: &Context,
timestamp_sort: i64,
) -> Result<()> {
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
let text = stock_str::messages_e2e_encrypted(context).await;
add_info_msg_with_cmd(
context,
self,
&text,
SystemMessage::ChatE2ee,
timestamp_sort,
None,
Some(timestamp),
timestamp,
None,
None,
None,
@@ -740,16 +736,15 @@ impl ChatId {
}
}
_ => {
if msg.viewtype == Viewtype::File {
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
if msg.viewtype == Viewtype::File
&& let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
// We do not do an automatic conversion to other viewtypes here so that
// users can send images as "files" to preserve the original quality
// (usually we compress images). The remaining conversions are done by
// `prepare_msg_blob()` later.
.filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
{
msg.viewtype = better_type;
}
{
msg.viewtype = better_type;
}
if msg.viewtype == Viewtype::Vcard {
let blob = msg
@@ -767,13 +762,13 @@ impl ChatId {
msg.chat_id = self;
// if possible, replace existing draft and keep id
if !msg.id.is_special() {
if let Some(old_draft) = self.get_draft(context).await? {
if old_draft.id == msg.id
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
let affected_rows = context
if !msg.id.is_special()
&& let Some(old_draft) = self.get_draft(context).await?
&& old_draft.id == msg.id
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
let affected_rows = context
.sql.execute(
"UPDATE msgs
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
@@ -793,9 +788,7 @@ impl ChatId {
msg.id,
),
).await?;
return Ok(affected_rows > 0);
}
}
return Ok(affected_rows > 0);
}
let row_id = context
@@ -993,11 +986,11 @@ impl ChatId {
let mut res = Vec::new();
let now = time();
for (chat_id, metric) in chats_with_metrics {
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
if now > chat_timestamp + 42 * 24 * 3600 {
// Chat was inactive for 42 days, skip.
continue;
}
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await?
&& now > chat_timestamp + 42 * 24 * 3600
{
// Chat was inactive for 42 days, skip.
continue;
}
if metric < 0.1 {
@@ -1252,10 +1245,10 @@ impl ChatId {
None
};
if let Some(last_msg_time) = last_msg_time {
if last_msg_time > sort_timestamp {
sort_timestamp = last_msg_time;
}
if let Some(last_msg_time) = last_msg_time
&& last_msg_time > sort_timestamp
{
sort_timestamp = last_msg_time;
}
Ok(sort_timestamp)
@@ -1376,10 +1369,10 @@ impl Chat {
let mut chat_name = "Err [Name not found]".to_owned();
match get_chat_contacts(context, chat.id).await {
Ok(contacts) => {
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
contact.get_display_name().clone_into(&mut chat_name);
}
if let Some(contact_id) = contacts.first()
&& let Ok(contact) = Contact::get_by_id(context, *contact_id).await
{
contact.get_display_name().clone_into(&mut chat_name);
}
}
Err(err) => {
@@ -1576,10 +1569,10 @@ impl Chat {
if self.typ == Chattype::Single {
let contacts = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
color = contact.get_color();
}
if let Some(contact_id) = contacts.first()
&& let Ok(contact) = Contact::get_by_id(context, *contact_id).await
{
color = contact.get_color();
}
} else if !self.grpid.is_empty() {
color = str_to_color(&self.grpid);
@@ -1841,8 +1834,8 @@ impl Chat {
}
// add independent location to database
if msg.param.exists(Param::SetLatitude) {
if let Ok(row_id) = context
if msg.param.exists(Param::SetLatitude)
&& let Ok(row_id) = context
.sql
.insert(
"INSERT INTO locations \
@@ -1857,9 +1850,8 @@ impl Chat {
),
)
.await
{
location_id = row_id;
}
{
location_id = row_id;
}
let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
@@ -2429,7 +2421,7 @@ impl ChatIdBlocked {
&& !chat.param.exists(Param::Devicetalk)
&& !chat.param.exists(Param::Selftalk)
{
chat_id.add_encrypted_msg(context, smeared_time).await?;
chat_id.add_e2ee_notice(context, smeared_time).await?;
}
Ok(Self {
@@ -2497,18 +2489,18 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
msg.param.set(Param::File, blob.as_name());
}
if !msg.param.exists(Param::MimeType) {
if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) {
// If we unexpectedly didn't recognize the file as image, don't send it as such,
// either the format is unsupported or the image is corrupted.
let mime = match viewtype != Viewtype::Image
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
{
true => mime,
false => "application/octet-stream",
};
msg.param.set(Param::MimeType, mime);
}
if !msg.param.exists(Param::MimeType)
&& let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg)
{
// If we unexpectedly didn't recognize the file as image, don't send it as such,
// either the format is unsupported or the image is corrupted.
let mime = match viewtype != Viewtype::Image
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
{
true => mime,
false => "application/octet-stream",
};
msg.param.set(Param::MimeType, mime);
}
msg.try_calc_and_set_dimensions(context).await?;
@@ -2692,15 +2684,15 @@ async fn prepare_send_msg(
// This is meant as a last line of defence, the UI should check that before as well.
// (We allow Chattype::Single in general for "Reply Privately";
// checking for exact contact_id will produce false positives when ppl just left the group)
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
if let Some(quoted_message) = msg.quoted_message(context).await? {
if quoted_message.chat_id != chat_id {
bail!(
"Quote of message from {} cannot be sent to {chat_id}",
quoted_message.chat_id
);
}
}
if chat.typ != Chattype::Single
&& !context.get_config_bool(Config::Bot).await?
&& let Some(quoted_message) = msg.quoted_message(context).await?
&& quoted_message.chat_id != chat_id
{
bail!(
"Quote of message from {} cannot be sent to {chat_id}",
quoted_message.chat_id
);
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2830,16 +2822,15 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let now = smeared_time(context);
if rendered_msg.last_added_location_id.is_some() {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if rendered_msg.last_added_location_id.is_some()
&& let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await
{
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
}
if attach_selfavatar && let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await
{
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
}
if rendered_msg.is_encrypted {
@@ -3457,7 +3448,7 @@ pub(crate) async fn create_group_ex(
if !grpid.is_empty() {
// Add "Messages are end-to-end encrypted." message.
chat_id.add_encrypted_msg(context, timestamp).await?;
chat_id.add_e2ee_notice(context, timestamp).await?;
}
if !context.get_config_bool(Config::Bot).await?
@@ -3470,7 +3461,7 @@ pub(crate) async fn create_group_ex(
// Add "Messages in this chat use classic email and are not encrypted." message.
stock_str::chat_unencrypted_explanation(context).await
};
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
add_info_msg(context, chat_id, &text).await?;
}
if let (true, true) = (sync.into(), !grpid.is_empty()) {
let id = SyncId::Grpid(grpid);
@@ -3538,7 +3529,7 @@ pub(crate) async fn create_out_broadcast_ex(
Ok(chat_id)
};
let chat_id = context.sql.transaction(trans_fn).await?;
chat_id.add_encrypted_msg(context, timestamp).await?;
chat_id.add_e2ee_notice(context, timestamp).await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
@@ -4483,11 +4474,11 @@ pub async fn add_device_msg_with_importance(
let mut chat_id = ChatId::new(0);
let mut msg_id = MsgId::new_unset();
if let Some(label) = label {
if was_device_msg_ever_added(context, label).await? {
info!(context, "Device-message {label} already added.");
return Ok(msg_id);
}
if let Some(label) = label
&& was_device_msg_ever_added(context, label).await?
{
info!(context, "Device-message {label} already added.");
return Ok(msg_id);
}
if let Some(msg) = msg {
@@ -4499,10 +4490,10 @@ pub async fn add_device_msg_with_importance(
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
msg.timestamp_sort = timestamp_sent;
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
if msg.timestamp_sort <= last_msg_time {
msg.timestamp_sort = last_msg_time + 1;
}
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
&& msg.timestamp_sort <= last_msg_time
{
msg.timestamp_sort = last_msg_time + 1;
}
prepare_msg_blob(context, msg).await?;
let state = MessageState::InFresh;
@@ -4621,9 +4612,11 @@ pub(crate) async fn add_info_msg_with_cmd(
chat_id: ChatId,
text: &str,
cmd: SystemMessage,
timestamp_sort: i64,
// Timestamp to show to the user (if this is None, `timestamp_sort` will be shown to the user)
timestamp_sent_rcvd: Option<i64>,
// Timestamp where in the chat the message will be sorted.
// If this is None, the message will be sorted to the bottom.
timestamp_sort: Option<i64>,
// Timestamp to show to the user
timestamp_sent_rcvd: i64,
parent: Option<&Message>,
from_id: Option<ContactId>,
added_removed_id: Option<ContactId>,
@@ -4639,6 +4632,22 @@ pub(crate) async fn add_info_msg_with_cmd(
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
}
let timestamp_sort = if let Some(ts) = timestamp_sort {
ts
} else {
let sort_to_bottom = true;
let (received, incoming) = (false, false);
chat_id
.calc_sort_timestamp(
context,
smeared_time(context),
sort_to_bottom,
received,
incoming,
)
.await?
};
let row_id =
context.sql.insert(
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
@@ -4648,8 +4657,8 @@ pub(crate) async fn add_info_msg_with_cmd(
from_id.unwrap_or(ContactId::INFO),
ContactId::INFO,
timestamp_sort,
timestamp_sent_rcvd.unwrap_or(0),
timestamp_sent_rcvd.unwrap_or(0),
timestamp_sent_rcvd,
timestamp_sent_rcvd,
Viewtype::Text,
MessageState::InNoticed,
text,
@@ -4669,19 +4678,14 @@ pub(crate) async fn add_info_msg_with_cmd(
}
/// Adds info message with a given text and `timestamp` to the chat.
pub(crate) async fn add_info_msg(
context: &Context,
chat_id: ChatId,
text: &str,
timestamp: i64,
) -> Result<MsgId> {
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: &str) -> Result<MsgId> {
add_info_msg_with_cmd(
context,
chat_id,
text,
SystemMessage::Unknown,
timestamp,
None,
time(),
None,
None,
None,

View File

@@ -1238,7 +1238,7 @@ async fn test_unarchive_if_muted() -> Result<()> {
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
set_muted(&t, chat_id, MuteDuration::Forever).await?;
send_text_msg(&t, chat_id, "out".to_string()).await?;
add_info_msg(&t, chat_id, "info", time()).await?;
add_info_msg(&t, chat_id, "info").await?;
assert_eq!(get_archived_cnt(&t).await?, 1);
// finally, unarchive on sending to not muted chat
@@ -1637,7 +1637,7 @@ async fn test_set_mute_duration() {
async fn test_add_info_msg() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group(&t, "foo").await?;
add_info_msg(&t, chat_id, "foo info", time()).await?;
add_info_msg(&t, chat_id, "foo info").await?;
let msg = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
@@ -1659,8 +1659,8 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
chat_id,
"foo bar info",
SystemMessage::EphemeralTimerChanged,
time(),
None,
time(),
None,
None,
None,
@@ -4507,7 +4507,7 @@ async fn test_info_not_referenced() -> Result<()> {
let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await;
let bob_chat_id = bob_received_message.chat_id;
add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?;
add_info_msg(bob, bob_chat_id, "Some info").await?;
// Bob sends a message.
// This message should reference Alice's "Hi!" message and not the info message.

View File

@@ -144,11 +144,11 @@ pub enum Config {
/// Send BCC copy to self.
///
/// Should be enabled for multidevice setups.
/// Default is 0 for chatmail accounts, 1 otherwise.
/// Should be enabled for multi-device setups.
///
/// This is automatically enabled when importing/exporting a backup,
/// setting up a second device, or receiving a sync message.
#[strum(props(default = "0"))]
BccSelf,
/// True if Message Delivery Notifications (read receipts) should
@@ -477,7 +477,10 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
)
}
}
@@ -523,10 +526,6 @@ impl Context {
// Default values
let val = match key {
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
false => Some("1".to_string()),
true => Some("0".to_string()),
},
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
@@ -717,6 +716,16 @@ impl Context {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
Self::check_config(key, value)?;
let n_transports = self.count_transports().await?;
if n_transports > 1
&& matches!(
key,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
)
{
bail!("Cannot reconfigure {key} when multiple transports are configured");
}
let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self).await?,
_ => Default::default(),
@@ -802,10 +811,11 @@ impl Context {
.await?;
}
Config::ConfiguredAddr => {
if self.is_configured().await? {
bail!("Cannot change ConfiguredAddr");
}
if let Some(addr) = value {
let Some(addr) = value else {
bail!("Cannot unset configured_addr");
};
if !self.is_configured().await? {
info!(
self,
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
@@ -816,6 +826,36 @@ impl Context {
.save_to_transports_table(self, &EnteredLoginParam::default())
.await?;
}
self.sql
.transaction(|transaction| {
if transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(addr,),
|row| {
let res: i64 = row.get(0)?;
Ok(res)
},
)? == 0
{
bail!("Address does not belong to any transport.");
}
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(addr,),
)?;
// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
// From address so we cannot send them over
// the new SMTP transport.
transaction.execute("DELETE FROM smtp", ())?;
transaction.execute("DELETE FROM imap_send", ())?;
Ok(())
})
.await?;
self.sql.uncache_raw_config("configured_addr").await;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;

View File

@@ -130,12 +130,6 @@ impl Context {
"cannot configure, database not opened."
);
param.addr = addr_normalize(&param.addr);
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), &param.addr) {
let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
progress!(self, 0, Some(error_msg.to_string()));
bail!(error_msg);
}
let cancel_channel = self.alloc_ongoing().await?;
let res = self
@@ -204,19 +198,72 @@ impl Context {
Ok(transports)
}
/// Returns the number of configured transports.
pub async fn count_transports(&self) -> Result<usize> {
self.sql.count("SELECT COUNT(*) FROM transports", ()).await
}
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
#[expect(clippy::unused_async)]
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
bail!(
"Adding and removing additional transports is not supported yet. Check back in a few months!"
)
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
self.sql
.transaction(|transaction| {
let primary_addr = transaction.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
(),
|row| {
let addr: String = row.get(0)?;
Ok(addr)
},
)?;
if primary_addr == addr {
bail!("Cannot delete primary transport");
}
let transport_id = transaction.query_row(
"DELETE FROM transports WHERE addr=? RETURNING id",
(addr,),
|row| {
let id: u32 = row.get(0)?;
Ok(id)
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
transaction.execute(
"DELETE FROM imap_sync WHERE transport_id=?",
(transport_id,),
)?;
Ok(())
})
.await?;
Ok(())
}
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
info!(self, "Configure ...");
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if old_addr.is_some()
&& !self
.sql
.exists(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(&param.addr,),
)
.await?
{
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with mvbox_move enabled.");
}
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
}
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
bail!("Cannot use multi-transport with disabled fetching of classic emails.");
}
}
let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
@@ -258,19 +305,18 @@ async fn on_configure_completed(
}
}
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Some(old_addr) = old_addr {
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
.log_err(context)
.ok();
}
}
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await?
&& let Some(old_addr) = old_addr
&& !addr_cmp(&new_addr, &old_addr)
{
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
.log_err(context)
.ok();
}
Ok(())
@@ -504,16 +550,9 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
// Configure IMAP
let transport_id = 0;
let (_s, r) = async_channel::bounded(1);
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
proxy_config,
&configured_param.addr,
strict_tls,
configured_param.oauth2,
r,
);
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
let configuring = true;
let mut imap_session = match imap.connect(ctx, configuring).await {
Ok(session) => session,
@@ -526,41 +565,19 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 850);
// Wait for SMTP configuration
smtp_config_task.await.unwrap()?;
smtp_config_task.await??;
progress!(ctx, 900);
let is_chatmail = match ctx.get_config_bool(Config::FixIsChatmail).await? {
false => {
let is_chatmail = imap_session.is_chatmail();
ctx.set_config(
Config::IsChatmail,
Some(match is_chatmail {
false => "0",
true => "1",
}),
)
.await?;
is_chatmail
}
true => ctx.get_config_bool(Config::IsChatmail).await?,
};
if is_chatmail {
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
ctx.set_config(Config::ShowEmails, None).await?;
if !ctx.is_configured().await? {
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
let create_mvbox = !is_chatmail;
let create_mvbox = false;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
let create = true;
imap_session
.select_with_uidvalidity(ctx, "INBOX", create)
.await
.context("could not read INBOX status")?;
drop(imap);
progress!(ctx, 910);

View File

@@ -154,10 +154,10 @@ fn parse_xml_reader<B: BufRead>(
if let Some(incoming_server) = parse_server(reader, event)? {
incoming_servers.push(incoming_server);
}
} else if tag == "outgoingserver" {
if let Some(outgoing_server) = parse_server(reader, event)? {
outgoing_servers.push(outgoing_server);
}
} else if tag == "outgoingserver"
&& let Some(outgoing_server) = parse_server(reader, event)?
{
outgoing_servers.push(outgoing_server);
}
}
Event::Eof => break,

View File

@@ -130,35 +130,37 @@ impl ContactId {
Ok((addr, fingerprint))
},
)?;
context.emit_event(EventType::ContactsChanged(Some(self)));
Ok(Some((addr, fingerprint)))
} else {
Ok(None)
}
})
.await?;
if row.is_some() {
context.emit_event(EventType::ContactsChanged(Some(self)));
}
if sync.into() {
if let Some((addr, fingerprint)) = row {
if fingerprint.is_empty() {
chat::sync(
context,
chat::SyncId::ContactAddr(addr),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
} else {
chat::sync(
context,
chat::SyncId::ContactFingerprint(fingerprint),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
if sync.into()
&& let Some((addr, fingerprint)) = row
{
if fingerprint.is_empty() {
chat::sync(
context,
chat::SyncId::ContactAddr(addr),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
} else {
chat::sync(
context,
chat::SyncId::ContactFingerprint(fingerprint),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
}
Ok(())
@@ -393,13 +395,13 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
);
}
}
if let Some(biography) = &contact.biography {
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
if let Some(biography) = &contact.biography
&& let Err(e) = set_status(context, id, biography.to_owned(), false, false).await
{
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
Ok(id)
}
@@ -1564,10 +1566,10 @@ impl Contact {
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
return Ok(Some(chat::get_unencrypted_icon(context).await?));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage)
&& !image_rel.is_empty()
{
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
Ok(None)
}
@@ -1800,10 +1802,11 @@ WHERE type=? AND id IN (
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
chat_id.unblock_ex(context, Nosync).await?;
}
if !new_blocking
&& contact.origin == Origin::MailinglistAddress
&& let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
chat_id.unblock_ex(context, Nosync).await?;
}
if sync.into() {

View File

@@ -46,7 +46,7 @@ use crate::{chatlist_events, stats};
///
/// # Examples
///
/// Creating a new unencrypted database:
/// Creating a new database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
@@ -61,24 +61,6 @@ use crate::{chatlist_events, stats};
/// drop(context);
/// # });
/// ```
///
/// To use an encrypted database provide a password. If the database does not yet exist it
/// will be created:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async move {
/// use deltachat::context::ContextBuilder;
///
/// let dir = tempfile::tempdir().unwrap();
/// let context = ContextBuilder::new(dir.path().join("db"))
/// .with_password("secret".into())
/// .open()
/// .await
/// .unwrap();
/// drop(context);
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct ContextBuilder {
dbfile: PathBuf,
@@ -150,9 +132,13 @@ impl ContextBuilder {
}
/// Sets the password to unlock the database.
/// Deprecated 2025-11:
/// - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
/// - Isolation from other apps is needed anyway.
///
/// If an encrypted database is used it must be opened with a password. Setting a
/// password on a new database will enable encryption.
#[deprecated(since = "TBD")]
pub fn with_password(mut self, password: String) -> Self {
self.password = Some(password);
self
@@ -180,7 +166,7 @@ impl ContextBuilder {
/// Builds the [`Context`] and opens it.
///
/// Returns error if context cannot be opened with the given passphrase.
/// Returns error if context cannot be opened.
pub async fn open(self) -> Result<Context> {
let password = self.password.clone().unwrap_or_default();
let context = self.build().await?;
@@ -400,9 +386,12 @@ impl Context {
}
/// Opens the database with the given passphrase.
/// NB: Db encryption is deprecated, so `passphrase` should be empty normally. See
/// [`ContextBuilder::with_password()`] for reasoning.
///
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
/// errors.
#[deprecated(since = "TBD")]
pub async fn open(&self, passphrase: String) -> Result<bool> {
if self.sql.check_passphrase(passphrase.clone()).await? {
self.sql.open(self, passphrase).await?;
@@ -413,6 +402,7 @@ impl Context {
}
/// Changes encrypted database passphrase.
/// Deprecated 2025-11, see [`ContextBuilder::with_password()`] for reasoning.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(passphrase).await?;
Ok(())
@@ -608,10 +598,9 @@ impl Context {
if self
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.await
&& let Err(err) = self.update_recent_quota(&mut session).await
{
if let Err(err) = self.update_recent_quota(&mut session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
warn!(self, "Failed to update quota: {err:#}.");
}
}
@@ -808,9 +797,10 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self)
.await?
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|| "Not configured".to_string(),
|(_transport_id, param)| param.to_string(),
);
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let chats = get_chat_cnt(self).await?;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;

View File

@@ -115,15 +115,13 @@ pub async fn maybe_set_logging_xdc_inner(
filename: Option<&str>,
msg_id: MsgId,
) -> anyhow::Result<()> {
if viewtype == Viewtype::Webxdc {
if let Some(filename) = filename {
if filename.starts_with("debug_logging")
&& filename.ends_with(".xdc")
&& chat_id.is_self_talk(context).await?
{
set_debug_logging_xdc(context, Some(msg_id)).await?;
}
}
if viewtype == Viewtype::Webxdc
&& let Some(filename) = filename
&& filename.starts_with("debug_logging")
&& filename.ends_with(".xdc")
&& chat_id.is_self_talk(context).await?
{
set_debug_logging_xdc(context, Some(msg_id)).await?;
}
Ok(())
}

View File

@@ -451,6 +451,8 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
let t = TestContext::new_alice().await;
const HOUR: i64 = 60 * 60;
let now = time();
let transport_id = 1;
let uidvalidity = 12345;
for (id, timestamp, ephemeral_timestamp) in &[
(900, now - 2 * HOUR, 0),
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
@@ -470,8 +472,8 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
.await?;
t.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
(&message_id, id),
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
(transport_id, &message_id, id, uidvalidity),
)
.await?;
}

View File

@@ -169,27 +169,28 @@ impl HtmlMsgParser {
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
},
});
if self.html.is_empty()
&& let Ok(decoded_data) = mail.get_body()
{
self.html = decoded_data;
}
} else if mimetype == mime::TEXT_PLAIN
&& self.plain.is_none()
&& let Ok(decoded_data) = mail.get_body()
{
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
},
});
}
Ok(())
}
@@ -213,31 +214,29 @@ impl HtmlMsgParser {
MimeMultipartType::Message => Ok(()),
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail) {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}", re_string, e
),
}
}
if mimetype.type_() == mime::IMAGE
&& let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId)
&& let Ok(cid) = parse_message_id(&cid)
&& let Ok(replacement) = mimepart_to_data_url(mail)
{
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}", re_string, e
),
}
}
Ok(())

View File

@@ -71,6 +71,11 @@ const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)]
pub(crate) struct Imap {
/// ID of the transport configuration in the `transports` table.
///
/// This ID is used to namespace records in the `imap` table.
transport_id: u32,
pub(crate) idle_interrupt_receiver: Receiver<()>,
/// Email address.
@@ -249,19 +254,21 @@ impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
impl Imap {
/// Creates new disconnected IMAP client using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub fn new(
lp: Vec<ConfiguredServerLoginParam>,
password: String,
proxy_config: Option<ProxyConfig>,
addr: &str,
strict_tls: bool,
oauth2: bool,
pub async fn new(
context: &Context,
transport_id: u32,
param: ConfiguredLoginParam,
idle_interrupt_receiver: Receiver<()>,
) -> Self {
) -> Result<Self> {
let lp = param.imap.clone();
let password = param.imap_password.clone();
let proxy_config = ProxyConfig::load(context).await?;
let addr = &param.addr;
let strict_tls = param.strict_tls(proxy_config.is_some());
let oauth2 = param.oauth2;
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
Imap {
Ok(Imap {
transport_id,
idle_interrupt_receiver,
addr: addr.to_string(),
lp,
@@ -277,7 +284,7 @@ impl Imap {
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
resync_request_sender,
resync_request_receiver,
}
})
}
/// Creates new disconnected IMAP client using configured parameters.
@@ -285,20 +292,10 @@ impl Imap {
context: &Context,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
let param = ConfiguredLoginParam::load(context)
let (transport_id, param) = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let proxy_config = ProxyConfig::load(context).await?;
let strict_tls = param.strict_tls(proxy_config.is_some());
let imap = Self::new(
param.imap.clone(),
param.imap_password.clone(),
proxy_config,
&param.addr,
strict_tls,
param.oauth2,
idle_interrupt_receiver,
);
let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
Ok(imap)
}
@@ -412,9 +409,19 @@ impl Imap {
})
.await
.context("Failed to enable IMAP compression")?;
Session::new(compressed_session, capabilities, resync_request_sender)
Session::new(
compressed_session,
capabilities,
resync_request_sender,
self.transport_id,
)
} else {
Session::new(session, capabilities, resync_request_sender)
Session::new(
session,
capabilities,
resync_request_sender,
self.transport_id,
)
};
// Store server ID in the context to display in account info.
@@ -593,8 +600,9 @@ impl Imap {
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<(usize, bool)> {
let uid_validity = get_uidvalidity(context, folder).await?;
let old_uid_next = get_uid_next(context, folder).await?;
let transport_id = self.transport_id;
let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
let old_uid_next = get_uid_next(context, transport_id, folder).await?;
info!(
context,
"fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
@@ -662,12 +670,19 @@ impl Imap {
context
.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(folder, uid, uidvalidity)
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(transport_id, folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(&message_id, &folder, uid, uid_validity, target),
(
self.transport_id,
&message_id,
&folder,
uid,
uid_validity,
target,
),
)
.await?;
@@ -778,7 +793,7 @@ impl Imap {
prefetch_uid_next < mailbox_uid_next
};
if new_uid_next > old_uid_next {
set_uid_next(context, folder, new_uid_next).await?;
set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
}
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
@@ -858,6 +873,7 @@ impl Session {
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
let transport_id = self.transport_id();
if folder_exists {
let mut list = self
.uid_fetch("1:*", RFC724MID_UID)
@@ -890,7 +906,7 @@ impl Session {
msgs.len(),
);
uid_validity = get_uidvalidity(context, folder).await?;
uid_validity = get_uidvalidity(context, transport_id, folder).await?;
} else {
warn!(context, "resync_folder_uids: No folder {folder}.");
uid_validity = 0;
@@ -905,12 +921,12 @@ impl Session {
// This may detect previously undetected moved
// messages, so we update server_folder too.
transaction.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(folder, uid, uidvalidity)
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(transport_id, folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(rfc724_mid, folder, uid, uid_validity, target),
(transport_id, rfc724_mid, folder, uid, uid_validity, target),
)?;
}
Ok(())
@@ -1232,11 +1248,12 @@ impl Session {
return Ok(());
}
let transport_id = self.transport_id();
let mut updated_chat_ids = BTreeSet::new();
let uid_validity = get_uidvalidity(context, folder)
let uid_validity = get_uidvalidity(context, transport_id, folder)
.await
.with_context(|| format!("failed to get UID validity for folder {folder}"))?;
let mut highest_modseq = get_modseq(context, folder)
let mut highest_modseq = get_modseq(context, transport_id, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
let mut list = self
@@ -1259,15 +1276,14 @@ impl Session {
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
if is_seen {
if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
if is_seen
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
.await
.with_context(|| {
format!("failed to update seen status for msg {folder}/{uid}")
})?
{
updated_chat_ids.insert(chat_id);
}
{
updated_chat_ids.insert(chat_id);
}
if let Some(modseq) = fetch.modseq {
@@ -1288,7 +1304,7 @@ impl Session {
self.new_mail = true;
}
set_modseq(context, folder, highest_modseq)
set_modseq(context, transport_id, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
if !updated_chat_ids.is_empty() {
@@ -1321,10 +1337,10 @@ impl Session {
while let Some(msg) = list.try_next().await? {
match get_fetch_headers(&msg) {
Ok(headers) => {
if let Some(from) = mimeparser::get_from(&headers) {
if context.is_self_addr(&from.addr).await? {
result.extend(mimeparser::get_recipients(&headers));
}
if let Some(from) = mimeparser::get_from(&headers)
&& context.is_self_addr(&from.addr).await?
{
result.extend(mimeparser::get_recipients(&headers));
}
}
Err(err) => {
@@ -1557,17 +1573,17 @@ impl Session {
.await?;
let mut got_turn_server = false;
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn" {
if let Some(value) = m.value {
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
}
}
@@ -1852,11 +1868,13 @@ impl Imap {
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter() {
if delimiter_is_default && !d.is_empty() && delimiter != d {
delimiter = d.to_string();
delimiter_is_default = false;
}
if let Some(d) = folder.delimiter()
&& delimiter_is_default
&& !d.is_empty()
&& delimiter != d
{
delimiter = d.to_string();
delimiter_is_default = false;
}
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
@@ -2097,12 +2115,10 @@ async fn needs_move_to_mvbox(
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.is_some()
&& let Some(from) = mimeparser::get_from(headers)
&& context.is_self_addr(&from.addr).await?
{
if let Some(from) = mimeparser::get_from(headers) {
if context.is_self_addr(&from.addr).await? {
return Ok(true);
}
}
return Ok(true);
}
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
@@ -2272,12 +2288,13 @@ pub(crate) async fn prefetch_should_download(
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
// the further process).
if let Some(chat) = prefetch_get_chat(context, headers).await? {
if chat.typ == Chattype::Group && !chat.id.is_special() {
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
}
if let Some(chat) = prefetch_get_chat(context, headers).await?
&& chat.typ == Chattype::Group
&& !chat.id.is_special()
{
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
}
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
@@ -2417,13 +2434,18 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// This function is used to update our uid_next after fetching messages.
pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) -> Result<()> {
pub(crate) async fn set_uid_next(
context: &Context,
transport_id: u32,
folder: &str,
uid_next: u32,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
(folder, uid_next),
"INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
(transport_id, folder, uid_next),
)
.await?;
Ok(())
@@ -2434,57 +2456,69 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
/// This method returns the uid_next from the last time we fetched messages.
/// We can compare this to the current uid_next to find out whether there are new messages
/// and fetch from this value on to get all new messages.
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
.query_get_value(
"SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
(transport_id, folder),
)
.await?
.unwrap_or(0))
}
pub(crate) async fn set_uidvalidity(
context: &Context,
transport_id: u32,
folder: &str,
uidvalidity: u32,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
(folder, uidvalidity),
"INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
(transport_id, folder, uidvalidity),
)
.await?;
Ok(())
}
async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value(
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
(folder,),
"SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
(transport_id, folder),
)
.await?
.unwrap_or(0))
}
pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> Result<()> {
pub(crate) async fn set_modseq(
context: &Context,
transport_id: u32,
folder: &str,
modseq: u64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
(folder, modseq),
"INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
(transport_id, folder, modseq),
)
.await?;
Ok(())
}
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
Ok(context
.sql
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
.query_get_value(
"SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
(transport_id, folder),
)
.await?
.unwrap_or(0))
}
@@ -2524,11 +2558,11 @@ fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
let mut ranges: Vec<UidRange> = vec![];
for &current in uids {
if let Some(last) = ranges.last_mut() {
if last.end + 1 == current {
last.end = current;
continue;
}
if let Some(last) = ranges.last_mut()
&& last.end + 1 == current
{
last.end = current;
continue;
}
ranges.push(UidRange {

View File

@@ -11,17 +11,23 @@ fn test_get_folder_meaning_by_name() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_uid_next_validity() {
let t = TestContext::new_alice().await;
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 0);
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
set_uidvalidity(&t.ctx, 1, "Inbox", 7).await.unwrap();
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 7);
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 0);
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
// For another transport there is still no UIDVALIDITY set.
assert_eq!(get_uidvalidity(&t.ctx, 2, "Inbox").await.unwrap(), 0);
set_uid_next(&t.ctx, 1, "Inbox", 5).await.unwrap();
set_uidvalidity(&t.ctx, 1, "Inbox", 6).await.unwrap();
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 5);
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 6);
assert_eq!(get_uid_next(&t.ctx, 2, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, 2, "Inbox").await.unwrap(), 0);
}
#[test]

View File

@@ -5,6 +5,7 @@ use anyhow::Context as _;
use super::session::Session as ImapSession;
use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::log::warn;
type Result<T> = std::result::Result<T, Error>;
@@ -34,16 +35,16 @@ impl ImapSession {
/// because no EXPUNGE responses are sent, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!(context, "Expunge messages in {folder:?}.");
if let Some(folder) = &self.selected_folder
&& self.selected_folder_needs_expunge
{
info!(context, "Expunge messages in {folder:?}.");
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "Close/expunge succeeded.");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
}
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "Close/expunge succeeded.");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
}
Ok(())
}
@@ -54,10 +55,10 @@ impl ImapSession {
async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
if let Some(selected_folder) = &self.selected_folder
&& folder == selected_folder
{
return Ok(NewlySelected::No);
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
@@ -129,7 +130,7 @@ impl ImapSession {
context: &Context,
folder: &str,
create: bool,
) -> Result<bool> {
) -> anyhow::Result<bool> {
let newly_selected = if create {
self.select_or_create_folder(context, folder)
.await
@@ -146,15 +147,24 @@ impl ImapSession {
},
}
};
let transport_id = self.transport_id();
// Folders should not be selected when transport_id is not assigned yet
// because we cannot save UID validity then.
ensure_and_debug_assert!(
transport_id > 0,
"Cannot select folder when transport ID is unknown"
);
let mailbox = self
.selected_mailbox
.as_mut()
.with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
let old_uid_validity = get_uidvalidity(context, folder)
let old_uid_validity = get_uidvalidity(context, transport_id, folder)
.await
.with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
let old_uid_next = get_uid_next(context, folder)
let old_uid_next = get_uid_next(context, transport_id, folder)
.await
.with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
@@ -205,7 +215,7 @@ impl ImapSession {
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, new_uid_next).await?;
set_uid_next(context, transport_id, folder, new_uid_next).await?;
self.resync_request_sender.try_send(()).ok();
}
@@ -224,21 +234,21 @@ impl ImapSession {
}
// UIDVALIDITY is modified, reset highest seen MODSEQ.
set_modseq(context, folder, 0).await?;
set_modseq(context, transport_id, folder, 0).await?;
// ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = new_uid_next.unwrap_or_default();
set_uid_next(context, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
set_uid_next(context, transport_id, folder, new_uid_next).await?;
set_uidvalidity(context, transport_id, folder, new_uid_validity).await?;
self.new_mail = true;
// Collect garbage entries in `imap` table.
context
.sql
.execute(
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
(&folder, new_uid_validity),
"DELETE FROM imap WHERE transport_id=? AND folder=? AND uidvalidity!=?",
(transport_id, &folder, new_uid_validity),
)
.await?;
@@ -247,12 +257,7 @@ impl ImapSession {
}
info!(
context,
"uid/validity change folder {}: new {}/{} previous {}/{}.",
folder,
new_uid_next,
new_uid_validity,
old_uid_next,
old_uid_validity,
"transport {transport_id}: UID validity for folder {folder} changed from {old_uid_validity}/{old_uid_next} to {new_uid_validity}/{new_uid_next}.",
);
Ok(true)
}

View File

@@ -30,6 +30,8 @@ const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIE
#[derive(Debug)]
pub(crate) struct Session {
transport_id: u32,
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
pub capabilities: Capabilities,
@@ -71,8 +73,10 @@ impl Session {
inner: ImapSession<Box<dyn SessionStream>>,
capabilities: Capabilities,
resync_request_sender: async_channel::Sender<()>,
transport_id: u32,
) -> Self {
Self {
transport_id,
inner,
capabilities,
selected_folder: None,
@@ -84,6 +88,11 @@ impl Session {
}
}
/// Returns ID of the transport for which this session was created.
pub(crate) fn transport_id(&self) -> u32 {
self.transport_id
}
pub fn can_idle(&self) -> bool {
self.capabilities.can_idle
}

View File

@@ -979,11 +979,10 @@ mod tests {
let context1 = &TestContext::new_alice().await;
// `bcc_self` is enabled by default for test contexts. Unset it.
context1.set_config(Config::BccSelf, None).await?;
// Check that the settings are displayed correctly.
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())

View File

@@ -89,6 +89,7 @@ pub mod securejoin;
mod simplify;
mod smtp;
pub mod stock_str;
pub mod storage_usage;
mod sync;
mod timesmearing;
mod token;

View File

@@ -294,7 +294,7 @@ pub async fn send_locations_to_chat(
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
chat::add_info_msg(context, chat_id, &stock_str).await?;
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
@@ -849,7 +849,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
.context("failed to disable location streaming")?;
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
chat::add_info_msg(context, chat_id, &stock_str).await?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}

View File

@@ -638,33 +638,33 @@ impl Message {
pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
if self.viewtype.has_file() {
let file_param = self.param.get_file_path(context)?;
if let Some(path_and_filename) = file_param {
if matches!(
if let Some(path_and_filename) = file_param
&& matches!(
self.viewtype,
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
) && !self.param.exists(Param::Width)
{
let buf = read_file(context, &path_and_filename).await?;
)
&& !self.param.exists(Param::Width)
{
let buf = read_file(context, &path_and_filename).await?;
match get_filemeta(&buf) {
Ok((width, height)) => {
self.param.set_int(Param::Width, width as i32);
self.param.set_int(Param::Height, height as i32);
}
Err(err) => {
self.param.set_int(Param::Width, 0);
self.param.set_int(Param::Height, 0);
warn!(
context,
"Failed to get width and height for {}: {err:#}.",
path_and_filename.display()
);
}
match get_filemeta(&buf) {
Ok((width, height)) => {
self.param.set_int(Param::Width, width as i32);
self.param.set_int(Param::Height, height as i32);
}
Err(err) => {
self.param.set_int(Param::Width, 0);
self.param.set_int(Param::Height, 0);
warn!(
context,
"Failed to get width and height for {}: {err:#}.",
path_and_filename.display()
);
}
}
if !self.id.is_unset() {
self.update_param(context).await?;
}
if !self.id.is_unset() {
self.update_param(context).await?;
}
}
}
@@ -992,14 +992,12 @@ impl Message {
return None;
}
if let Some(filename) = self.get_file(context) {
if let Ok(ref buf) = read_file(context, &filename).await {
if let Ok((typ, headers, _)) = split_armored_data(buf) {
if typ == pgp::armor::BlockType::Message {
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
}
}
}
if let Some(filename) = self.get_file(context)
&& let Ok(ref buf) = read_file(context, &filename).await
&& let Ok((typ, headers, _)) = split_armored_data(buf)
&& typ == pgp::armor::BlockType::Message
{
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
}
None
@@ -1224,26 +1222,25 @@ impl Message {
///
/// `References` header is not taken into account.
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
let msg = Message::load_from_db_optional(context, msg_id).await?;
return Ok(msg);
}
if let Some(in_reply_to) = &self.in_reply_to
&& let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await?
{
let msg = Message::load_from_db_optional(context, msg_id).await?;
return Ok(msg);
}
Ok(None)
}
/// Returns original message ID for message from "Saved Messages".
pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
if !self.original_msg_id.is_special() {
if let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
{
return if msg.chat_id.is_trash() {
Ok(None)
} else {
Ok(Some(msg.id))
};
}
if !self.original_msg_id.is_special()
&& let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
{
return if msg.chat_id.is_trash() {
Ok(None)
} else {
Ok(Some(msg.id))
};
}
Ok(None)
}
@@ -1613,10 +1610,10 @@ pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Resu
.expect("RwLock is poisoned")
.as_ref()
.map(|dl| dl.msg_id);
if let Some(id) = logging_xdc_id {
if id == msg.id {
set_debug_logging_xdc(context, None).await?;
}
if let Some(id) = logging_xdc_id
&& id == msg.id
{
set_debug_logging_xdc(context, None).await?;
}
Ok(())
@@ -1871,6 +1868,33 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
Ok(())
}
/// Checks if the messages with given IDs exist.
///
/// Returns IDs of existing messages.
pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Vec<MsgId>> {
let query_only = true;
let res = context
.sql
.transaction_ex(query_only, |transaction| {
let mut res: Vec<MsgId> = Vec::new();
for id in ids {
if transaction.query_one(
"SELECT COUNT(*) > 0 FROM msgs WHERE id=? AND chat_id!=3",
(id,),
|row| {
let exists: bool = row.get(0)?;
Ok(exists)
},
)? {
res.push(*id);
}
}
Ok(res)
})
.await?;
Ok(res)
}
pub(crate) async fn update_msg_state(
context: &Context,
msg_id: MsgId,

View File

@@ -39,17 +39,16 @@ async fn test_get_width_height() {
let mut has_image = false;
let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
for chatitem in chatitems {
if let ChatItem::Message { msg_id } = chatitem {
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
if msg.get_viewtype() == Viewtype::Image {
has_image = true;
// just check that width/height are inside some reasonable ranges
assert!(msg.get_width() > 100);
assert!(msg.get_height() > 100);
assert!(msg.get_width() < 4000);
assert!(msg.get_height() < 4000);
}
}
if let ChatItem::Message { msg_id } = chatitem
&& let Ok(msg) = Message::load_from_db(&t, msg_id).await
&& msg.get_viewtype() == Viewtype::Image
{
has_image = true;
// just check that width/height are inside some reasonable ranges
assert!(msg.get_width() > 100);
assert!(msg.get_height() > 100);
assert!(msg.get_width() < 4000);
assert!(msg.get_height() < 4000);
}
}
assert!(has_image);
@@ -765,3 +764,27 @@ async fn test_load_unknown_viewtype() -> Result<()> {
assert_eq!(bob_msg.get_viewtype(), Viewtype::Unknown);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_existing_msg_ids() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg1_id = tcm.send_recv(alice, bob, "Hello 1!").await.id;
let msg2_id = tcm.send_recv(alice, bob, "Hello 2!").await.id;
let msg3_id = tcm.send_recv(alice, bob, "Hello 3!").await.id;
let msg4_id = tcm.send_recv(alice, bob, "Hello 4!").await.id;
assert_eq!(
get_existing_msg_ids(bob, &[msg1_id, msg2_id, msg3_id, msg4_id]).await?,
vec![msg1_id, msg2_id, msg3_id, msg4_id]
);
delete_msgs(bob, &[msg1_id, msg3_id]).await?;
assert_eq!(
get_existing_msg_ids(bob, &[msg1_id, msg2_id, msg3_id, msg4_id]).await?,
vec![msg2_id, msg4_id]
);
Ok(())
}

View File

@@ -292,11 +292,10 @@ impl MimeFactory {
// In a broadcast channel, only send member-added/removed messages
// to the affected member:
if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
if fp? != fingerprint {
if let Some(fp) = must_have_only_one_recipient(&msg, &chat)
&& fp? != fingerprint {
continue;
}
}
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
Some(SignedPublicKey::from_slice(public_key_bytes)?)
@@ -347,8 +346,8 @@ impl MimeFactory {
// Row is a tombstone,
// member is not actually part of the group.
if !recipients_contain_addr(&past_members, &addr) {
if let Some(email_to_remove) = email_to_remove {
if email_to_remove == addr {
if let Some(email_to_remove) = email_to_remove
&& email_to_remove == addr {
// This is a "member removed" message,
// we need to notify removed member
// that it was removed.
@@ -365,7 +364,6 @@ impl MimeFactory {
}
}
}
}
if !undisclosed_recipients {
past_members.push((name, addr.clone()));
past_member_timestamps.push(remove_timestamp);
@@ -395,15 +393,14 @@ impl MimeFactory {
"member_fingerprints.len() ({}) < to.len() ({})",
member_fingerprints.len(), to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
if to.len() > 1
&& let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
to.remove(position);
member_timestamps.remove(position);
if is_encrypted {
member_fingerprints.remove(position);
}
}
}
member_timestamps.extend(past_member_timestamps);
if is_encrypted {
@@ -733,36 +730,35 @@ impl MimeFactory {
));
}
if let Loaded::Message { chat, .. } = &self.loaded {
if chat.typ == Chattype::Group {
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await?
{
headers.push((
"Chat-Group-Member-Timestamps",
mail_builder::headers::raw::Raw::new(
self.member_timestamps
.iter()
.map(|ts| ts.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
if let Loaded::Message { chat, .. } = &self.loaded
&& chat.typ == Chattype::Group
{
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? {
headers.push((
"Chat-Group-Member-Timestamps",
mail_builder::headers::raw::Raw::new(
self.member_timestamps
.iter()
.map(|ts| ts.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
if !self.member_fingerprints.is_empty() {
headers.push((
"Chat-Group-Member-Fpr",
mail_builder::headers::raw::Raw::new(
self.member_fingerprints
.iter()
.map(|fp| fp.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
if !self.member_fingerprints.is_empty() {
headers.push((
"Chat-Group-Member-Fpr",
mail_builder::headers::raw::Raw::new(
self.member_fingerprints
.iter()
.map(|fp| fp.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
}
@@ -814,37 +810,34 @@ impl MimeFactory {
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
));
} else if let Loaded::Message { msg, .. } = &self.loaded {
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
));
}
} else if let Loaded::Message { msg, .. } = &self.loaded
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
));
}
}
if let Loaded::Message { msg, chat } = &self.loaded {
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
headers.push((
"Chat-List-ID",
mail_builder::headers::text::Text::new(format!(
"{} <{}>",
chat.name, chat.grpid
))
if let Loaded::Message { msg, chat } = &self.loaded
&& (chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast)
{
headers.push((
"Chat-List-ID",
mail_builder::headers::text::Text::new(format!("{} <{}>", chat.name, chat.grpid))
.into(),
));
));
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup {
if let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET) {
headers.push((
"Chat-Broadcast-Secret",
mail_builder::headers::text::Text::new(secret.to_string()).into(),
));
}
}
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
&& let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET)
{
headers.push((
"Chat-Broadcast-Secret",
mail_builder::headers::text::Text::new(secret.to_string()).into(),
));
}
}
@@ -1043,7 +1036,15 @@ impl MimeFactory {
//
// and the explanation page says
// "The time information deviates too much from the actual time".
let timestamp_offset = rand::random_range(0..1000000);
//
// We also limit the range to 6 days (518400 seconds)
// because with a larger range we got
// error "500 Date header far in the past/future"
// which apparently originates from Symantec Messaging Gateway
// and means the message has a Date that is more
// than 7 days in the past:
// <https://github.com/chatmail/core/issues/7466>
let timestamp_offset = rand::random_range(0..518400);
let protected_timestamp = self.timestamp.saturating_sub(timestamp_offset);
let unprotected_date =
chrono::DateTime::<chrono::Utc>::from_timestamp(protected_timestamp, 0)
@@ -1204,16 +1205,16 @@ impl MimeFactory {
// Set the appropriate Content-Type for the inner message.
for (h, v) in &mut message.headers {
if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "cipher");
}
*ct = ct_new;
break;
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "cipher");
}
*ct = ct_new;
break;
}
}
@@ -1238,7 +1239,7 @@ impl MimeFactory {
// created before we had symmetric encryption,
// we show an error message.
let text = BROADCAST_INCOMPATIBILITY_MSG;
chat::add_info_msg(context, chat.id, text, time()).await?;
chat::add_info_msg(context, chat.id, text).await?;
bail!(text);
}
secret
@@ -1257,10 +1258,10 @@ impl MimeFactory {
// once new core versions are sufficiently deployed.
let anonymous_recipients = false;
if context.get_config_bool(Config::TestHooks).await? {
if let Some(hook) = &*context.pre_encrypt_mime_hook.lock() {
message = hook(context, message);
}
if context.get_config_bool(Config::TestHooks).await?
&& let Some(hook) = &*context.pre_encrypt_mime_hook.lock()
{
message = hook(context, message);
}
let encrypted = if let Some(shared_secret) = shared_secret {
@@ -1348,16 +1349,16 @@ impl MimeFactory {
message
} else {
for (h, v) in &mut message.headers {
if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "clear");
}
*ct = ct_new;
break;
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "clear");
}
*ct = ct_new;
break;
}
}
@@ -1882,10 +1883,10 @@ impl MimeFactory {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await? {
if let Some(part) = self.get_location_kml_part(context).await? {
parts.push(part);
}
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
&& let Some(part) = self.get_location_kml_part(context).await?
{
parts.push(part);
}
// we do not piggyback sync-files to other self-sent-messages

View File

@@ -273,12 +273,11 @@ impl MimeMessage {
&mut chat_disposition_notification_to,
&mail,
);
headers.retain(|k, _| {
!is_hidden(k) || {
headers_removed.insert(k.to_string());
false
}
});
headers_removed.extend(
headers
.extract_if(|k, _v| is_hidden(k))
.map(|(k, _v)| k.to_string()),
);
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -310,13 +309,14 @@ impl MimeMessage {
// Currently we do not sign unencrypted messages by default.
(&mail, mimetype)
};
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
if let Some(part) = part.subparts.first() {
for field in &part.headers {
let key = field.get_key().to_lowercase();
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
headers.insert(key.to_string(), field.get_value());
}
if mimetype.type_() == mime::MULTIPART
&& mimetype.subtype().as_str() == "mixed"
&& let Some(part) = part.subparts.first()
{
for field in &part.headers {
let key = field.get_key().to_lowercase();
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
headers.insert(key.to_string(), field.get_value());
}
}
}
@@ -679,8 +679,9 @@ impl MimeMessage {
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
self.parts.retain(|part| {
part.mimetype.is_none()
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
part.mimetype
.as_ref()
.is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
});
if self.parts.len() == 1 {
@@ -805,22 +806,20 @@ impl MimeMessage {
{
part.typ = Viewtype::Voice;
}
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
}
}
}
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
&& let Some(value) = self.get_header(HeaderDef::ChatContent)
&& value == "sticker"
{
if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
}
part.typ = Viewtype::Sticker;
}
if (part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video)
&& let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
{
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
}
}
@@ -836,38 +835,38 @@ impl MimeMessage {
self.squash_attachment_parts();
}
if !context.get_config_bool(Config::Bot).await? {
if let Some(ref subject) = self.get_subject() {
let mut prepend_subject = true;
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
|| self.has_chat_version()
|| subject.contains("Chat:")
{
prepend_subject = false
}
if !context.get_config_bool(Config::Bot).await?
&& let Some(ref subject) = self.get_subject()
{
let mut prepend_subject = true;
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
|| self.has_chat_version()
|| subject.contains("Chat:")
{
prepend_subject = false
}
}
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.is_mailinglist_message() && !self.has_chat_version() {
prepend_subject = true;
}
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.is_mailinglist_message() && !self.has_chat_version() {
prepend_subject = true;
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
if let Some(part) = part_with_text {
// Message bubbles are small, so we use en dash to save space. In some
// languages there may be em dashes in the message text added by the author,
// they may look stronger than Subject separation, this is a known thing.
// Anyway, classic email support isn't a priority as of 2025.
part.msg = format!("{} {}", subject, part.msg);
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
if let Some(part) = part_with_text {
// Message bubbles are small, so we use en dash to save space. In some
// languages there may be em dashes in the message text added by the author,
// they may look stronger than Subject separation, this is a known thing.
// Anyway, classic email support isn't a priority as of 2025.
part.msg = format!("{} {}", subject, part.msg);
}
}
}
@@ -881,21 +880,22 @@ impl MimeMessage {
self.parse_attachments();
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to) = self.chat_disposition_notification_to {
// Check that the message is not outgoing.
let from = &self.from.addr;
if !context.is_self_addr(from).await? {
if from.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
);
if !self.decrypting_failed
&& !self.parts.is_empty()
&& let Some(ref dn_to) = self.chat_disposition_notification_to
{
// Check that the message is not outgoing.
let from = &self.from.addr;
if !context.is_self_addr(from).await? {
if from.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
);
}
}
}
@@ -910,10 +910,11 @@ impl MimeMessage {
..Default::default()
};
if let Some(ref subject) = self.get_subject() {
if !self.has_chat_version() && self.webxdc_status_update.is_none() {
part.msg = subject.to_string();
}
if let Some(ref subject) = self.get_subject()
&& !self.has_chat_version()
&& self.webxdc_status_update.is_none()
{
part.msg = subject.to_string();
}
self.do_add_single_part(part);
@@ -952,15 +953,15 @@ impl MimeMessage {
let mut i = 0;
while let Some(part) = self.parts.get_mut(i) {
if let Some(part_filename) = &part.org_filename {
if part_filename == &header_value {
if let Some(blob) = part.param.get(Param::File) {
let res = Some(AvatarAction::Change(blob.to_string()));
self.parts.remove(i);
return Ok(res);
}
break;
if let Some(part_filename) = &part.org_filename
&& part_filename == &header_value
{
if let Some(blob) = part.param.get(Param::File) {
let res = Some(AvatarAction::Change(blob.to_string()));
self.parts.remove(i);
return Ok(res);
}
break;
}
i += 1;
}
@@ -1604,13 +1605,13 @@ impl MimeMessage {
} 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" {
// 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);
}
if let Some(precedence) = self.get_header(HeaderDef::Precedence)
&& (precedence == "list" || precedence == "bulk")
{
// 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);
}
}
None
@@ -1651,10 +1652,10 @@ impl MimeMessage {
remove_header(headers, "autocrypt-gossip", removed);
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
if secure_join == "vc-request" || secure_join == "vg-request" {
headers.insert("secure-join".to_string(), secure_join);
}
if let Some(secure_join) = remove_header(headers, "secure-join", removed)
&& (secure_join == "vc-request" || secure_join == "vg-request")
{
headers.insert("secure-join".to_string(), secure_join);
}
}
@@ -1679,12 +1680,11 @@ impl MimeMessage {
// See <https://www.rfc-editor.org/rfc/rfc9788.html>.
let has_header_protection = part.ctype.params.contains_key("hp");
headers.retain(|k, _| {
!(has_header_protection || is_protected(k)) || {
headers_removed.insert(k.to_string());
false
}
});
headers_removed.extend(
headers
.extract_if(|k, _v| has_header_protection || is_protected(k))
.map(|(k, _v)| k.to_string()),
);
for field in fields {
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
@@ -1889,12 +1889,11 @@ impl MimeMessage {
.iter()
.filter(|p| p.typ == Viewtype::Text)
.count();
if text_part_cnt == 2 {
if let Some(last_part) = self.parts.last() {
if last_part.typ == Viewtype::Text {
self.parts.pop();
}
}
if text_part_cnt == 2
&& let Some(last_part) = self.parts.last()
&& last_part.typ == Viewtype::Text
{
self.parts.pop();
}
}
}
@@ -1947,15 +1946,15 @@ impl MimeMessage {
}
}
if let Some(delivery_report) = &self.delivery_report {
if delivery_report.failure {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(err) = handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle NDN: {err:#}.");
}
if let Some(delivery_report) = &self.delivery_report
&& delivery_report.failure
{
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(err) = handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle NDN: {err:#}.");
}
}
}
@@ -2303,14 +2302,14 @@ fn get_attachment_filename(
// `Content-Disposition: ... filename=...`
let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
if desired_filename.is_none() {
if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
// be graceful and just use the original name.
// some MUA, including Delta Chat up to core1.50,
// use `filename*` mistakenly for simple encoded-words without following rfc2231
warn!(context, "apostrophed encoding invalid: {}", name);
desired_filename = Some(name);
}
if desired_filename.is_none()
&& let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
{
// be graceful and just use the original name.
// some MUA, including Delta Chat up to core1.50,
// use `filename*` mistakenly for simple encoded-words without following rfc2231
warn!(context, "apostrophed encoding invalid: {}", name);
desired_filename = Some(name);
}
// if no filename is set, try `Content-Disposition: ... name=...`
@@ -2383,24 +2382,23 @@ fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<Si
.iter()
.rev()
.find(|h| h.get_key().to_lowercase() == header)
&& let Ok(addrs) = mailparse::addrparse_header(header)
{
if let Ok(addrs) = mailparse::addrparse_header(header) {
for addr in addrs.iter() {
match addr {
mailparse::MailAddr::Single(info) => {
for addr in addrs.iter() {
match addr {
mailparse::MailAddr::Single(info) => {
result.push(SingleInfo {
addr: addr_normalize(&info.addr).to_lowercase(),
display_name: info.display_name.clone(),
});
}
mailparse::MailAddr::Group(infos) => {
for info in &infos.addrs {
result.push(SingleInfo {
addr: addr_normalize(&info.addr).to_lowercase(),
display_name: info.display_name.clone(),
});
}
mailparse::MailAddr::Group(infos) => {
for info in &infos.addrs {
result.push(SingleInfo {
addr: addr_normalize(&info.addr).to_lowercase(),
display_name: info.display_name.clone(),
});
}
}
}
}
}

View File

@@ -1774,10 +1774,10 @@ async fn test_hp_legacy_display() -> Result<()> {
alice.set_config_bool(Config::TestHooks, true).await?;
*alice.pre_encrypt_mime_hook.lock() = Some(|_, mut mime| {
for (h, v) in &mut mime.headers {
if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
*ct = ct.clone().attribute("hp-legacy-display", "1");
}
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
*ct = ct.clone().attribute("hp-legacy-display", "1");
}
}
mime

View File

@@ -229,64 +229,6 @@ pub(crate) async fn update_connect_timestamp(
/// Preloaded DNS results that can be used in case of DNS server failures.
static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new(|| {
HashMap::from([
(
"mail.sangham.net",
vec![
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
],
),
(
"nine.testrun.org",
vec![
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
IpAddr::V4(Ipv4Addr::new(49, 12, 116, 128)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
],
),
(
"disroot.org",
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
),
(
"imap.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
],
),
(
"smtp.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
],
),
(
"mail.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
],
),
(
"smtp.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
],
),
(
"daleth.cafe",
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
),
(
"imap.163.com",
vec![IpAddr::V4(Ipv4Addr::new(111, 124, 203, 45))],
@@ -295,6 +237,21 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"smtp.163.com",
vec![IpAddr::V4(Ipv4Addr::new(103, 129, 252, 45))],
),
(
"newyear.aktivix.org",
vec![IpAddr::V4(Ipv4Addr::new(209, 51, 180, 245))],
),
(
"smtp.aliyun.com",
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
),
(
"imap.aliyun.com",
vec![
IpAddr::V4(Ipv4Addr::new(59, 82, 43, 123)),
IpAddr::V4(Ipv4Addr::new(59, 82, 9, 176)),
],
),
(
"imap.aol.com",
vec![
@@ -302,17 +259,67 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(87, 248, 98, 69)),
],
),
(
"imap.arcor.de",
vec![
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 210)),
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
],
),
(
"smtp.aol.com",
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 31))],
),
(
"mail.arcor.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
vec![
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 206)),
],
),
(
"imap.arcor.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
"mail.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
IpAddr::V4(Ipv4Addr::new(185, 218, 207, 228)),
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
],
),
(
"smtp.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
],
),
(
"imaps.bluewin.ch",
vec![
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
],
),
(
"smtpauths.bluewin.ch",
vec![
IpAddr::V4(Ipv4Addr::new(16, 62, 176, 232)),
IpAddr::V4(Ipv4Addr::new(16, 62, 15, 25)),
IpAddr::V4(Ipv4Addr::new(16, 63, 183, 216)),
],
),
(
"mail.buzon.uy",
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
),
(
"daleth.cafe",
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
),
(
"disroot.org",
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
),
(
"imap.fastmail.com",
@@ -328,6 +335,39 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 60)),
],
),
(
"imap.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
],
),
(
"mail.ecloud.global",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
),
(
"mail.ende.in.net",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
),
(
"smtp.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
],
),
(
"mail.gmx.net",
vec![
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
],
),
(
"imap.gmx.net",
vec![
@@ -335,6 +375,13 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 186)),
],
),
(
"mail.sangham.net",
vec![
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
],
),
(
"imap.mail.de",
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 16))],
@@ -349,7 +396,7 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.naver.com",
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 153))],
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 233, 34))],
),
(
"imap.ouvaton.coop",
@@ -359,6 +406,57 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"imap.purelymail.com",
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
),
(
"mail.systemausfall.org",
vec![
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
],
),
(
"mail.systemli.org",
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
),
("testrun.org", vec![IpAddr::V4(Ipv4Addr::new(5, 1, 76, 52))]),
(
"nine.testrun.org",
vec![
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
IpAddr::V4(Ipv4Addr::new(216, 144, 228, 100)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
IpAddr::V6(Ipv6Addr::new(
0x2001, 0x41d0, 0x701, 0x1100, 0, 0, 0, 0x8ab1,
)),
],
),
(
"secureimap.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
],
),
(
"securesmtp.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
],
),
(
"mail.riseup.net",
vec![
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 171)),
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 170)),
],
),
(
"pimap.schulon.org",
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
),
(
"imap.tiscali.it",
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 10))],
@@ -367,6 +465,14 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"smtp.tiscali.it",
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 13))],
),
(
"imap.ukr.net",
vec![IpAddr::V4(Ipv4Addr::new(212, 42, 75, 240))],
),
(
"smtp.ukr.net",
vec![IpAddr::V4(Ipv4Addr::new(212, 42, 75, 250))],
),
(
"imap.web.de",
vec![
@@ -380,33 +486,9 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.zoho.eu",
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25))],
),
(
"imaps.bluewin.ch",
vec![
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
],
),
(
"mail.buzon.uy",
vec![IpAddr::V4(Ipv4Addr::new(185, 101, 93, 79))],
),
(
"mail.ecloud.global",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
),
(
"mail.ende.in.net",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
),
(
"mail.gmx.net",
vec![
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25)),
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 206)),
],
),
(
@@ -424,24 +506,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"mail.nubo.coop",
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
),
(
"mail.riseup.net",
vec![
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
],
),
(
"mail.systemausfall.org",
vec![
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
],
),
(
"mail.systemli.org",
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
),
(
"mehl.cloud",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
@@ -449,20 +513,14 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
(
"mx.freenet.de",
vec![
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 210)),
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 211)),
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 212)),
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 213)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 36)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 34)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 35)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 39)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 37)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 38)),
],
),
(
"newyear.aktivix.org",
vec![IpAddr::V4(Ipv4Addr::new(162, 247, 75, 192))],
),
(
"pimap.schulon.org",
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
),
(
"posteo.de",
vec![
@@ -474,26 +532,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"psmtp.schulon.org",
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
),
(
"secureimap.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
],
),
(
"securesmtp.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
],
),
(
"smtp.aliyun.com",
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
),
(
"smtp.mail.de",
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 21))],
@@ -501,8 +539,8 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
(
"smtp.mail.ru",
vec![
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
IpAddr::V4(Ipv4Addr::new(94, 100, 180, 160)),
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
],
),
(
@@ -520,6 +558,20 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"imap.mailo.com",
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
),
(
"imap.migadu.com",
vec![
IpAddr::V4(Ipv4Addr::new(51, 210, 3, 23)),
IpAddr::V4(Ipv4Addr::new(51, 210, 3, 20)),
],
),
(
"smtp.migadu.com",
vec![
IpAddr::V4(Ipv4Addr::new(37, 59, 57, 117)),
IpAddr::V4(Ipv4Addr::new(51, 255, 82, 75)),
],
),
(
"smtp.mailo.com",
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
@@ -538,30 +590,48 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.qq.com",
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
vec![
IpAddr::V4(Ipv4Addr::new(43, 163, 178, 76)),
IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54)),
],
),
(
"smtp.qq.com",
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
vec![
IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54)),
IpAddr::V4(Ipv4Addr::new(43, 163, 178, 76)),
],
),
(
"imap.rambler.ru",
vec![
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 169)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 171)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 168)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 169)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 170)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 171)),
],
),
(
"smtp.rambler.ru",
vec![
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 165)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 167)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 164)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 165)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 167)),
],
),
(
"stinpriza.net",
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
),
(
"webbox222.server-home.org",
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
),
(
"undernet.uy",
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
),
(
"imap.vivaldi.net",
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 15))],
@@ -572,17 +642,23 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.vodafonemail.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
vec![
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 210)),
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
],
),
(
"smtp.vodafonemail.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
vec![
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 206)),
],
),
(
"smtp.web.de",
vec![
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 124)),
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
],
),
(
@@ -599,23 +675,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"smtp.zoho.eu",
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164))],
),
(
"smtpauths.bluewin.ch",
vec![IpAddr::V4(Ipv4Addr::new(195, 186, 120, 54))],
),
(
"stinpriza.net",
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
),
(
"undernet.uy",
vec![IpAddr::V4(Ipv4Addr::new(167, 62, 254, 153))],
),
(
"webbox222.server-home.org",
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
vec![
IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164)),
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
],
),
])
});

View File

@@ -139,10 +139,10 @@ pub(crate) async fn get_oauth2_access_token(
value = &redirect_uri;
} else if value == "$CODE" {
value = code;
} else if value == "$REFRESH_TOKEN" {
if let Some(refresh_token) = refresh_token.as_ref() {
value = refresh_token;
}
} else if value == "$REFRESH_TOKEN"
&& let Some(refresh_token) = refresh_token.as_ref()
{
value = refresh_token;
}
post_param.insert(key, value);
@@ -261,14 +261,12 @@ impl Oauth2 {
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
&& let Some(oauth2_authorizer) = provider::get_provider_info(domain)
.and_then(|provider| provider.oauth2_authorizer.as_ref())
{
return Some(match oauth2_authorizer {
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
});
}
{
return Some(match oauth2_authorizer {
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
});
}
None
}

View File

@@ -1,6 +1,6 @@
//! Internet Message Format reception pipeline.
use std::collections::{BTreeMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::iter;
use std::sync::LazyLock;
@@ -884,53 +884,35 @@ pub(crate) async fn receive_imf_inner(
}
}
if let Some(avatar_action) = &mime_parser.user_avatar {
if from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(
from_id,
Param::AvatarTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = contact::set_profile_image(
context,
from_id,
avatar_action,
mime_parser.was_encrypted(),
)
.await
{
warn!(context, "receive_imf cannot update profile image: {err:#}.");
};
}
}
if let Some(avatar_action) = &mime_parser.user_avatar
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
.await?
&& let Err(err) =
contact::set_profile_image(context, from_id, avatar_action, mime_parser.was_encrypted())
.await
{
warn!(context, "receive_imf cannot update profile image: {err:#}.");
};
// Ignore footers from mailinglists as they are often created or modified by the mailinglist software.
if let Some(footer) = &mime_parser.footer {
if !mime_parser.is_mailinglist_message()
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(
from_id,
Param::StatusTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = contact::set_status(
context,
from_id,
footer.to_string(),
mime_parser.was_encrypted(),
mime_parser.has_chat_version(),
)
.await
{
warn!(context, "Cannot update contact status: {err:#}.");
}
}
if let Some(footer) = &mime_parser.footer
&& !mime_parser.is_mailinglist_message()
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
.await?
&& let Err(err) = contact::set_status(
context,
from_id,
footer.to_string(),
mime_parser.was_encrypted(),
mime_parser.has_chat_version(),
)
.await
{
warn!(context, "Cannot update contact status: {err:#}.");
}
// Get user-configured server deletion
@@ -1205,19 +1187,19 @@ async fn decide_chat_assignment(
let mut num_recipients = 0;
let mut has_self_addr = false;
for recipient in &mime_parser.recipients {
has_self_addr |= context.is_self_addr(&recipient.addr).await?;
if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
continue;
}
if context.is_self_addr(&recipient.addr).await? {
has_self_addr = true;
}
num_recipients += 1;
}
if from_id != ContactId::SELF && !has_self_addr {
num_recipients += 1;
}
let can_be_11_chat = num_recipients <= 1
&& (from_id != ContactId::SELF
|| !(mime_parser.recipients.is_empty() || has_self_addr)
|| mime_parser.was_encrypted());
let chat_assignment = if should_trash {
ChatAssignment::Trash
@@ -1261,14 +1243,14 @@ async fn decide_chat_assignment(
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
ChatAssignment::AdHocGroup
} else if num_recipients <= 1 {
} else if can_be_11_chat {
ChatAssignment::OneOneChat
} else {
ChatAssignment::AdHocGroup
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
ChatAssignment::AdHocGroup
} else if num_recipients <= 1 {
} else if can_be_11_chat {
ChatAssignment::OneOneChat
} else {
ChatAssignment::AdHocGroup
@@ -1341,8 +1323,8 @@ async fn do_chat_assignment(
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation || test_normal_chat.is_some() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
} else if (allow_creation || test_normal_chat.is_some())
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
@@ -1353,16 +1335,15 @@ async fn do_chat_assignment(
grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
}
ChatAssignment::MailingListOrBroadcast => {
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
&& let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
create_or_lookup_mailinglist_or_broadcast(
context,
allow_creation,
@@ -1372,13 +1353,12 @@ async fn do_chat_assignment(
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = new_chat_created;
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = new_chat_created;
apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
}
apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
}
}
ChatAssignment::ExistingChat {
@@ -1413,11 +1393,10 @@ async fn do_chat_assignment(
if chat_id_blocked != Blocked::Not
&& create_blocked != Blocked::Yes
&& !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
&& let Some(chat_id) = chat_id
{
if let Some(chat_id) = chat_id {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if chat_id.is_none() {
@@ -1441,21 +1420,20 @@ async fn do_chat_assignment(
chat_created = true;
}
if let Some(chat_id) = chat_id {
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
}
if create_blocked == Blocked::Request && parent_message.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
.await?;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
}
if let Some(chat_id) = chat_id
&& chat_id_blocked != Blocked::Not
{
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
}
if create_blocked == Blocked::Request && parent_message.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
}
}
}
@@ -1476,8 +1454,8 @@ async fn do_chat_assignment(
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
} else if allow_creation
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
@@ -1488,11 +1466,10 @@ async fn do_chat_assignment(
grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
}
ChatAssignment::ExistingChat {
@@ -1574,11 +1551,12 @@ async fn do_chat_assignment(
chat_created = true;
}
}
if chat_id.is_none() && mime_parser.has_chat_version() {
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
if chat_id.is_none()
&& mime_parser.has_chat_version()
&& let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
@@ -1598,11 +1576,11 @@ async fn do_chat_assignment(
}
// automatically unblock chat when the user sends a message
if chat_id_blocked != Blocked::Not {
if let Some(chat_id) = chat_id {
chat_id.unblock_ex(context, Nosync).await?;
chat_id_blocked = Blocked::Not;
}
if chat_id_blocked != Blocked::Not
&& let Some(chat_id) = chat_id
{
chat_id.unblock_ex(context, Nosync).await?;
chat_id_blocked = Blocked::Not;
}
}
let chat_id = chat_id.unwrap_or_else(|| {
@@ -1640,11 +1618,9 @@ async fn add_parts(
// if contact renaming is prevented (for mailinglists and bots),
// we use name from From:-header as override name
if prevent_rename {
if let Some(name) = &mime_parser.from.display_name {
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
}
if prevent_rename && let Some(name) = &mime_parser.from.display_name {
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
}
}
@@ -1801,11 +1777,16 @@ async fn add_parts(
"Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
);
if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
chat::add_info_msg(
chat::add_info_msg_with_cmd(
context,
chat_id,
&stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
sort_timestamp,
SystemMessage::Unknown,
Some(sort_timestamp),
mime_parser.timestamp_sent,
None,
None,
None,
)
.await?;
}
@@ -1913,8 +1894,8 @@ async fn add_parts(
chat_id,
&group_changes_msg,
cmd,
sort_timestamp,
None,
Some(sort_timestamp),
mime_parser.timestamp_sent,
None,
None,
added_removed_id,
@@ -2270,36 +2251,36 @@ async fn handle_edit_delete(
"Edit message: rfc724_mid {rfc724_mid:?} not found."
);
}
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
if let Some(part) = mime_parser.parts.first() {
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
// deletion requests, so there's no need to support them.
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
let mut modified_chat_ids = HashSet::new();
let mut msg_ids = Vec::new();
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
&& let Some(part) = mime_parser.parts.first()
{
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
// deletion requests, so there's no need to support them.
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
let mut modified_chat_ids = HashSet::new();
let mut msg_ids = Vec::new();
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
for rfc724_mid in rfc724_mid_vec {
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
if msg.from_id == from_id {
message::delete_msg_locally(context, &msg).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
warn!(context, "Delete message: Bad sender.");
}
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
for rfc724_mid in rfc724_mid_vec {
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
if msg.from_id == from_id {
message::delete_msg_locally(context, &msg).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
warn!(context, "Delete message: Database entry does not exist.");
warn!(context, "Delete message: Bad sender.");
}
} else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
warn!(context, "Delete message: Database entry does not exist.");
}
} else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
}
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
} else {
warn!(context, "Delete message: Not encrypted.");
}
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
} else {
warn!(context, "Delete message: Not encrypted.");
}
}
Ok(())
@@ -2356,33 +2337,32 @@ async fn save_locations(
let mut send_event = false;
if let Some(message_kml) = &mime_parser.message_kml {
if let Some(newest_location_id) =
if let Some(message_kml) = &mime_parser.message_kml
&& let Some(newest_location_id) =
location::save(context, chat_id, from_id, &message_kml.locations, true).await?
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
send_event = true;
}
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
send_event = true;
}
if let Some(location_kml) = &mime_parser.location_kml {
if let Some(addr) = &location_kml.addr {
let contact = Contact::get_by_id(context, from_id).await?;
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if location::save(context, chat_id, from_id, &location_kml.locations, false)
.await?
.is_some()
{
send_event = true;
}
} else {
warn!(
context,
"Address in location.kml {:?} is not the same as the sender address {:?}.",
addr,
contact.get_addr()
);
if let Some(location_kml) = &mime_parser.location_kml
&& let Some(addr) = &location_kml.addr
{
let contact = Contact::get_by_id(context, from_id).await?;
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if location::save(context, chat_id, from_id, &location_kml.locations, false)
.await?
.is_some()
{
send_event = true;
}
} else {
warn!(
context,
"Address in location.kml {:?} is not the same as the sender address {:?}.",
addr,
contact.get_addr()
);
}
}
if send_event {
@@ -2491,11 +2471,8 @@ async fn lookup_or_create_adhoc_group(
.unwrap_or_else(|| "👥📧".to_string())
});
let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
contact_ids.extend(&to_ids);
if !contact_ids.contains(&from_id) {
contact_ids.push(from_id);
}
let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
contact_ids.insert(from_id);
let trans_fn = |t: &mut rusqlite::Transaction| {
t.pragma_update(None, "query_only", "0")?;
t.execute(
@@ -2734,13 +2711,13 @@ async fn update_chats_contacts_timestamps(
to_ids.iter(),
chat_group_member_timestamps.iter().take(to_ids.len()),
) {
if let Some(contact_id) = contact_id {
if Some(*contact_id) != ignored_id {
// It could be that member was already added,
// but updated addition timestamp
// is also a modification worth notifying about.
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
}
if let Some(contact_id) = contact_id
&& Some(*contact_id) != ignored_id
{
// It could be that member was already added,
// but updated addition timestamp
// is also a modification worth notifying about.
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
}
}
@@ -3003,13 +2980,14 @@ async fn apply_group_changes(
.copied()
.collect();
if let Some(added_id) = added_id {
if !added_ids.remove(&added_id) && added_id != ContactId::SELF {
// No-op "Member added" message. An exception is self-addition messages because they at
// least must be shown when a chat is created on our side.
info!(context, "No-op 'Member added' message (TRASH)");
better_msg = Some(String::new());
}
if let Some(added_id) = added_id
&& !added_ids.remove(&added_id)
&& added_id != ContactId::SELF
{
// No-op "Member added" message. An exception is self-addition messages because they at
// least must be shown when a chat is created on our side.
info!(context, "No-op 'Member added' message (TRASH)");
better_msg = Some(String::new());
}
if let Some(removed_id) = removed_id {
removed_ids.remove(&removed_id);
@@ -3064,59 +3042,52 @@ async fn apply_chat_name_and_avatar_changes(
Some(_) => Some(chat.name.as_str()),
None => None,
})
{
if let Some(grpname) = mime_parser
&& let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200)
{
let grpname = &sanitize_single_line(grpname);
{
let grpname = &sanitize_single_line(grpname);
let chat_group_name_timestamp =
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
&& chat
.id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
.await?
&& grpname != &chat.name
{
info!(context, "Updating grpname for chat {}.", chat.id);
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
.await?;
*send_event_chat_modified = true;
}
if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
);
}
let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
&& chat
.id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
.await?
&& grpname != &chat.name
{
info!(context, "Updating grpname for chat {}.", chat.id);
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
.await?;
*send_event_chat_modified = true;
}
if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
let old_name = &sanitize_single_line(old_name);
better_msg
.get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
}
// ========== Apply chat avatar changes ==========
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
});
}
}
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
&& value == "group-avatar-changed"
&& let Some(avatar_action) = &mime_parser.group_avatar
{
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
});
}
if let Some(avatar_action) = &mime_parser.group_avatar {
@@ -3297,10 +3268,10 @@ fn compute_mailinglist_name(
// 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.
if listid.ends_with(".list-id.mcsv.net") {
if let Some(display_name) = &mime_parser.from.display_name {
name.clone_from(display_name);
}
if listid.ends_with(".list-id.mcsv.net")
&& let Some(display_name) = &mime_parser.from.display_name
{
name.clone_from(display_name);
}
// additional names in square brackets in the subject are preferred
@@ -3325,10 +3296,9 @@ fn compute_mailinglist_name(
|| mime_parser.from.addr.starts_with("notifications@")
|| mime_parser.from.addr.starts_with("newsletter@")
|| listid.ends_with(".xt.local"))
&& let Some(display_name) = &mime_parser.from.display_name
{
if let Some(display_name) = &mime_parser.from.display_name {
name.clone_from(display_name);
}
name.clone_from(display_name);
}
// as a last resort, use the ListId as the name
@@ -3476,15 +3446,15 @@ async fn apply_out_broadcast_changes(
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
info!(context, "Broadcast leave message (TRASH)");
better_msg = Some("".to_string());
} else if from_id == ContactId::SELF {
if let Some(removed_id) = removed_id {
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
.await?;
} else if from_id == ContactId::SELF
&& let Some(removed_id) = removed_id
{
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
.await?;
better_msg.get_or_insert(
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
);
}
better_msg.get_or_insert(
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
);
}
}
@@ -3508,14 +3478,14 @@ async fn apply_in_broadcast_changes(
) -> Result<GroupChangesInfo> {
ensure!(chat.typ == Chattype::InBroadcast);
if let Some(part) = mime_parser.parts.first() {
if let Some(error) = &part.error {
warn!(
context,
"Not applying broadcast changes from message with error: {error}"
);
return Ok(GroupChangesInfo::default());
}
if let Some(part) = mime_parser.parts.first()
&& let Some(error) = &part.error
{
warn!(
context,
"Not applying broadcast changes from message with error: {error}"
);
return Ok(GroupChangesInfo::default());
}
let mut send_event_chat_modified = false;
@@ -3531,21 +3501,21 @@ async fn apply_in_broadcast_changes(
)
.await?;
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
if context.is_self_addr(added_addr).await? {
let msg = if chat.is_self_in_chat(context).await? {
// Self is already in the chat.
// Probably Alice has two devices and her second device added us again;
// just hide the message.
info!(context, "No-op broadcast 'Member added' message (TRASH)");
"".to_string()
} else {
stock_str::msg_you_joined_broadcast(context).await
};
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
&& context.is_self_addr(added_addr).await?
{
let msg = if chat.is_self_in_chat(context).await? {
// Self is already in the chat.
// Probably Alice has two devices and her second device added us again;
// just hide the message.
info!(context, "No-op broadcast 'Member added' message (TRASH)");
"".to_string()
} else {
stock_str::msg_you_joined_broadcast(context).await
};
better_msg.get_or_insert(msg);
send_event_chat_modified = true;
}
better_msg.get_or_insert(msg);
send_event_chat_modified = true;
}
if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
@@ -3628,13 +3598,6 @@ async fn create_adhoc_group(
);
return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
}
if member_ids.len() < 2 {
info!(
context,
"Not creating ad hoc group with less than 2 members."
);
return Ok(None);
}
let new_chat_id: ChatId = ChatId::create_multiuser_record(
context,
@@ -3756,12 +3719,11 @@ async fn get_previous_message(
context: &Context,
mime_parser: &MimeMessage,
) -> Result<Option<Message>> {
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
if let Some(rfc724mid) = parse_message_ids(field).last() {
if let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await? {
return Message::load_from_db_optional(context, msg_id).await;
}
}
if let Some(field) = mime_parser.get_header(HeaderDef::References)
&& let Some(rfc724mid) = parse_message_ids(field).last()
&& let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
{
return Message::load_from_db_optional(context, msg_id).await;
}
Ok(None)
}

View File

@@ -1964,28 +1964,53 @@ Message content",
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
}
/// Tests that message with hidden recipients is assigned to Saved Messages chat.
/// Tests that an outgoing self-sent unencrypted message doesn't go to the self-chat, but to a
/// proper unencrypted chat instead.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hidden_recipients_self_chat() {
let t = TestContext::new_alice().await;
async fn test_unencrypted_doesnt_goto_self_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let mut chat_id = None;
receive_imf(
&t,
b"Subject: s
for (i, to) in [
"<alice@example.org>",
"<alice@example.org>",
"alice@example.org, alice@example.org",
"hidden-recipients:;",
]
.iter()
.enumerate()
{
receive_imf(
t,
format!(
"Subject: s
Chat-Version: 1.0
Message-ID: <foobar@localhost>
To: hidden-recipients:;
Message-ID: <foobar{i}@localhost>
To: {to}
From: <alice@example.org>
Message content",
false,
)
.await
.unwrap();
Your server is hacked. Have a nice day!"
)
.as_bytes(),
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.chat_id, t.get_self_chat().await.id);
assert_eq!(msg.to_id, ContactId::SELF);
let msg = t.get_last_msg().await;
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
assert_eq!(msg.from_id, ContactId::SELF);
assert_eq!(msg.to_id, ContactId::SELF);
if let Some(chat_id) = chat_id {
assert_eq!(msg.chat_id, chat_id);
} else {
chat_id = Some(msg.chat_id);
let chat = Chat::load_from_db(t, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Group);
assert!(!chat.is_encrypted(t).await?);
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -4986,7 +5011,7 @@ async fn test_make_n_send_vcard() -> Result<()> {
Ok(())
}
/// Tests that group is not created if the message
/// Tests that an ad-hoc group is created if the message
/// has no recipients even if it has unencrypted Chat-Group-ID.
///
/// Chat-Group-ID in unencrypted messages should be ignored.
@@ -5005,8 +5030,12 @@ Hello!"
.as_bytes();
let received = receive_imf(t, raw, false).await?.unwrap();
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
assert_eq!(msg.from_id, ContactId::SELF);
assert_eq!(msg.to_id, ContactId::SELF);
let chat = Chat::load_from_db(t, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(chat.typ, Chattype::Group);
assert!(!chat.is_encrypted(t).await?);
assert!(chat.grpid.is_empty());
// Check that the weird group name is sanitzied correctly:
let mail = mailparse::parse_mail(raw).unwrap();
@@ -5017,7 +5046,7 @@ Hello!"
.get_value_raw(),
"Group\n name\u{202B}".as_bytes()
);
assert_eq!(chat.name, "Saved messages");
assert_eq!(chat.name, "Group name");
Ok(())
}
@@ -5154,6 +5183,58 @@ async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
let mut tcm = TestContextManager::new();
let a0 = &tcm.alice().await;
let a1 = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = chat::create_group(bob, "Group").await?;
bob.set_chat_protected(bob_chat_id).await;
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
tcm.exec_securejoin_qr(a0, bob, &qr).await;
let qr = get_securejoin_qr(bob, None).await?;
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
// Bob verifies Fiona for Alice#0.
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(bob, bob_chat_id, bob_fiona_id).await?;
let sent_msg = bob.pop_sent_msg().await;
a0.recv_msg(&sent_msg).await;
fiona.recv_msg(&sent_msg).await;
let a0_bob = a0.add_or_lookup_contact(bob).await;
let a0_fiona = a0.add_or_lookup_contact(fiona).await;
assert_eq!(a0_fiona.get_verifier_id(a0).await?, Some(Some(a0_bob.id)));
let chat_id = a0.create_group_with_members("", &[fiona]).await;
a0.set_chat_protected(chat_id).await;
a1.recv_msg(&a0.send_text(chat_id, "Hi").await).await;
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
assert_eq!(a1_fiona.get_verifier_id(a1).await?, Some(None));
let some_time_to_regossip = Duration::from_secs(20 * 24 * 3600);
SystemTime::shift(some_time_to_regossip);
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
fiona.set_chat_protected(fiona_chat_id).await;
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
.await;
let a1_bob = a1.add_or_lookup_contact(bob).await;
// There was a bug that Bob is verified by Fiona on Alice's other device.
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(None));
SystemTime::shift(some_time_to_regossip);
tcm.execute_securejoin(a1, fiona).await;
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
.await;
// But now Bob's verifier id must be updated because Fiona is verified by a known verifier
// (moreover, directly), so Alice has reverse verification chains on her devices.
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(Some(a1_fiona.id)));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sanitize_filename_in_received() -> Result<()> {
let alice = &TestContext::new_alice().await;

View File

@@ -1,5 +1,4 @@
use std::cmp;
use std::iter::{self, once};
use std::num::NonZeroUsize;
use anyhow::{Context as _, Error, Result, bail};
@@ -26,6 +25,7 @@ use crate::smtp::{Smtp, send_smtp_messages};
use crate::sql;
use crate::stats::maybe_send_stats;
use crate::tools::{self, duration_to_str, maybe_add_time_based_warnings, time, time_elapsed};
use crate::transport::ConfiguredLoginParam;
use crate::{constants, stats};
pub(crate) mod connectivity;
@@ -145,14 +145,14 @@ impl SchedulerState {
InnerSchedulerState::Started(_) => {
let new_state = InnerSchedulerState::Paused {
started: true,
pause_guards_count: NonZeroUsize::new(1).unwrap(),
pause_guards_count: NonZeroUsize::MIN,
};
Self::do_stop(&mut inner, context, new_state).await;
}
InnerSchedulerState::Stopped => {
*inner = InnerSchedulerState::Paused {
started: false,
pause_guards_count: NonZeroUsize::new(1).unwrap(),
pause_guards_count: NonZeroUsize::MIN,
};
}
InnerSchedulerState::Paused {
@@ -183,7 +183,7 @@ impl SchedulerState {
ref started,
ref mut pause_guards_count,
} => {
if *pause_guards_count == NonZeroUsize::new(1).unwrap() {
if *pause_guards_count == NonZeroUsize::MIN {
match *started {
true => SchedulerState::do_start(&mut inner, &context).await,
false => *inner = InnerSchedulerState::Stopped,
@@ -212,21 +212,25 @@ impl SchedulerState {
/// Indicate that the network likely has come back.
pub(crate) async fn maybe_network(&self) {
let inner = self.inner.read().await;
let (inbox, oboxes) = match *inner {
let (inboxes, oboxes) = match *inner {
InnerSchedulerState::Started(ref scheduler) => {
scheduler.maybe_network();
let inbox = scheduler.inbox.conn_state.state.connectivity.clone();
let inboxes = scheduler
.inboxes
.iter()
.map(|b| b.conn_state.state.connectivity.clone())
.collect::<Vec<_>>();
let oboxes = scheduler
.oboxes
.iter()
.map(|b| b.conn_state.state.connectivity.clone())
.collect::<Vec<_>>();
(inbox, oboxes)
(inboxes, oboxes)
}
_ => return,
};
drop(inner);
connectivity::idle_interrupted(inbox, oboxes);
connectivity::idle_interrupted(inboxes, oboxes);
}
/// Indicate that the network likely is lost.
@@ -331,7 +335,8 @@ struct SchedBox {
/// Job and connection scheduler.
#[derive(Debug)]
pub(crate) struct Scheduler {
inbox: SchedBox,
/// Inboxes, one per transport.
inboxes: Vec<SchedBox>,
/// Optional boxes -- mvbox.
oboxes: Vec<SchedBox>,
smtp: SmtpConnectionState,
@@ -474,17 +479,17 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
}
// Update quota no more than once a minute.
if ctx.quota_needs_update(60).await {
if let Err(err) = ctx.update_recent_quota(&mut session).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
if ctx.quota_needs_update(60).await
&& let Err(err) = ctx.update_recent_quota(&mut session).await
{
warn!(ctx, "Failed to update quota: {:#}.", err);
}
if let Ok(()) = imap.resync_request_receiver.try_recv() {
if let Err(err) = session.resync_folders(ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
imap.resync_request_sender.try_send(()).ok();
}
if let Ok(()) = imap.resync_request_receiver.try_recv()
&& let Err(err) = session.resync_folders(ctx).await
{
warn!(ctx, "Failed to resync folders: {:#}.", err);
imap.resync_request_sender.try_send(()).ok();
}
maybe_add_time_based_warnings(ctx).await;
@@ -857,34 +862,40 @@ impl Scheduler {
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
let mut inboxes = Vec::new();
let mut oboxes = Vec::new();
let mut start_recvs = Vec::new();
let (conn_state, inbox_handlers) = ImapConnectionState::new(ctx).await?;
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
let handle = {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let inbox = SchedBox {
meaning: FolderMeaning::Inbox,
conn_state,
handle,
};
start_recvs.push(inbox_start_recv);
if ctx.should_watch_mvbox().await? {
let (conn_state, handlers) = ImapConnectionState::new(ctx).await?;
let (start_send, start_recv) = oneshot::channel();
let ctx = ctx.clone();
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
meaning,
for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? {
let (conn_state, inbox_handlers) =
ImapConnectionState::new(ctx, transport_id, configured_login_param.clone()).await?;
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
let handle = {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let inbox = SchedBox {
meaning: FolderMeaning::Inbox,
conn_state,
handle,
});
start_recvs.push(start_recv);
};
inboxes.push(inbox);
start_recvs.push(inbox_start_recv);
if ctx.should_watch_mvbox().await? {
let (conn_state, handlers) =
ImapConnectionState::new(ctx, transport_id, configured_login_param).await?;
let (start_send, start_recv) = oneshot::channel();
let ctx = ctx.clone();
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
meaning,
conn_state,
handle,
});
start_recvs.push(start_recv);
}
}
let smtp_handle = {
@@ -910,7 +921,7 @@ impl Scheduler {
let recently_seen_loop = RecentlySeenLoop::new(ctx.clone());
let res = Self {
inbox,
inboxes,
oboxes,
smtp,
smtp_handle,
@@ -930,8 +941,8 @@ impl Scheduler {
Ok(res)
}
fn boxes(&self) -> iter::Chain<iter::Once<&SchedBox>, std::slice::Iter<'_, SchedBox>> {
once(&self.inbox).chain(self.oboxes.iter())
fn boxes(&self) -> impl Iterator<Item = &SchedBox> {
self.inboxes.iter().chain(self.oboxes.iter())
}
fn maybe_network(&self) {
@@ -949,7 +960,9 @@ impl Scheduler {
}
fn interrupt_inbox(&self) {
self.inbox.conn_state.interrupt();
for b in &self.inboxes {
b.conn_state.interrupt();
}
}
fn interrupt_oboxes(&self) {
@@ -989,7 +1002,7 @@ impl Scheduler {
let timeout_duration = std::time::Duration::from_secs(30);
let tracker = TaskTracker::new();
for b in once(self.inbox).chain(self.oboxes) {
for b in self.inboxes.into_iter().chain(self.oboxes.into_iter()) {
let context = context.clone();
tracker.spawn(async move {
tokio::time::timeout(timeout_duration, b.handle)
@@ -1095,12 +1108,17 @@ pub(crate) struct ImapConnectionState {
impl ImapConnectionState {
/// Construct a new connection.
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
async fn new(
context: &Context,
transport_id: u32,
login_param: ConfiguredLoginParam,
) -> Result<(Self, ImapConnectionHandlers)> {
let stop_token = CancellationToken::new();
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
let handlers = ImapConnectionHandlers {
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
connection: Imap::new(context, transport_id, login_param, idle_interrupt_receiver)
.await?,
stop_token: stop_token.clone(),
};

View File

@@ -201,19 +201,20 @@ impl ConnectivityStore {
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
let mut connectivity_lock = inbox.0.lock();
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
// return Connected until DC is completely done with fetching folders; this also
// includes scan_folders() which happens on the inbox thread.
if *connectivity_lock == DetailedConnectivity::Idle
|| *connectivity_lock == DetailedConnectivity::NotConfigured
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>, oboxes: Vec<ConnectivityStore>) {
for inbox in inboxes {
let mut connectivity_lock = inbox.0.lock();
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
// return Connected until DC is completely done with fetching folders; this also
// includes scan_folders() which happens on the inbox thread.
if *connectivity_lock == DetailedConnectivity::Idle
|| *connectivity_lock == DetailedConnectivity::NotConfigured
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
}
drop(connectivity_lock);
for state in oboxes {
let mut connectivity_lock = state.0.lock();

View File

@@ -118,7 +118,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
chat.id,
);
let text = BROADCAST_INCOMPATIBILITY_MSG;
add_info_msg(context, chat.id, text, time()).await?;
add_info_msg(context, chat.id, text).await?;
bail!(text.to_string());
}
}

View File

@@ -127,7 +127,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Group { .. } => {
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
chat::add_info_msg(context, joining_chat_id, &msg).await?;
Ok(joining_chat_id)
}
QrInvite::Broadcast { .. } => {
@@ -144,32 +144,23 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
}
// If we were not in the broadcast channel before, show a 'please wait' info message.
// Since we don't have any specific stock string for this,
// use the generic `Establishing guaranteed end-to-end encryption, please wait…`
if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
let msg = stock_str::securejoin_wait(context).await;
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
let msg =
stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
chat::add_info_msg(context, joining_chat_id, &msg).await?;
}
Ok(joining_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
// uses it to send the handshake messages.
// Calculate the sort timestamp before checking the chat protection status so that if we
// race with its change, we don't add our message below the protection message.
let sort_to_bottom = true;
let (received, incoming) = (false, false);
let ts_sort = private_chat_id
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
.await?;
let ts_start = time();
chat::add_info_msg_with_cmd(
context,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
ts_sort,
Some(ts_start),
None,
time(),
None,
None,
None,
@@ -243,7 +234,7 @@ pub(super) async fn handle_auth_required(
let contact_id = invite.contact_id();
let msg = stock_str::secure_join_replies(context, contact_id).await;
let chat_id = joining_chat_id(context, &invite, chat_id).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat::add_info_msg(context, chat_id, &msg).await?;
}
}

View File

@@ -199,19 +199,17 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
})
.collect::<Vec<&str>>()
.join("\n");
if l_last > 1 {
if let Some(line) = lines.get(l_last - 1) {
if is_empty_line(line) {
l_last -= 1
}
}
if l_last > 1
&& let Some(line) = lines.get(l_last - 1)
&& is_empty_line(line)
{
l_last -= 1
}
if l_last > 1 {
if let Some(line) = lines.get(l_last - 1) {
if is_quoted_headline(line) {
l_last -= 1
}
}
if l_last > 1
&& let Some(line) = lines.get(l_last - 1)
&& is_quoted_headline(line)
{
l_last -= 1
}
(lines.get(..l_last).unwrap_or(lines), Some(quoted_text))
} else {

View File

@@ -89,7 +89,7 @@ impl Smtp {
}
self.connectivity.set_connecting(context);
let lp = ConfiguredLoginParam::load(context)
let (_transport_id, lp) = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let proxy_config = ProxyConfig::load(context).await?;
@@ -307,24 +307,23 @@ pub(crate) async fn smtp_send(
Ok(()) => SendResult::Success,
};
if let SendResult::Failure(err) = &status {
if let Some(msg_id) = msg_id {
// We couldn't send the message, so mark it as failed
match Message::load_from_db(context, msg_id).await {
Ok(mut msg) => {
if let Err(err) =
message::set_msg_failed(context, &mut msg, &err.to_string()).await
{
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
}
}
Err(err) => {
error!(
context,
"Failed to load {msg_id} to mark it as failed: {err:#}."
);
if let SendResult::Failure(err) = &status
&& let Some(msg_id) = msg_id
{
// We couldn't send the message, so mark it as failed
match Message::load_from_db(context, msg_id).await {
Ok(mut msg) => {
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
{
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
}
}
Err(err) => {
error!(
context,
"Failed to load {msg_id} to mark it as failed: {err:#}."
);
}
}
}
status
@@ -444,11 +443,11 @@ pub(crate) async fn send_msg_to_smtp(
chat_id,
&text,
crate::mimeparser::SystemMessage::InvalidUnencryptedMail,
Some(timestamp_sort),
timestamp_sort,
None,
None,
None,
None,
)
.await?;
};

View File

@@ -2,6 +2,7 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
@@ -235,26 +236,24 @@ impl Sql {
}
}
if recode_avatar {
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
match blob.recode_to_avatar_size(context).await {
Ok(()) => {
if let Some(path) = blob.to_abs_path().to_str() {
context
.set_config_internal(Config::Selfavatar, Some(path))
.await?;
} else {
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
}
}
Err(e) => {
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
if recode_avatar && let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
match blob.recode_to_avatar_size(context).await {
Ok(()) => {
if let Some(path) = blob.to_abs_path().to_str() {
context
.set_config_internal(Config::Selfavatar, None)
.await?
.set_config_internal(Config::Selfavatar, Some(path))
.await?;
} else {
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
}
}
Err(e) => {
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
context
.set_config_internal(Config::Selfavatar, None)
.await?
}
}
}
@@ -762,7 +761,6 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
conn.execute_batch(
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
PRAGMA secure_delete=on;
PRAGMA busy_timeout = 0; -- fail immediately
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
PRAGMA foreign_keys=on;
",
@@ -775,6 +773,22 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
conn.pragma_update(None, "temp_store", "memory")?;
}
// Fail immediately when the database is busy,
// except for iOS. On iOS we don't have
// `accounts.lock` lockfile and the database
// is used by two processes:
// main process and the notification extension.
// Due to a bug they both may run at the same time
// and try to write to the database.
// As a workaround, we wait up to 1 minute and retry
// instead of failing immediately and
// possibly missing a message.
if cfg!(target_os = "ios") {
conn.busy_timeout(Duration::new(60, 0))?;
} else {
conn.busy_timeout(Duration::ZERO)?;
}
if !passphrase.is_empty() {
conn.pragma_update(None, "key", passphrase)?;
}

View File

@@ -449,12 +449,11 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
disable_server_delete = true;
// Don't disable server delete if it was on by default (Nauta):
if let Some(provider) = context.get_configured_provider().await? {
if let Some(defaults) = &provider.config_defaults {
if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) {
disable_server_delete = false;
}
}
if let Some(provider) = context.get_configured_provider().await?
&& let Some(defaults) = &provider.config_defaults
&& defaults.iter().any(|d| d.key == Config::DeleteServerAfter)
{
disable_server_delete = false;
}
}
sql.set_db_version(73).await?;
@@ -1363,6 +1362,89 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 139)?;
if dbversion < migration_version {
sql.execute_migration_transaction(
|transaction| {
if exists_before_update {
let is_chatmail = transaction
.query_row(
"SELECT value FROM config WHERE keyname='is_chatmail'",
(),
|row| {
let value: String = row.get(0)?;
Ok(value)
},
)
.optional()?
.as_deref()
== Some("1");
// For non-chatmail accounts
// default "bcc_self" was "1".
// If it is not in the database,
// save the old default explicity
// as the new default is "0"
// for all accounts.
if !is_chatmail {
transaction.execute(
"INSERT OR IGNORE
INTO config (keyname, value)
VALUES (?, ?)",
("bcc_self", "1"),
)?;
}
}
Ok(())
},
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 140)?;
if dbversion < migration_version {
sql.execute_migration(
"
CREATE TABLE new_imap (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
rfc724_mid TEXT NOT NULL, -- Message-ID header
folder TEXT NOT NULL, -- IMAP folder
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
uid INTEGER NOT NULL, -- UID
uidvalidity INTEGER NOT NULL,
UNIQUE (transport_id, folder, uid, uidvalidity)
) STRICT;
INSERT OR IGNORE INTO new_imap SELECT
id, 1, rfc724_mid, folder, target, uid, uidvalidity
FROM imap;
DROP TABLE imap;
ALTER TABLE new_imap RENAME TO imap;
CREATE INDEX imap_folder ON imap(transport_id, folder);
CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
CREATE TABLE new_imap_sync (
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
folder TEXT NOT NULL,
uidvalidity INTEGER NOT NULL DEFAULT 0,
uid_next INTEGER NOT NULL DEFAULT 0,
modseq INTEGER NOT NULL DEFAULT 0,
UNIQUE (transport_id, folder)
) STRICT;
INSERT OR IGNORE INTO new_imap_sync SELECT
1, folder, uidvalidity, uid_next, modseq
FROM imap_sync;
DROP TABLE imap_sync;
ALTER TABLE new_imap_sync RENAME TO imap_sync;
CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -151,10 +151,10 @@ pub struct PooledConnection {
impl Drop for PooledConnection {
fn drop(&mut self) {
// Put the connection back unless the pool is already dropped.
if let Some(pool) = self.pool.upgrade() {
if let Some(conn) = self.conn.take() {
pool.put(conn);
}
if let Some(pool) = self.pool.upgrade()
&& let Some(conn) = self.conn.take()
{
pool.put(conn);
}
}
}

View File

@@ -392,7 +392,7 @@ pub enum StockMessage {
#[strum(props(fallback = "Member %1$s removed."))]
MsgDelMember = 178,
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
#[strum(props(fallback = "Establishing connection, please wait…"))]
SecurejoinWait = 190,
#[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
@@ -429,6 +429,10 @@ https://delta.chat/donate"))]
#[strum(props(fallback = "You joined the channel."))]
MsgYouJoinedBroadcast = 202,
#[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
Waiting for the device of %2$s to reply…"))]
SecureJoinBroadcastStarted = 203,
#[strum(props(
fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
))]
@@ -737,6 +741,21 @@ pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
translated(context, StockMessage::MsgYouJoinedBroadcast).await
}
/// Stock string: `%1$s invited you to join this channel. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_broadcast_started(
context: &Context,
inviter_contact_id: ContactId,
) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinBroadcastStarted)
.await
.replace1(contact.get_display_name())
.replace2(contact.get_display_name())
} else {
format!("secure_join_started: unknown contact {inviter_contact_id}")
}
}
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
pub(crate) async fn msg_reacted(
context: &Context,
@@ -811,7 +830,7 @@ pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Establishing guaranteed end-to-end encryption, please wait…`.
/// Stock string: `Establishing connection, please wait…`.
pub(crate) async fn securejoin_wait(context: &Context) -> String {
translated(context, StockMessage::SecurejoinWait).await
}

109
src/storage_usage.rs Normal file
View File

@@ -0,0 +1,109 @@
//! Module to collect and display Disk Space Usage of a Profile.
use crate::{context::Context, message::MsgId};
use anyhow::Result;
use humansize::{BINARY, format_size};
/// Storage Usage Report
/// Useful for debugging space usage problems in the deltachat database.
#[derive(Debug)]
pub struct StorageUsage {
/// Total database size, subtract this from the backup size to estimate size of all blobs
pub db_size: usize,
/// size and row count of the 10 biggest tables
pub largest_tables: Vec<(String, usize, Option<usize>)>,
/// count and total size of status updates
/// for the 10 webxdc apps with the most size usage in status updates
pub largest_webxdc_data: Vec<(MsgId, usize, usize)>,
}
impl std::fmt::Display for StorageUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Storage Usage:")?;
let human_db_size = format_size(self.db_size, BINARY);
writeln!(f, "[Database Size]: {human_db_size}")?;
writeln!(f, "[Largest Tables]:")?;
for (name, size, row_count) in &self.largest_tables {
let human_table_size = format_size(*size, BINARY);
writeln!(
f,
" {name:<20} {human_table_size:>10}, {row_count:>6} rows",
name = format!("{name}:"),
row_count = row_count.map(|c| c.to_string()).unwrap_or("?".to_owned())
)?;
}
writeln!(f, "[Webxdc With Biggest Status Update Space Usage]:")?;
for (msg_id, size, update_count) in &self.largest_webxdc_data {
let human_size = format_size(*size, BINARY);
writeln!(
f,
" {msg_id:<8} {human_size:>10} across {update_count:>5} updates",
msg_id = format!("{msg_id}:")
)?;
}
Ok(())
}
}
/// Get storage usage information for the Context's database
pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
let page_size: usize = ctx
.sql
.query_get_value("PRAGMA page_size", ())
.await?
.unwrap_or_default();
let page_count: usize = ctx
.sql
.query_get_value("PRAGMA page_count", ())
.await?
.unwrap_or_default();
let mut largest_tables = ctx
.sql
.query_map_vec(
"SELECT name,
SUM(pgsize) AS size
FROM dbstat
WHERE name IN (SELECT name FROM sqlite_master WHERE type='table')
GROUP BY name ORDER BY size DESC LIMIT 10",
(),
|row| {
let name: String = row.get(0)?;
let size: usize = row.get(1)?;
Ok((name, size, None))
},
)
.await?;
for row in &mut largest_tables {
let name = &row.0;
let row_count: Result<Option<usize>> = ctx
.sql
// SECURITY: the table name comes from the db, not from the user
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
.await;
row.2 = row_count.unwrap_or_default();
}
let largest_webxdc_data = ctx
.sql
.query_map_vec(
"SELECT msg_id, SUM(length(update_item)) as size, COUNT(*) as update_count
FROM msgs_status_updates
GROUP BY msg_id ORDER BY size DESC LIMIT 10",
(),
|row| {
let msg_id: MsgId = row.get(0)?;
let size: usize = row.get(1)?;
let count: usize = row.get(2)?;
Ok((msg_id, size, count))
},
)
.await?;
Ok(StorageUsage {
db_size: page_size * page_count,
largest_tables,
largest_webxdc_data,
})
}

View File

@@ -657,14 +657,11 @@ mod tests {
alice1.set_config_bool(Config::SyncMsgs, true).await?;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
if chatmail {
alice1.set_config_bool(Config::IsChatmail, true).await?;
alice2.set_config_bool(Config::IsChatmail, true).await?;
} else {
alice2.set_config_bool(Config::BccSelf, false).await?;
}
alice1.set_config_bool(Config::IsChatmail, chatmail).await?;
alice2.set_config_bool(Config::IsChatmail, chatmail).await?;
alice1.set_config_bool(Config::BccSelf, true).await?;
alice2.set_config_bool(Config::BccSelf, false).await?;
let sent_msg = if sync_message_sent {
alice1

View File

@@ -549,6 +549,7 @@ impl TestContext {
ctx.set_config(Config::SkipStartMessages, Some("1"))
.await
.unwrap();
ctx.set_config(Config::BccSelf, Some("1")).await.unwrap();
ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap();
Self {

View File

@@ -727,10 +727,10 @@ pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
/// Otherwise, return None.
pub(crate) fn single_value<T>(collection: impl IntoIterator<Item = T>) -> Option<T> {
let mut iter = collection.into_iter();
if let Some(value) = iter.next() {
if iter.next().is_none() {
return Some(value);
}
if let Some(value) = iter.next()
&& iter.next().is_none()
{
return Some(value);
}
None
}

View File

@@ -10,8 +10,8 @@
use std::fmt;
use anyhow::{Context as _, Result, bail, ensure, format_err};
use deltachat_contact_tools::{EmailAddress, addr_cmp, addr_normalize};
use anyhow::{Context as _, Result, bail, format_err};
use deltachat_contact_tools::{EmailAddress, addr_normalize};
use serde::{Deserialize, Serialize};
use crate::config::Config;
@@ -240,24 +240,46 @@ impl fmt::Display for ConfiguredLoginParam {
impl ConfiguredLoginParam {
/// Load configured account settings from the database.
///
/// Returns transport ID and configured parameters
/// of the current primary transport.
/// Returns `None` if account is not configured.
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
pub(crate) async fn load(context: &Context) -> Result<Option<(u32, Self)>> {
let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
return Ok(None);
};
let json: Option<String> = context
let Some((id, json)) = context
.sql
.query_get_value(
"SELECT configured_param FROM transports WHERE addr=?",
.query_row_optional(
"SELECT id, configured_param FROM transports WHERE addr=?",
(&self_addr,),
|row| {
let id: u32 = row.get(0)?;
let json: String = row.get(1)?;
Ok((id, json))
},
)
.await?;
if let Some(json) = json {
Ok(Some(Self::from_json(&json)?))
} else {
.await?
else {
bail!("Self address {self_addr} doesn't have a corresponding transport");
}
};
Ok(Some((id, Self::from_json(&json)?)))
}
/// Loads configured login parameters for all transports.
///
/// Returns a vector of all transport IDs
/// paired with the configured parameters for the transports.
pub(crate) async fn load_all(context: &Context) -> Result<Vec<(u32, Self)>> {
context
.sql
.query_map_vec("SELECT id, configured_param FROM transports", (), |row| {
let id: u32 = row.get(0)?;
let json: String = row.get(1)?;
let param = Self::from_json(&json)?;
Ok((id, param))
})
.await
}
/// Loads legacy configured param. Only used for tests and the migration.
@@ -536,12 +558,6 @@ impl ConfiguredLoginParam {
let addr = addr_normalize(&self.addr);
let provider_id = self.provider.map(|provider| provider.id);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
if let Some(configured_addr) = &configured_addr {
ensure!(
addr_cmp(configured_addr, &addr),
"Adding a second transport is not supported right now."
);
}
context
.sql
.execute(
@@ -680,7 +696,7 @@ mod tests {
expected_param
);
assert_eq!(t.is_configured().await?, true);
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(param, loaded);
// Legacy ConfiguredImapCertificateChecks config is ignored
@@ -789,7 +805,7 @@ mod tests {
assert_eq!(loaded, param);
migrate_configured_login_param(&t).await;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded, param);
Ok(())
@@ -833,7 +849,7 @@ mod tests {
migrate_configured_login_param(&t).await;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
@@ -890,7 +906,7 @@ mod tests {
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);

View File

@@ -309,13 +309,11 @@ impl Context {
},
)
.await?
&& last_from_id == from_id
&& last_param.get_cmd() == SystemMessage::WebxdcInfoMessage
&& last_in_repl_to == instance.rfc724_mid
{
if last_from_id == from_id
&& last_param.get_cmd() == SystemMessage::WebxdcInfoMessage
&& last_in_repl_to == instance.rfc724_mid
{
return Ok(Some(last_msg_id));
}
return Ok(Some(last_msg_id));
}
Ok(None)
}
@@ -341,63 +339,59 @@ impl Context {
let mut param_changed = false;
let mut instance = instance.clone();
if let Some(ref document) = status_update_item.document {
if instance
if let Some(ref document) = status_update_item.document
&& instance
.param
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
{
instance.param.set(Param::WebxdcDocument, document);
param_changed = true;
}
{
instance.param.set(Param::WebxdcDocument, document);
param_changed = true;
}
if let Some(ref summary) = status_update_item.summary {
if instance
if let Some(ref summary) = status_update_item.summary
&& instance
.param
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
{
let summary = sanitize_bidi_characters(summary);
instance.param.set(Param::WebxdcSummary, summary.clone());
param_changed = true;
}
{
let summary = sanitize_bidi_characters(summary);
instance.param.set(Param::WebxdcSummary, summary.clone());
param_changed = true;
}
if can_info_msg {
if let Some(ref info) = status_update_item.info {
let info_msg_id = self
.get_overwritable_info_msg_id(&instance, from_id)
.await?;
if can_info_msg && let Some(ref info) = status_update_item.info {
let info_msg_id = self
.get_overwritable_info_msg_id(&instance, from_id)
.await?;
if let (Some(info_msg_id), None) = (info_msg_id, &status_update_item.href) {
chat::update_msg_text_and_timestamp(
self,
instance.chat_id,
info_msg_id,
info.as_str(),
timestamp,
)
.await?;
notify_msg_id = info_msg_id;
} else {
notify_msg_id = chat::add_info_msg_with_cmd(
self,
instance.chat_id,
info.as_str(),
SystemMessage::WebxdcInfoMessage,
timestamp,
None,
Some(&instance),
Some(from_id),
None,
)
.await?;
}
if let (Some(info_msg_id), None) = (info_msg_id, &status_update_item.href) {
chat::update_msg_text_and_timestamp(
self,
instance.chat_id,
info_msg_id,
info.as_str(),
timestamp,
)
.await?;
notify_msg_id = info_msg_id;
} else {
notify_msg_id = chat::add_info_msg_with_cmd(
self,
instance.chat_id,
info.as_str(),
SystemMessage::WebxdcInfoMessage,
Some(timestamp),
timestamp,
Some(&instance),
Some(from_id),
None,
)
.await?;
}
if let Some(ref href) = status_update_item.href {
let mut notify_msg = Message::load_from_db(self, notify_msg_id).await?;
notify_msg.param.set(Param::Arg, href);
notify_msg.update_param(self).await?;
}
if let Some(ref href) = status_update_item.href {
let mut notify_msg = Message::load_from_db(self, notify_msg_id).await?;
notify_msg.param.set(Param::Arg, href);
notify_msg.update_param(self).await?;
}
}
@@ -413,20 +407,19 @@ impl Context {
});
}
if from_id != ContactId::SELF {
if let Some(notify_list) = status_update_item.notify {
let self_addr = instance.get_webxdc_self_addr(self).await?;
if let Some(notify_text) =
notify_list.get(&self_addr).or_else(|| notify_list.get("*"))
{
self.emit_event(EventType::IncomingWebxdcNotify {
chat_id: instance.chat_id,
contact_id: from_id,
msg_id: notify_msg_id,
text: notify_text.clone(),
href: status_update_item.href,
});
}
if from_id != ContactId::SELF
&& let Some(notify_list) = status_update_item.notify
{
let self_addr = instance.get_webxdc_self_addr(self).await?;
if let Some(notify_text) = notify_list.get(&self_addr).or_else(|| notify_list.get("*"))
{
self.emit_event(EventType::IncomingWebxdcNotify {
chat_id: instance.chat_id,
contact_id: from_id,
msg_id: notify_msg_id,
text: notify_text.clone(),
href: status_update_item.href,
});
}
}
@@ -882,18 +875,15 @@ impl Message {
let mut archive = self.get_webxdc_archive(context).await?;
if name == "index.html" {
if let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await {
if let Ok(manifest) = parse_webxdc_manifest(&bytes) {
if let Some(min_api) = manifest.min_api {
if min_api > WEBXDC_API_VERSION {
return Ok(Vec::from(
"<!DOCTYPE html>This Webxdc requires a newer Delta Chat version.",
));
}
}
}
}
if name == "index.html"
&& let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await
&& let Ok(manifest) = parse_webxdc_manifest(&bytes)
&& let Some(min_api) = manifest.min_api
&& min_api > WEBXDC_API_VERSION
{
return Ok(Vec::from(
"<!DOCTYPE html>This Webxdc requires a newer Delta Chat version.",
));
}
get_blob(&mut archive, name).await

View File

@@ -122,12 +122,11 @@ pub(crate) async fn intercept_get_updates(
if location.independent != 0 {
if let Some(marker) = &location.marker {
label = marker.to_string() // marker contains one-char labels only
} else if location.msg_id != 0 {
if let Some(msg) =
} else if location.msg_id != 0
&& let Some(msg) =
Message::load_from_db_optional(context, MsgId::new(location.msg_id)).await?
{
label = msg.get_text()
}
{
label = msg.get_text()
}
}

View File

@@ -1,6 +1,6 @@
# Standards used in Delta Chat
# Standards used in chatmail core
Some of the standards Delta Chat is based on:
Some of the standards chatmail is based on:
Tasks | Standards
-------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -19,14 +19,14 @@ Authorization | OAuth2 ([RFC 6749][])
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Detect/prevent active attacks | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme
Header encryption | [Header Protection for Cryptographically Protected E-mail](https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/)
Metadata minimization | Header Protection for Cryptographically Protected Email ([RFC 9788][])
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
Messenger functions | [Chat-over-Email](https://github.com/chatmail/core/blob/main/spec.md#chat-mail-specification)
Detect mailing list | List-Id ([RFC 2919][]) and Precedence ([RFC 3834][])
User and chat colors | [XEP-0392][]: Consistent Color Generation
Send and receive system messages | Multipart/Report Media Type ([RFC 6522][])
Send and receive contact files | vCard ([RFC 6350][])
Return receipts | Message Disposition Notification (MDN, [RFC 8098][], [RFC 3503][]) using the Chat-Disposition-Notification-To header
Return receipts | Message Disposition Notification (MDN, [RFC 8098][]) using the Chat-Disposition-Notification-To header
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
[Autocrypt Level 1]: https://autocrypt.org/level1.html
@@ -47,7 +47,6 @@ Locations | KML ([Open Geospatial Consortium](http://www.
[RFC 2919]: https://tools.ietf.org/html/rfc2919
[RFC 2971]: https://tools.ietf.org/html/rfc2971
[RFC 3501]: https://tools.ietf.org/html/rfc3501
[RFC 3503]: https://tools.ietf.org/html/rfc3503
[RFC 3676]: https://tools.ietf.org/html/rfc3676
[RFC 3834]: https://tools.ietf.org/html/rfc3834
[RFC 4880]: https://tools.ietf.org/html/rfc4880
@@ -60,3 +59,4 @@ Locations | KML ([Open Geospatial Consortium](http://www.
[RFC 7162]: https://tools.ietf.org/html/rfc7162
[RFC 8098]: https://tools.ietf.org/html/rfc8098
[RFC 9078]: https://tools.ietf.org/html/rfc9078
[RFC 9788]: https://tools.ietf.org/html/rfc9788

View File

@@ -1,6 +1,8 @@
InBroadcast#Chat#2002: My Channel [2 member(s)] Icon: e9b6c7a78aa2e4f415644f55a553e73.png
--------------------------------------------------------------------------------
Msg#2004: info (Contact#Contact#Info): Establishing guaranteed end-to-end encryption, please wait… [NOTICED][INFO]
Msg#2003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#2004: info (Contact#Contact#Info): Alice invited you to join this channel.
Waiting for the device of Alice to reply… [NOTICED][INFO]
Msg#2007🔒: (Contact#Contact#2001): You joined the channel. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -1,7 +1,9 @@
InBroadcast#Chat#2002: Channel [1 member(s)]
--------------------------------------------------------------------------------
Msg#2004: info (Contact#Contact#Info): Establishing guaranteed end-to-end encryption, please wait… [NOTICED][INFO]
Msg#2003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#2004: info (Contact#Contact#Info): alice@example.org invited you to join this channel.
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#2008🔒: (Contact#Contact#2001): You joined the channel. [FRESH][INFO]
Msg#2010🔒: (Contact#Contact#2001): hi [FRESH]
Msg#2011🔒: (Contact#Contact#2001): Member Me removed by alice@example.org. [FRESH][INFO]

View File

@@ -1,9 +1,9 @@
Group#Chat#6002: Group [3 member(s)]
--------------------------------------------------------------------------------
Msg#6003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#6004: info (Contact#Contact#Info): alice@example.org invited you to join this group.
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#6006: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#6003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#6008🔒: (Contact#Contact#6001): Member Me added by alice@example.org. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -1,10 +1,10 @@
Group#Chat#3002: Group [3 member(s)]
--------------------------------------------------------------------------------
Msg#3003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#3004: info (Contact#Contact#Info): alice@example.org invited you to join this group.
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#3006: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#3003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#3008🔒: (Contact#Contact#3002): [FRESH]
Msg#3009: info (Contact#Contact#Info): Member bob@example.net added. [NOTICED][INFO]
Msg#3010🔒: (Contact#Contact#3001): Member Me added by alice@example.org. [FRESH][INFO]