mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 05:26:42 +03:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6077499f07 | ||
|
|
94d2d8cfd7 | ||
|
|
ba3cad6ad6 | ||
|
|
c9c362d5ff | ||
|
|
6514b4ca7f | ||
|
|
e7e31d7914 | ||
|
|
51d6855e0d | ||
|
|
2f90b55309 | ||
|
|
be3e202470 | ||
|
|
57aadfbbf6 | ||
|
|
849cde9757 | ||
|
|
b4cd99fc56 | ||
|
|
9305a0676c | ||
|
|
39c9ba19ef | ||
|
|
af574279fd | ||
|
|
713c929e03 | ||
|
|
c83c131a37 | ||
|
|
0d0602a4a5 | ||
|
|
abfb556377 | ||
|
|
72788daca0 | ||
|
|
16bd87c78f | ||
|
|
d44e2420bc | ||
|
|
88d213fcdb | ||
|
|
fb14acb0fb | ||
|
|
a5c470fbae | ||
|
|
6bdba33d32 | ||
|
|
c6ace749e3 | ||
|
|
22ebd6436f |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -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
|
||||
|
||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contributing to Delta Chat
|
||||
# Contributing to chatmail core
|
||||
|
||||
## Bug reports
|
||||
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.27.0"
|
||||
version = "2.28.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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"
|
||||
///
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.27.0"
|
||||
"version": "2.28.0"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
158
deltachat-rpc-client/tests/test_multitransport.py
Normal file
158
deltachat-rpc-client/tests/test_multitransport.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.27.0"
|
||||
"version": "2.28.0"
|
||||
}
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -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": {
|
||||
|
||||
23
flake.nix
23
flake.nix
@@ -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 = ''
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-11-16
|
||||
2025-11-23
|
||||
@@ -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 {
|
||||
|
||||
202
src/chat.rs
202
src/chat.rs
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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?;
|
||||
|
||||
137
src/configure.rs
137
src/configure.rs
@@ -130,12 +130,6 @@ impl Context {
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
param.addr = addr_normalize(¶m.addr);
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.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=?",
|
||||
(¶m.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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
87
src/html.rs
87
src/html.rs
@@ -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(())
|
||||
|
||||
234
src/imap.rs
234
src/imap.rs
@@ -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 = ¶m.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,
|
||||
¶m.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 ¤t 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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
120
src/message.rs
120
src/message.rs
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
395
src/net/dns.rs
395
src/net/dns.rs
@@ -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)),
|
||||
],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
114
src/scheduler.rs
114
src/scheduler.rs
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
35
src/smtp.rs
35
src/smtp.rs
@@ -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?;
|
||||
};
|
||||
|
||||
50
src/sql.rs
50
src/sql.rs
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
109
src/storage_usage.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
148
src/webxdc.rs
148
src/webxdc.rs
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
standards.md
10
standards.md
@@ -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
|
||||
|
||||
@@ -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]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user