mirror of
https://github.com/chatmail/core.git
synced 2026-05-10 10:26:30 +03:00
Compare commits
14 Commits
iequidoo/r
...
r10s/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07efd41537 | ||
|
|
7bd1429f4f | ||
|
|
d069e75cd8 | ||
|
|
ad5e904d1c | ||
|
|
38affa2c62 | ||
|
|
6dfc6f8780 | ||
|
|
8cca0cf75d | ||
|
|
b81f50be8f | ||
|
|
970222f376 | ||
|
|
83e31a5f17 | ||
|
|
31fabb24df | ||
|
|
66df0d2a3c | ||
|
|
5a6b1c62dd | ||
|
|
18d878378f |
2
.github/workflows/deltachat-rpc-server.yml
vendored
2
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -382,7 +382,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -23,4 +23,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -3933,9 +3933,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.72"
|
||||
version = "0.10.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
@@ -3974,9 +3974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.107"
|
||||
version = "0.9.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -5164,7 +5164,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.12",
|
||||
"rustls-webpki 0.103.13",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5201,9 +5201,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.12"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
||||
@@ -390,27 +390,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
/**
|
||||
* Configure the context. The configuration is handled by key=value pairs as:
|
||||
*
|
||||
* - `addr` = Email address to use for configuration.
|
||||
* If dc_configure() fails this is not the email address actually in use.
|
||||
* Use `configured_addr` to find out the email address actually in use.
|
||||
* - `configured_addr` = Email address actually in use.
|
||||
* - `configured_addr` = Email address in use.
|
||||
* Unless for testing, do not set this value using dc_set_config().
|
||||
* Instead, set `addr` and call dc_configure().
|
||||
* - `mail_server` = IMAP-server, guessed if left out
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
* - `mail_port` = IMAP-port, guessed if left out
|
||||
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `send_server` = SMTP-server, guessed if left out
|
||||
* - `send_user` = SMTP-user, guessed if left out
|
||||
* - `send_pw` = SMTP-password, guessed if left out
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `proxy_enabled` = Proxy enabled. Disabled by default.
|
||||
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
|
||||
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
@@ -513,6 +495,27 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1 = Contacts (default, does not include contact requests),
|
||||
* 2 = Nobody (calls never result in a notification).
|
||||
*
|
||||
* Also, there are configs that are only needed
|
||||
* if you want to use the deprecated dc_configure() API, such as:
|
||||
*
|
||||
* - `addr` = Email address to use for configuration.
|
||||
* If dc_configure() fails this is not the email address actually in use.
|
||||
* Use `configured_addr` to find out the email address actually in use.
|
||||
* - `mail_server` = IMAP-server, guessed if left out
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
* - `mail_port` = IMAP-port, guessed if left out
|
||||
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `send_server` = SMTP-server, guessed if left out
|
||||
* - `send_user` = SMTP-user, guessed if left out
|
||||
* - `send_pw` = SMTP-password, guessed if left out
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `proxy_enabled` = Proxy enabled. Disabled by default.
|
||||
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
|
||||
* - `imap_certificate_checks` = how to check IMAP and SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -699,6 +702,12 @@ int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
/**
|
||||
* Configure a context.
|
||||
*
|
||||
* This way of configuring a context is deprecated,
|
||||
* and does not allow to configure multiple transports.
|
||||
* If you can, use the JSON-RPC API (../deltachat-jsonrpc/src/api.rs)
|
||||
* `add_or_update_transport()`/`addOrUpdateTransport()` instead.
|
||||
*
|
||||
* During configuration IO must not be started,
|
||||
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
|
||||
* If the context is already configured,
|
||||
@@ -2632,6 +2641,8 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
* so that the QR code is useful also without Delta Chat being installed
|
||||
* or can be passed to contacts through other channels.
|
||||
*
|
||||
* To reset invitations, pass the link to dc_set_config_from_qr().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id If set to a group-chat-id,
|
||||
@@ -5804,7 +5815,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* These constants configure TLS certificate checks for IMAP and SMTP connections.
|
||||
*
|
||||
* These constants are set via dc_set_config()
|
||||
* using keys "imap_certificate_checks" and "smtp_certificate_checks".
|
||||
* using key "imap_certificate_checks".
|
||||
*
|
||||
* @addtogroup DC_CERTCK
|
||||
* @{
|
||||
|
||||
@@ -883,6 +883,8 @@ impl CommandApi {
|
||||
|
||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||
///
|
||||
/// To reset invitations, pass the link to `set_config_from_qr()`.
|
||||
///
|
||||
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
||||
/// If `chat_id` is unset, setup contact QR code is returned.
|
||||
async fn get_chat_securejoin_qr_code(
|
||||
|
||||
@@ -85,7 +85,7 @@ mod tests {
|
||||
assert_eq!(result, response.to_owned());
|
||||
}
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":""}]}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||
session.handle_incoming(request).await;
|
||||
let result = receiver.recv().await?;
|
||||
|
||||
@@ -442,35 +442,6 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
# Get an accepted chat, otherwise alice2 won't be notified about the 2nd message.
|
||||
chat_alice2 = alice2.create_chat(bob)
|
||||
chat_id_alice2 = chat_alice2.get_basic_snapshot().id
|
||||
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_text("Hello!")
|
||||
msg_alice = alice.wait_for_incoming_msg()
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_bob_alice.send_text("What's new?")
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_alice2 = alice2.get_chat_by_id(chat_id_alice2)
|
||||
assert chat_alice2.get_fresh_message_count() == 2
|
||||
|
||||
msg_alice.mark_seen()
|
||||
assert alice2.wait_for_msgs_changed_event().chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 1
|
||||
|
||||
msg_id = alice.wait_for_msgs_changed_event().msg_id
|
||||
msg = alice.get_message_by_id(msg_id)
|
||||
msg.mark_seen()
|
||||
assert alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 0
|
||||
|
||||
|
||||
def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -27,7 +27,13 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||
"RUSTSEC-2026-0049",
|
||||
"RUSTSEC-2026-0098",
|
||||
"RUSTSEC-2026-0099"
|
||||
"RUSTSEC-2026-0099",
|
||||
|
||||
# Panic in CRL signature checks.
|
||||
# We do not check CRL and cannot update rustls-webpki 0.102.8
|
||||
# which is a dependency of iroh 0.35.0.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
|
||||
"RUSTSEC-2026-0104"
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -433,7 +433,6 @@ class ACFactory:
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
assert "addr" in configdict and "mail_pw" in configdict
|
||||
return configdict
|
||||
@@ -505,7 +504,6 @@ class ACFactory:
|
||||
"addr": cloned_from.get_config("addr"),
|
||||
"mail_pw": cloned_from.get_config("mail_pw"),
|
||||
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
|
||||
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
|
||||
}
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
|
||||
REV=ad097ee40579c884e7757de2d3bb0a51f481a32a
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
85
src/chat.rs
85
src/chat.rs
@@ -23,8 +23,9 @@ use crate::chatlist_events;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
||||
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
|
||||
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
|
||||
TIMESTAMP_SENT_TOLERANCE,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
@@ -34,7 +35,7 @@ use crate::download::{
|
||||
};
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
@@ -2946,17 +2947,19 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs WHERE
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutMdnRcvd` inclusive except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26,28) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=?
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
@@ -3125,7 +3128,8 @@ pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<Cha
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns messages belonging to the chat according to the given options.
|
||||
/// Returns messages belonging to the chat according to the given options,
|
||||
/// sorted by oldest message first.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn get_chat_msgs_ex(
|
||||
context: &Context,
|
||||
@@ -3136,6 +3140,7 @@ pub async fn get_chat_msgs_ex(
|
||||
info_only,
|
||||
add_daymarker,
|
||||
} = options;
|
||||
// TODO: Remove `info_only` parameter; it's not used by anything
|
||||
let process_row = if info_only {
|
||||
|row: &rusqlite::Row| {
|
||||
// is_info logic taken from Message.is_info()
|
||||
@@ -4009,9 +4014,47 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if sync.into() {
|
||||
chat.sync_contacts(context).await.log_err(context).ok();
|
||||
}
|
||||
if chat.typ == Chattype::OutBroadcast {
|
||||
resend_last_msgs(context, chat.id, &contact)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn resend_last_msgs(context: &Context, chat_id: ChatId, to_contact: &Contact) -> Result<()> {
|
||||
let msgs: Vec<MsgId> = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=?
|
||||
AND hidden=0
|
||||
AND NOT ( -- Exclude info and system messages
|
||||
param GLOB '*\nS=*' OR param GLOB 'S=*'
|
||||
OR from_id=?
|
||||
OR to_id=?
|
||||
)
|
||||
AND type!=?
|
||||
ORDER BY timestamp DESC, id DESC LIMIT ?",
|
||||
(
|
||||
chat_id,
|
||||
ContactId::INFO,
|
||||
ContactId::INFO,
|
||||
Viewtype::Webxdc,
|
||||
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER,
|
||||
),
|
||||
|row: &rusqlite::Row| Ok(row.get::<_, MsgId>(0)?),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
resend_msgs_ex(context, &msgs, to_contact.fingerprint()).await
|
||||
}
|
||||
|
||||
/// Returns true if an avatar should be attached in the given chat.
|
||||
///
|
||||
/// This function does not check if the avatar is set.
|
||||
@@ -4675,10 +4718,26 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
Ok(msg.rfc724_mid)
|
||||
}
|
||||
|
||||
/// Resends given messages with the same Message-ID.
|
||||
/// Resends given messages to members of the corresponding chats.
|
||||
///
|
||||
/// This is primarily intended to make existing webxdcs available to new chat members.
|
||||
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
resend_msgs_ex(context, msg_ids, None).await
|
||||
}
|
||||
|
||||
/// Resends given messages to a contact with fingerprint `to_fingerprint` or, if it's `None`, to
|
||||
/// members of the corresponding chats.
|
||||
///
|
||||
/// NB: Actually `to_fingerprint` is only passed for `OutBroadcast` chats when a new member is
|
||||
/// added. Regarding webxdcs: It is not trivial to resend only the own status updates,
|
||||
/// and it is not trivial to resend them only to the newly-joined member,
|
||||
/// so that for now, [`resend_last_msgs`] does not automatically resend webxdcs at all.
|
||||
pub(crate) async fn resend_msgs_ex(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
to_fingerprint: Option<Fingerprint>,
|
||||
) -> Result<()> {
|
||||
let to_fingerprint = to_fingerprint.map(|f| f.hex());
|
||||
let mut msgs: Vec<Message> = Vec::new();
|
||||
for msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, *msg_id).await?;
|
||||
@@ -4697,10 +4756,17 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
| MessageState::OutFailed
|
||||
| MessageState::OutDelivered
|
||||
| MessageState::OutMdnRcvd => {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
||||
// Broadcast owners shouldn't see spinners on messages being auto-re-sent to new
|
||||
// subscribers (otherwise big channel owners will see spinners most of the time).
|
||||
if to_fingerprint.is_none() {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
}
|
||||
msg_state => bail!("Unexpected message state {msg_state}"),
|
||||
}
|
||||
if let Some(to_fingerprint) = &to_fingerprint {
|
||||
msg.param.set(Param::Arg4, to_fingerprint.clone());
|
||||
}
|
||||
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -4712,7 +4778,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
||||
// The event only matters if the message is last in the chat.
|
||||
// But it's probably too expensive check, and UIs anyways need to debounce.
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use super::*;
|
||||
use crate::Event;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
@@ -746,7 +746,7 @@ async fn test_leave_group() -> Result<()> {
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InNoticed);
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InSeen);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
@@ -2692,6 +2692,49 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_doesnt_resort_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_grp = create_group(alice, "").await?;
|
||||
let sent1 = alice.send_text(alice_grp, "hi").await;
|
||||
let sent1_ts = Message::load_from_db(alice, sent1.sender_msg_id)
|
||||
.await?
|
||||
.timestamp_sort;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
|
||||
let sent2 = alice
|
||||
.send_text(
|
||||
alice_grp,
|
||||
"Let's test resending, there are very few tests on it",
|
||||
)
|
||||
.await;
|
||||
let resent_msg_id = sent1.sender_msg_id;
|
||||
resend_msgs(alice, &[resent_msg_id]).await?;
|
||||
assert_eq!(
|
||||
resent_msg_id.get_state(alice).await?,
|
||||
MessageState::OutPending
|
||||
);
|
||||
alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
resent_msg_id.get_state(alice).await?,
|
||||
MessageState::OutDelivered
|
||||
);
|
||||
assert_eq!(
|
||||
Message::load_from_db(alice, sent1.sender_msg_id)
|
||||
.await?
|
||||
.timestamp_sort,
|
||||
sent1_ts
|
||||
);
|
||||
assert_eq!(
|
||||
alice.get_last_msg_id_in(alice_grp).await,
|
||||
sent2.sender_msg_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_foreign_message_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -2805,6 +2848,15 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
"alice@example.org charlie@example.net"
|
||||
);
|
||||
|
||||
// Check additionally that subscribers don't send "Chat-Group-Name*" headers.
|
||||
let parsed = alice.parse_msg(&request_with_auth).await;
|
||||
assert!(parsed.get_header(HeaderDef::ChatGroupName).is_none());
|
||||
assert!(
|
||||
parsed
|
||||
.get_header(HeaderDef::ChatGroupNameTimestamp)
|
||||
.is_none()
|
||||
);
|
||||
|
||||
alice.recv_msg_trash(&request_with_auth).await;
|
||||
}
|
||||
|
||||
@@ -2947,6 +2999,56 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let alice_bc_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
|
||||
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let mut alice_msg_ids = Vec::new();
|
||||
for i in 0..(N_MSGS_TO_NEW_BROADCAST_MEMBER + 1) {
|
||||
alice_msg_ids.push(
|
||||
alice
|
||||
.send_text(alice_bc_id, &i.to_string())
|
||||
.await
|
||||
.sender_msg_id,
|
||||
);
|
||||
}
|
||||
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
for msg_id in alice_msg_ids {
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
|
||||
}
|
||||
for i in 0..N_MSGS_TO_NEW_BROADCAST_MEMBER {
|
||||
let rev_order = false;
|
||||
let resent_msg = alice
|
||||
.pop_sent_msg_ex(rev_order, Duration::ZERO)
|
||||
.await
|
||||
.unwrap();
|
||||
let fiona_msg = fiona.recv_msg(&resent_msg).await;
|
||||
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
|
||||
assert_eq!(fiona_msg.text, (i + 1).to_string());
|
||||
assert!(resent_msg.recipients.contains("fiona@example.net"));
|
||||
assert!(!resent_msg.recipients.contains("bob@"));
|
||||
// The message is undecryptable for Bob, he mustn't be able to know yet that somebody joined
|
||||
// the broadcast even if he is a postman in this land. E.g. Fiona may leave after fetching
|
||||
// the news, Bob won't know about that.
|
||||
assert!(
|
||||
MimeMessage::from_bytes(bob, resent_msg.payload().as_bytes())
|
||||
.await?
|
||||
.decryption_error
|
||||
.is_some()
|
||||
);
|
||||
bob.recv_msg_trash(&resent_msg).await;
|
||||
}
|
||||
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// - Alice has multiple devices
|
||||
/// - Alice creates a broadcast and sends a message into it
|
||||
/// - Alice's second device sees the broadcast
|
||||
|
||||
@@ -42,50 +42,85 @@ use crate::{constants, stats};
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Config {
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
|
||||
/// or add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Email address, used in the `From:` field.
|
||||
Addr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server hostname.
|
||||
MailServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server username.
|
||||
MailUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server password.
|
||||
MailPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server port.
|
||||
MailPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server security (e.g. TLS, STARTTLS).
|
||||
MailSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// How to check TLS certificates.
|
||||
///
|
||||
/// "IMAP" in the name is for compatibility,
|
||||
/// this actually applies to both IMAP and SMTP connections.
|
||||
ImapCertificateChecks,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server hostname.
|
||||
SendServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server username.
|
||||
SendUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server password.
|
||||
SendPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server port.
|
||||
SendPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server security (e.g. TLS, STARTTLS).
|
||||
SendSecurity,
|
||||
|
||||
/// Deprecated option for backwards compatibility.
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
|
||||
SmtpCertificateChecks,
|
||||
|
||||
/// Whether to use OAuth 2.
|
||||
///
|
||||
/// Historically contained other bitflags, which are now deprecated.
|
||||
@@ -185,32 +220,47 @@ pub enum Config {
|
||||
/// The primary email address.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// List of configured IMAP servers as a JSON array.
|
||||
ConfiguredImapServers,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server hostname.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailServer,
|
||||
|
||||
/// Configured IMAP server port.
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
/// Configured IMAP server port.
|
||||
ConfiguredMailPort,
|
||||
|
||||
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
||||
ConfiguredMailSecurity,
|
||||
|
||||
/// Configured IMAP server username.
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
/// Configured IMAP server username.
|
||||
ConfiguredMailUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server password.
|
||||
ConfiguredMailPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured TLS certificate checks.
|
||||
/// This option is saved on successful configuration
|
||||
/// and should not be modified manually.
|
||||
@@ -219,37 +269,53 @@ pub enum Config {
|
||||
/// but has "IMAP" in the name for backwards compatibility.
|
||||
ConfiguredImapCertificateChecks,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// List of configured SMTP servers as a JSON array.
|
||||
ConfiguredSmtpServers,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server hostname.
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server port.
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server security (e.g. TLS, STARTTLS).
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server username.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
ConfiguredSendUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server password.
|
||||
ConfiguredSendPw,
|
||||
|
||||
/// Deprecated, stored for backwards compatibility.
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// ConfiguredImapCertificateChecks is actually used.
|
||||
ConfiguredSmtpCertificateChecks,
|
||||
|
||||
/// Whether OAuth 2 is used with configured provider.
|
||||
ConfiguredServerFlags,
|
||||
|
||||
@@ -262,6 +328,9 @@ pub enum Config {
|
||||
/// ID of the configured provider from the provider database.
|
||||
ConfiguredProvider,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use [`Context::is_configured()`] instead.
|
||||
///
|
||||
/// True if account is configured.
|
||||
Configured,
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Context {
|
||||
/// Deprecated since 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_or_update_transport()` instead.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
let mut param = EnteredLoginParam::load(self).await?;
|
||||
let mut param = EnteredLoginParam::load_legacy(self).await?;
|
||||
|
||||
self.add_transport_inner(&mut param).await
|
||||
}
|
||||
@@ -150,7 +150,7 @@ impl Context {
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
bail!(error_msg);
|
||||
} else {
|
||||
param.save(self).await?;
|
||||
param.save_legacy(self).await?;
|
||||
progress!(self, 1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,9 @@ Here is what to do:
|
||||
|
||||
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
|
||||
|
||||
/// How many recent messages should be re-sent to a new broadcast member.
|
||||
pub(crate) const N_MSGS_TO_NEW_BROADCAST_MEMBER: usize = 10;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -284,7 +284,6 @@ async fn test_get_info_completeness() {
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
"socks5_host",
|
||||
|
||||
193
src/imap.rs
193
src/imap.rs
@@ -6,7 +6,7 @@
|
||||
use std::{
|
||||
cmp::max,
|
||||
cmp::min,
|
||||
collections::{BTreeMap, HashMap},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
iter::Peekable,
|
||||
mem::take,
|
||||
sync::atomic::Ordering,
|
||||
@@ -24,7 +24,8 @@ use url::Url;
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::chat::{self, ChatIdBlocked, add_device_msg};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, DC_VERSION_STR};
|
||||
use crate::contact::ContactId;
|
||||
@@ -33,7 +34,7 @@ use crate::ensure_and_debug_assert;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{self, Message};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
@@ -488,7 +489,7 @@ impl Imap {
|
||||
let session = match self.connect(context, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
self.connectivity.set_err(context, &err);
|
||||
self.connectivity.set_err(context, format!("{err:#}"));
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
@@ -1172,6 +1173,114 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes `\Seen` flags using `CONDSTORE` extension.
|
||||
pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
if !self.can_condstore() {
|
||||
info!(
|
||||
context,
|
||||
"Server does not support CONDSTORE, skipping flag synchronization."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.context("Failed to select folder")?;
|
||||
if !folder_exists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mailbox = self
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.with_context(|| format!("No mailbox selected, folder: {folder}"))?;
|
||||
|
||||
// Check if the mailbox supports MODSEQ.
|
||||
// We are not interested in actual value of HIGHESTMODSEQ.
|
||||
if mailbox.highest_modseq.is_none() {
|
||||
info!(
|
||||
context,
|
||||
"Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
let mut updated_chat_ids = BTreeSet::new();
|
||||
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, transport_id, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
|
||||
.await
|
||||
.context("failed to fetch flags")?;
|
||||
|
||||
let mut got_unsolicited_fetch = false;
|
||||
|
||||
while let Some(fetch) = list
|
||||
.try_next()
|
||||
.await
|
||||
.context("failed to get FETCH result")?
|
||||
{
|
||||
let uid = if let Some(uid) = fetch.uid {
|
||||
uid
|
||||
} else {
|
||||
info!(context, "FETCH result contains no UID, skipping");
|
||||
got_unsolicited_fetch = true;
|
||||
continue;
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
if is_seen
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
|
||||
})?
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
}
|
||||
|
||||
if let Some(modseq) = fetch.modseq {
|
||||
if modseq > highest_modseq {
|
||||
highest_modseq = modseq;
|
||||
}
|
||||
} else {
|
||||
warn!(context, "FETCH result contains no MODSEQ");
|
||||
}
|
||||
}
|
||||
drop(list);
|
||||
|
||||
if got_unsolicited_fetch {
|
||||
// We got unsolicited FETCH, which means some flags
|
||||
// have been modified while our request was in progress.
|
||||
// We may or may not have these new flags as a part of the response,
|
||||
// so better skip next IDLE and do another round of flag synchronization.
|
||||
info!(context, "Got unsolicited fetch, will skip idle");
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
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() {
|
||||
context.on_archived_chats_maybe_noticed();
|
||||
}
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches a list of messages by server UID.
|
||||
///
|
||||
/// Sends pairs of UID and info about each downloaded message to the provided channel.
|
||||
@@ -1919,6 +2028,71 @@ pub(crate) async fn prefetch_should_download(
|
||||
Ok(should_download)
|
||||
}
|
||||
|
||||
/// Marks messages in `msgs` table as seen, searching for them by UID.
|
||||
///
|
||||
/// Returns updated chat ID if any message was marked as seen.
|
||||
async fn mark_seen_by_uid(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
uid_validity: u32,
|
||||
uid: u32,
|
||||
) -> Result<Option<ChatId>> {
|
||||
if let Some((msg_id, chat_id)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, chat_id FROM msgs
|
||||
WHERE id > 9 AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM imap
|
||||
WHERE transport_id=?
|
||||
AND folder=?
|
||||
AND uidvalidity=?
|
||||
AND uid=?
|
||||
LIMIT 1
|
||||
)",
|
||||
(transport_id, &folder, uid_validity, uid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let chat_id: ChatId = row.get(1)?;
|
||||
Ok((msg_id, chat_id))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
|
||||
{
|
||||
let updated = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?1
|
||||
WHERE (state=?2 OR state=?3)
|
||||
AND id=?4",
|
||||
(
|
||||
MessageState::InSeen,
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to update msg {msg_id} state"))?
|
||||
> 0;
|
||||
|
||||
if updated {
|
||||
msg_id
|
||||
.start_ephemeral_timer(context)
|
||||
.await
|
||||
.with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
|
||||
Ok(Some(chat_id))
|
||||
} else {
|
||||
// Message state has not changed.
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
// There is no message is `msgs` table matching the given UID.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
|
||||
/// the given Message-ID to `imap_markseen` table.
|
||||
pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
|
||||
@@ -2016,6 +2190,17 @@ pub(crate) async fn set_modseq(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
|
||||
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
|
||||
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
|
||||
|
||||
@@ -17,6 +17,10 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc2087>
|
||||
pub can_check_quota: bool,
|
||||
|
||||
/// True if the server has CONDSTORE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc7162>
|
||||
pub can_condstore: bool,
|
||||
|
||||
/// True if the server has METADATA capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc5464>
|
||||
pub can_metadata: bool,
|
||||
|
||||
@@ -66,6 +66,7 @@ pub(crate) async fn determine_capabilities(
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_condstore: caps.has_str("CONDSTORE"),
|
||||
can_metadata: caps.has_str("METADATA"),
|
||||
can_compress: caps.has_str("COMPRESS=DEFLATE"),
|
||||
can_push: caps.has_str("XDELTAPUSH"),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::Contact;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
|
||||
@@ -65,7 +65,11 @@ impl ImapSession {
|
||||
self.maybe_close_folder(context).await?;
|
||||
|
||||
// select new folder
|
||||
let res = self.select(folder).await;
|
||||
let res = if self.can_condstore() {
|
||||
self.select_condstore(folder).await
|
||||
} else {
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
|
||||
@@ -100,6 +100,10 @@ impl Session {
|
||||
self.capabilities.can_check_quota
|
||||
}
|
||||
|
||||
pub fn can_condstore(&self) -> bool {
|
||||
self.capabilities.can_condstore
|
||||
}
|
||||
|
||||
pub fn can_metadata(&self) -> bool {
|
||||
self.capabilities.can_metadata
|
||||
}
|
||||
|
||||
@@ -138,10 +138,11 @@ pub struct EnteredLoginParam {
|
||||
}
|
||||
|
||||
impl EnteredLoginParam {
|
||||
/// Loads entered account settings.
|
||||
/// Loads entered account settings
|
||||
/// that were set by the deprecated `configured_*` configs.
|
||||
///
|
||||
/// This is a legacy API for loading from separate config parameters.
|
||||
pub(crate) async fn load(context: &Context) -> Result<Self> {
|
||||
/// This is only needed by tests and clients using the old CFFI API.
|
||||
pub(crate) async fn load_legacy(context: &Context) -> Result<Self> {
|
||||
let addr = context
|
||||
.get_config(Config::Addr)
|
||||
.await?
|
||||
@@ -178,7 +179,7 @@ impl EnteredLoginParam {
|
||||
// The setting is named `imap_certificate_checks`
|
||||
// for backwards compatibility,
|
||||
// but now it is a global setting applied to all protocols,
|
||||
// while `smtp_certificate_checks` is ignored.
|
||||
// while `smtp_certificate_checks` has been removed.
|
||||
let certificate_checks = if let Some(certificate_checks) = context
|
||||
.get_config_parsed::<i32>(Config::ImapCertificateChecks)
|
||||
.await?
|
||||
@@ -241,7 +242,10 @@ impl EnteredLoginParam {
|
||||
|
||||
/// Saves entered account settings,
|
||||
/// so that they can be prefilled if the user wants to configure the server again.
|
||||
pub(crate) async fn save(&self, context: &Context) -> Result<()> {
|
||||
///
|
||||
/// This is needed in case a UI is not yet updated, and still uses `get_config("mail_pw")` etc.
|
||||
/// in order to prefill the entered account settings.
|
||||
pub(crate) async fn save_legacy(&self, context: &Context) -> Result<()> {
|
||||
context.set_config(Config::Addr, Some(&self.addr)).await?;
|
||||
|
||||
context
|
||||
@@ -364,7 +368,7 @@ mod tests {
|
||||
.await?;
|
||||
t.set_config(Config::MailPw, Some("foobarbaz")).await?;
|
||||
|
||||
let param = EnteredLoginParam::load(t).await?;
|
||||
let param = EnteredLoginParam::load_legacy(t).await?;
|
||||
assert_eq!(param.addr, "alice@example.org");
|
||||
assert_eq!(
|
||||
param.certificate_checks,
|
||||
@@ -373,13 +377,13 @@ mod tests {
|
||||
|
||||
t.set_config(Config::ImapCertificateChecks, Some("1"))
|
||||
.await?;
|
||||
let param = EnteredLoginParam::load(t).await?;
|
||||
let param = EnteredLoginParam::load_legacy(t).await?;
|
||||
assert_eq!(param.certificate_checks, EnteredCertificateChecks::Strict);
|
||||
|
||||
// Fail to load invalid settings, but do not panic.
|
||||
t.set_config(Config::ImapCertificateChecks, Some("999"))
|
||||
.await?;
|
||||
assert!(EnteredLoginParam::load(t).await.is_err());
|
||||
assert!(EnteredLoginParam::load_legacy(t).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -407,7 +411,7 @@ mod tests {
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
};
|
||||
param.save(&t).await?;
|
||||
param.save_legacy(&t).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
@@ -416,7 +420,7 @@ mod tests {
|
||||
assert_eq!(t.get_config(Config::SendPw).await?, None);
|
||||
assert_eq!(t.get_config_int(Config::SendPort).await?, 2947);
|
||||
|
||||
assert_eq!(EnteredLoginParam::load(&t).await?, param);
|
||||
assert_eq!(EnteredLoginParam::load_legacy(&t).await?, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ fn new_address_with_name(name: &str, address: String) -> Address<'static> {
|
||||
}
|
||||
|
||||
impl MimeFactory {
|
||||
/// Returns `MimeFactory` for rendering `msg`.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
||||
let now = time();
|
||||
@@ -1394,10 +1395,7 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
{
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||
headers.push((
|
||||
"Chat-Group-Name",
|
||||
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
|
||||
@@ -1408,7 +1406,11 @@ impl MimeFactory {
|
||||
mail_builder::headers::text::Text::new(ts.to_string()).into(),
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
{
|
||||
match command {
|
||||
SystemMessage::MemberRemovedFromGroup => {
|
||||
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
@@ -2225,18 +2227,18 @@ fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool {
|
||||
/// rather than all recipients.
|
||||
/// This function returns the fingerprint of the recipient the message should be sent to.
|
||||
fn must_have_only_one_recipient<'a>(msg: &'a Message, chat: &Chat) -> Option<Result<&'a str>> {
|
||||
if chat.typ == Chattype::OutBroadcast
|
||||
&& matches!(
|
||||
msg.param.get_cmd(),
|
||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
|
||||
)
|
||||
{
|
||||
let Some(fp) = msg.param.get(Param::Arg4) else {
|
||||
return Some(Err(format_err!("Missing removed/added member")));
|
||||
};
|
||||
return Some(Ok(fp));
|
||||
if chat.typ != Chattype::OutBroadcast {
|
||||
None
|
||||
} else if let Some(fp) = msg.param.get(Param::Arg4) {
|
||||
Some(Ok(fp))
|
||||
} else if matches!(
|
||||
msg.param.get_cmd(),
|
||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
|
||||
) {
|
||||
Some(Err(format_err!("Missing removed/added member")))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
|
||||
|
||||
@@ -136,6 +136,10 @@ pub enum Param {
|
||||
/// For "MemberAddedToGroup" and "MemberRemovedFromGroup",
|
||||
/// this is the fingerprint added to / removed from the group.
|
||||
///
|
||||
/// For messages resent when adding a new member to a broadcast channel,
|
||||
/// this is the fingerprint of the added member;
|
||||
/// the message must only be sent to this one member then.
|
||||
///
|
||||
/// For call messages, this is the end timsetamp.
|
||||
Arg4 = b'H',
|
||||
|
||||
|
||||
@@ -234,34 +234,6 @@ static P_BLUEWIN_CH: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// buzon.uy.md: buzon.uy
|
||||
static P_BUZON_UY: Provider = Provider {
|
||||
id: "buzon.uy",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/buzon-uy",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "mail.buzon.uy",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "mail.buzon.uy",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// chello.at.md: chello.at
|
||||
static P_CHELLO_AT: Provider = Provider {
|
||||
id: "chello.at",
|
||||
@@ -303,48 +275,6 @@ static P_COMCAST: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// daleth.cafe.md: daleth.cafe
|
||||
static P_DALETH_CAFE: Provider = Provider {
|
||||
id: "daleth.cafe",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/daleth-cafe",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "daleth.cafe",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "daleth.cafe",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "daleth.cafe",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "daleth.cafe",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// dismail.de.md: dismail.de
|
||||
static P_DISMAIL_DE: Provider = Provider {
|
||||
id: "dismail.de",
|
||||
@@ -496,22 +426,6 @@ static P_FIREMAIL_DE: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// five.chat.md: five.chat
|
||||
static P_FIVE_CHAT: Provider = Provider {
|
||||
id: "five.chat",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/five-chat",
|
||||
server: &[],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// freenet.de.md: freenet.de
|
||||
static P_FREENET_DE: Provider = Provider {
|
||||
id: "freenet.de",
|
||||
@@ -629,16 +543,10 @@ static P_HERMES_RADIO: Provider = Provider {
|
||||
strict_tls: false,
|
||||
..ProviderOptions::new()
|
||||
},
|
||||
config_defaults: Some(&[
|
||||
ConfigDefault {
|
||||
key: Config::MdnsEnabled,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::ShowEmails,
|
||||
value: "2",
|
||||
},
|
||||
]),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MdnsEnabled,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -919,90 +827,6 @@ static P_MAILO_COM: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// mehl.cloud.md: mehl.cloud
|
||||
static P_MEHL_CLOUD: Provider = Provider {
|
||||
id: "mehl.cloud",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mehl-cloud",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 443,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 443,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "mehl.cloud",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "mehl.cloud",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// mehl.store.md: mehl.store, ende.in.net, l2i.top, szh.homes, sls.post.in, ente.quest, ente.cfd, nein.jetzt
|
||||
static P_MEHL_STORE: Provider = Provider {
|
||||
id: "mehl.store",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "This account provides 3GB storage for eMails and the possibility to access a NEXTCLOUD-instance by using the email-credits!",
|
||||
overview_page: "https://providers.delta.chat/mehl-store",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mail.ende.in.net",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "mail.ende.in.net",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// migadu.md: migadu.com
|
||||
static P_MIGADU: Provider = Provider {
|
||||
id: "migadu",
|
||||
@@ -1250,8 +1074,8 @@ static P_OUVATON_COOP: Provider = Provider {
|
||||
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
|
||||
static P_POSTEO: Provider = Provider {
|
||||
id: "posteo",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "You must create an app-specific password before you can log in.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/posteo",
|
||||
server: &[
|
||||
@@ -1562,51 +1386,6 @@ static P_T_ONLINE: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// testrun.md: testrun.org
|
||||
static P_TESTRUN: Provider = Provider {
|
||||
id: "testrun",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/testrun",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "testrun.org",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "testrun.org",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// tiscali.it.md: tiscali.it
|
||||
static P_TISCALI_IT: Provider = Provider {
|
||||
id: "tiscali.it",
|
||||
@@ -2004,7 +1783,7 @@ static P_ZOHO: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
|
||||
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
|
||||
("163.com", &P_163),
|
||||
("aktivix.org", &P_AKTIVIX_ORG),
|
||||
("aliyun.com", &P_ALIYUN),
|
||||
@@ -2014,11 +1793,9 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
|
||||
("delta.blinzeln.de", &P_BLINDZELN_ORG),
|
||||
("delta.blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("xfinity.com", &P_COMCAST),
|
||||
("comcast.net", &P_COMCAST),
|
||||
("daleth.cafe", &P_DALETH_CAFE),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
("disroot.org", &P_DISROOT),
|
||||
("e.email", &P_E_EMAIL),
|
||||
@@ -2145,7 +1922,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
|
||||
("your-mail.com", &P_FASTMAIL),
|
||||
("firemail.at", &P_FIREMAIL_DE),
|
||||
("firemail.de", &P_FIREMAIL_DE),
|
||||
("five.chat", &P_FIVE_CHAT),
|
||||
("freenet.de", &P_FREENET_DE),
|
||||
("gmail.com", &P_GMAIL),
|
||||
("googlemail.com", &P_GMAIL),
|
||||
@@ -2368,15 +2144,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
|
||||
("mailbox.org", &P_MAILBOX_ORG),
|
||||
("secure.mailbox.org", &P_MAILBOX_ORG),
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("mehl.cloud", &P_MEHL_CLOUD),
|
||||
("mehl.store", &P_MEHL_STORE),
|
||||
("ende.in.net", &P_MEHL_STORE),
|
||||
("l2i.top", &P_MEHL_STORE),
|
||||
("szh.homes", &P_MEHL_STORE),
|
||||
("sls.post.in", &P_MEHL_STORE),
|
||||
("ente.quest", &P_MEHL_STORE),
|
||||
("ente.cfd", &P_MEHL_STORE),
|
||||
("nein.jetzt", &P_MEHL_STORE),
|
||||
("migadu.com", &P_MIGADU),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver.com", &P_NAVER),
|
||||
@@ -2469,7 +2236,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
|
||||
("systemli.org", &P_SYSTEMLI_ORG),
|
||||
("t-online.de", &P_T_ONLINE),
|
||||
("magenta.de", &P_T_ONLINE),
|
||||
("testrun.org", &P_TESTRUN),
|
||||
("tiscali.it", &P_TISCALI_IT),
|
||||
("tutanota.com", &P_TUTANOTA),
|
||||
("tutanota.de", &P_TUTANOTA),
|
||||
@@ -2552,10 +2318,8 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
("autistici.org", &P_AUTISTICI_ORG),
|
||||
("blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("comcast", &P_COMCAST),
|
||||
("daleth.cafe", &P_DALETH_CAFE),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
("disroot", &P_DISROOT),
|
||||
("e.email", &P_E_EMAIL),
|
||||
@@ -2563,7 +2327,6 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
("example.com", &P_EXAMPLE_COM),
|
||||
("fastmail", &P_FASTMAIL),
|
||||
("firemail.de", &P_FIREMAIL_DE),
|
||||
("five.chat", &P_FIVE_CHAT),
|
||||
("freenet.de", &P_FREENET_DE),
|
||||
("gmail", &P_GMAIL),
|
||||
("gmx.net", &P_GMX_NET),
|
||||
@@ -2581,8 +2344,6 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
("mail2tor", &P_MAIL2TOR),
|
||||
("mailbox.org", &P_MAILBOX_ORG),
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("mehl.cloud", &P_MEHL_CLOUD),
|
||||
("mehl.store", &P_MEHL_STORE),
|
||||
("migadu", &P_MIGADU),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver", &P_NAVER),
|
||||
@@ -2602,7 +2363,6 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
|
||||
("systemli.org", &P_SYSTEMLI_ORG),
|
||||
("t-online", &P_T_ONLINE),
|
||||
("testrun", &P_TESTRUN),
|
||||
("tiscali.it", &P_TISCALI_IT),
|
||||
("tutanota", &P_TUTANOTA),
|
||||
("ukr.net", &P_UKR_NET),
|
||||
@@ -2622,4 +2382,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 1, 28).unwrap());
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 4, 21).unwrap());
|
||||
|
||||
@@ -709,7 +709,7 @@ async fn test_decode_dclogin_advanced_options() -> Result<()> {
|
||||
assert_eq!(param.smtp.security, Socket::Plain);
|
||||
|
||||
// `sc` option is actually ignored and `ic` is used instead
|
||||
// because `smtp_certificate_checks` is deprecated.
|
||||
// because `smtp_certificate_checks` has been removed.
|
||||
assert_eq!(param.certificate_checks, EnteredCertificateChecks::Strict);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1913,11 +1913,14 @@ async fn add_parts(
|
||||
|
||||
let state = if !mime_parser.incoming {
|
||||
MessageState::OutDelivered
|
||||
} else if seen || !mime_parser.mdn_reports.is_empty() || chat_id_blocked == Blocked::Yes
|
||||
} else if seen
|
||||
|| !mime_parser.mdn_reports.is_empty()
|
||||
|| chat_id_blocked == Blocked::Yes
|
||||
|| group_changes.silent
|
||||
// No check for `hidden` because only reactions are such and they should be `InFresh`.
|
||||
{
|
||||
MessageState::InSeen
|
||||
} else if mime_parser.from.addr == STATISTICS_BOT_EMAIL || group_changes.silent {
|
||||
} else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
|
||||
MessageState::InNoticed
|
||||
} else {
|
||||
MessageState::InFresh
|
||||
|
||||
@@ -533,6 +533,14 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session)
|
||||
.await
|
||||
.context("download_msgs")?;
|
||||
|
||||
// Synchronize Seen flags.
|
||||
session
|
||||
.sync_seen_flags(ctx, &watch_folder)
|
||||
.await
|
||||
.context("sync_seen_flags")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
|
||||
connection.connectivity.set_idle(ctx);
|
||||
|
||||
ctx.emit_event(EventType::ImapInboxIdle);
|
||||
@@ -623,7 +631,7 @@ async fn smtp_loop(
|
||||
info!(ctx, "SMTP fake idle started.");
|
||||
match &connection.last_send_error {
|
||||
None => connection.connectivity.set_idle(&ctx),
|
||||
Some(err) => connection.connectivity.set_err(&ctx, err),
|
||||
Some(err) => connection.connectivity.set_err(&ctx, err.clone()),
|
||||
}
|
||||
|
||||
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that
|
||||
|
||||
@@ -157,8 +157,8 @@ impl ConnectivityStore {
|
||||
context.emit_event(EventType::ConnectivityChanged);
|
||||
}
|
||||
|
||||
pub(crate) fn set_err(&self, context: &Context, e: impl ToString) {
|
||||
self.set(context, DetailedConnectivity::Error(e.to_string()));
|
||||
pub(crate) fn set_err(&self, context: &Context, e: String) {
|
||||
self.set(context, DetailedConnectivity::Error(e));
|
||||
}
|
||||
pub(crate) fn set_connecting(&self, context: &Context) {
|
||||
self.set(context, DetailedConnectivity::Connecting);
|
||||
|
||||
@@ -1919,7 +1919,7 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
inc_and_check(&mut migration_version, 131)?;
|
||||
if dbversion < migration_version {
|
||||
let entered_param = EnteredLoginParam::load(context).await?;
|
||||
let entered_param = EnteredLoginParam::load_legacy(context).await?;
|
||||
let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
|
||||
|
||||
sql.execute_migration_transaction(
|
||||
|
||||
@@ -275,16 +275,17 @@ impl TestContextManager {
|
||||
|
||||
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
|
||||
|
||||
loop {
|
||||
for _ in 0..2 {
|
||||
let mut something_sent = false;
|
||||
if let Some(sent) = joiner.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
let rev_order = false;
|
||||
if let Some(sent) = joiner.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
|
||||
for inviter in inviters {
|
||||
inviter.recv_msg_opt(&sent).await;
|
||||
}
|
||||
something_sent = true;
|
||||
}
|
||||
for inviter in inviters {
|
||||
if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
if let Some(sent) = inviter.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
|
||||
joiner.recv_msg_opt(&sent).await;
|
||||
something_sent = true;
|
||||
}
|
||||
@@ -623,25 +624,35 @@ impl TestContext {
|
||||
}
|
||||
|
||||
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
|
||||
let rev_order = true;
|
||||
self.pop_sent_msg_ex(rev_order, timeout).await
|
||||
}
|
||||
|
||||
pub async fn pop_sent_msg_ex(
|
||||
&self,
|
||||
rev_order: bool,
|
||||
timeout: Duration,
|
||||
) -> Option<SentMessage<'_>> {
|
||||
let start = Instant::now();
|
||||
let mut query = "
|
||||
SELECT id, msg_id, mime, recipients
|
||||
FROM smtp
|
||||
ORDER BY id"
|
||||
.to_string();
|
||||
if rev_order {
|
||||
query += " DESC";
|
||||
}
|
||||
let (rowid, msg_id, payload, recipients) = loop {
|
||||
let row = self
|
||||
.ctx
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT id, msg_id, mime, recipients
|
||||
FROM smtp
|
||||
ORDER BY id DESC"#,
|
||||
(),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
let recipients: String = row.get(3)?;
|
||||
Ok((rowid, msg_id, mime, recipients))
|
||||
},
|
||||
)
|
||||
.query_row_optional(&query, (), |row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
let recipients: String = row.get(3)?;
|
||||
Ok((rowid, msg_id, mime, recipients))
|
||||
})
|
||||
.await
|
||||
.expect("query_row_optional failed");
|
||||
if let Some(row) = row {
|
||||
@@ -823,17 +834,24 @@ impl TestContext {
|
||||
assert_eq!(received.chat_id, DC_CHAT_ID_TRASH);
|
||||
}
|
||||
|
||||
/// Gets the most recent message ID of a chat.
|
||||
///
|
||||
/// Panics on errors or if the most recent message is a marker.
|
||||
pub async fn get_last_msg_id_in(&self, chat_id: ChatId) -> MsgId {
|
||||
let msgs = chat::get_chat_msgs(&self.ctx, chat_id).await.unwrap();
|
||||
if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
*msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the most recent message of a chat.
|
||||
///
|
||||
/// Panics on errors or if the most recent message is a marker.
|
||||
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
|
||||
let msgs = chat::get_chat_msgs(&self.ctx, chat_id).await.unwrap();
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
|
||||
let msg_id = self.get_last_msg_id_in(chat_id).await;
|
||||
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Gets the most recent message over all chats.
|
||||
|
||||
@@ -118,8 +118,6 @@ async fn test_posteo_alias() -> Result<()> {
|
||||
t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
@@ -207,8 +205,6 @@ async fn test_empty_server_list_legacy() -> Result<()> {
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Filename encoding | Encoded Words ([RFC 2047][]), Encoded Word Ex
|
||||
Identify server folders | IMAP LIST Extension ([RFC 6154][])
|
||||
Push | IMAP IDLE ([RFC 2177][])
|
||||
Quota | IMAP QUOTA extension ([RFC 2087][])
|
||||
Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162][])
|
||||
Client/server identification | IMAP ID extension ([RFC 2971][])
|
||||
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://datatracker.ietf.org/doc/html/draft-dkg-openpgp-pgpmime-message-mangling-00)
|
||||
|
||||
Reference in New Issue
Block a user