mirror of
https://github.com/chatmail/core.git
synced 2026-05-17 05:46:30 +03:00
feat: Remove "Delete Messages from Server" (delete_server_after) config option, second try
This commit is contained in:
@@ -413,11 +413,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||||
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
||||||
* See also dc_estimate_deletion_cnt().
|
* See also dc_estimate_deletion_cnt().
|
||||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
|
||||||
* 1=delete messages directly after receiving from server, mvbox is skipped.
|
|
||||||
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
|
|
||||||
* "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out.
|
|
||||||
* See also dc_estimate_deletion_cnt().
|
|
||||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||||
* DC_MEDIA_QUALITY_WORSE (1)
|
* DC_MEDIA_QUALITY_WORSE (1)
|
||||||
@@ -1461,16 +1456,16 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate the number of messages that will be deleted
|
* Estimate the number of messages that will be deleted
|
||||||
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
* by the dc_set_config()-option `delete_device_after`.
|
||||||
* This is typically used to show the estimated impact to the user
|
* This is typically used to show the estimated impact to the user
|
||||||
* before actually enabling deletion of old messages.
|
* before actually enabling deletion of old messages.
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object as returned from dc_context_new().
|
* @param context The context object as returned from dc_context_new().
|
||||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
* @param from_server Deprecated, pass 0 here
|
||||||
* @param seconds Count messages older than the given number of seconds.
|
* @param seconds Count messages older than the given number of seconds.
|
||||||
* @return Number of messages that are older than the given number of seconds.
|
* @return Number of messages that are older than the given number of seconds.
|
||||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
* Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||||
*/
|
*/
|
||||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||||
|
|
||||||
|
|||||||
@@ -735,10 +735,19 @@ impl CommandApi {
|
|||||||
Ok(msg_ids)
|
Ok(msg_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estimate the number of messages that will be deleted
|
/// Estimates the number of messages that will be deleted
|
||||||
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
/// by the `set_config()`-option `delete_device_after`.
|
||||||
|
///
|
||||||
/// This is typically used to show the estimated impact to the user
|
/// This is typically used to show the estimated impact to the user
|
||||||
/// before actually enabling deletion of old messages.
|
/// before actually enabling deletion of old messages.
|
||||||
|
///
|
||||||
|
/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - `from_server`: Deprecated, pass `false` here
|
||||||
|
/// - `seconds`: Count messages older than the given number of seconds.
|
||||||
|
///
|
||||||
|
/// Returns the number of messages that are older than the given number of seconds.
|
||||||
async fn estimate_auto_deletion_count(
|
async fn estimate_auto_deletion_count(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
|||||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||||
ac2.bring_online()
|
ac2.bring_online()
|
||||||
|
|
||||||
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
|
ac1.set_config("bcc_self", "1")
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
|
|
||||||
log.section("ac2: creating DeltaChat folder")
|
log.section("ac2: creating DeltaChat folder")
|
||||||
ac2_direct_imap = direct_imap(ac2)
|
ac2_direct_imap = direct_imap(ac2)
|
||||||
ac2_direct_imap.create_folder("DeltaChat")
|
ac2_direct_imap.create_folder("DeltaChat")
|
||||||
ac2.set_config("delete_server_after", "0")
|
|
||||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||||
|
|
||||||
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
||||||
@@ -57,11 +60,9 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
|||||||
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
for ac in ac1, ac2:
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
ac.set_config("delete_server_after", "0")
|
ac1.set_config("bcc_self", "1")
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
|
||||||
ac1.set_config("bcc_self", "0")
|
|
||||||
|
|
||||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||||
msg = ac2.wait_for_incoming_msg()
|
msg = ac2.wait_for_incoming_msg()
|
||||||
@@ -81,17 +82,18 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
|||||||
ac1_direct_imap.select_folder("INBOX")
|
ac1_direct_imap.select_folder("INBOX")
|
||||||
ac2_direct_imap.select_folder("INBOX")
|
ac2_direct_imap.select_folder("INBOX")
|
||||||
|
|
||||||
# Check that the mdn is marked as seen
|
# Check that the mdn and original message is marked as seen
|
||||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||||
# Check original message is marked as seen
|
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac2.stop_io()
|
ac2.stop_io()
|
||||||
|
|
||||||
ac2.set_config("delete_server_after", "0")
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
|
|
||||||
ac2.set_config("sync_msgs", "0")
|
ac2.set_config("sync_msgs", "0")
|
||||||
|
|
||||||
ac2.start_io()
|
ac2.start_io()
|
||||||
|
|||||||
@@ -4,39 +4,29 @@ from deltachat_rpc_client import EventType
|
|||||||
from deltachat_rpc_client.const import MessageState
|
from deltachat_rpc_client.const import MessageState
|
||||||
|
|
||||||
|
|
||||||
def test_bcc_self_delete_server_after_defaults(acfactory):
|
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
|
||||||
"""Test default values for bcc_self and delete_server_after."""
|
|
||||||
ac = acfactory.get_online_account()
|
ac = acfactory.get_online_account()
|
||||||
|
|
||||||
# Initially after getting online
|
# Initially after getting online
|
||||||
# the setting bcc_self is set to 0 because there is only one device
|
# the setting bcc_self is set to 0 because there is only one device
|
||||||
# and delete_server_after is "1", meaning immediate deletion.
|
|
||||||
assert ac.get_config("bcc_self") == "0"
|
assert ac.get_config("bcc_self") == "0"
|
||||||
assert ac.get_config("delete_server_after") == "1"
|
|
||||||
|
|
||||||
# Setup a second device.
|
# Setup a second device.
|
||||||
ac_clone = ac.clone()
|
ac_clone = ac.clone()
|
||||||
ac_clone.bring_online()
|
ac_clone.bring_online()
|
||||||
|
|
||||||
# Second device setup
|
# Second device setup enables bcc_self.
|
||||||
# enables bcc_self and changes default delete_server_after.
|
|
||||||
assert ac.get_config("bcc_self") == "1"
|
assert ac.get_config("bcc_self") == "1"
|
||||||
assert ac.get_config("delete_server_after") == "0"
|
|
||||||
|
|
||||||
assert ac_clone.get_config("bcc_self") == "1"
|
assert ac_clone.get_config("bcc_self") == "1"
|
||||||
assert ac_clone.get_config("delete_server_after") == "0"
|
|
||||||
|
|
||||||
# Manually disabling bcc_self
|
# Test manually disabling bcc_self
|
||||||
# also restores the default for delete_server_after.
|
|
||||||
ac.set_config("bcc_self", "0")
|
ac.set_config("bcc_self", "0")
|
||||||
assert ac.get_config("bcc_self") == "0"
|
assert ac.get_config("bcc_self") == "0"
|
||||||
assert ac.get_config("delete_server_after") == "1"
|
|
||||||
|
|
||||||
# Cloning the account again enables bcc_self
|
# Cloning the account again enables bcc_self again
|
||||||
# even though it was manually disabled.
|
# even though it was manually disabled.
|
||||||
ac_clone = ac.clone()
|
ac_clone = ac.clone()
|
||||||
assert ac.get_config("bcc_self") == "1"
|
assert ac.get_config("bcc_self") == "1"
|
||||||
assert ac.get_config("delete_server_after") == "0"
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||||
|
|||||||
@@ -1232,11 +1232,12 @@ def test_leave_and_delete_group(acfactory, log):
|
|||||||
|
|
||||||
|
|
||||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||||
|
"""
|
||||||
|
`bcc_self` is off by default,
|
||||||
|
so that messages are supposed to be immediately autodeleted
|
||||||
|
"""
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# "1" means delete immediately, while "0" means do not delete
|
|
||||||
ac2.set_config("delete_server_after", "1")
|
|
||||||
|
|
||||||
log.section("ac1: create chat with ac2")
|
log.section("ac1: create chat with ac2")
|
||||||
chat1 = ac1.create_chat(ac2)
|
chat1 = ac1.create_chat(ac2)
|
||||||
ac2.create_chat(ac1)
|
ac2.create_chat(ac1)
|
||||||
|
|||||||
@@ -521,7 +521,6 @@ class ACFactory:
|
|||||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||||
configdict.setdefault("bcc_self", False)
|
configdict.setdefault("bcc_self", False)
|
||||||
configdict.setdefault("sync_msgs", False)
|
configdict.setdefault("sync_msgs", False)
|
||||||
configdict.setdefault("delete_server_after", 0)
|
|
||||||
ac.update_config(configdict)
|
ac.update_config(configdict)
|
||||||
self._acsetup._account2config[ac] = configdict
|
self._acsetup._account2config[ac] = configdict
|
||||||
self._preconfigure_key(ac)
|
self._preconfigure_key(ac)
|
||||||
|
|||||||
@@ -298,73 +298,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
|||||||
assert msg_in.text == msg_out.text
|
assert msg_in.text == msg_out.text
|
||||||
|
|
||||||
|
|
||||||
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
|
||||||
"""Test for the issue #4346:
|
|
||||||
- User is added to a verified group.
|
|
||||||
- First device of the user downloads "member added" from the group.
|
|
||||||
- First device removes "member added" from the server.
|
|
||||||
- Some new messages are sent to the group.
|
|
||||||
- Second device comes online, receives these new messages.
|
|
||||||
The result is an unverified group with unverified members.
|
|
||||||
- First device re-gossips Autocrypt keys to the group.
|
|
||||||
- Now the second device has all members and group verified.
|
|
||||||
"""
|
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
|
||||||
acfactory.remove_preconfigured_keys()
|
|
||||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
|
||||||
for ac in [ac2, ac2_offl]:
|
|
||||||
ac.set_config("bcc_self", "1")
|
|
||||||
ac2.set_config("delete_server_after", "1")
|
|
||||||
ac2.set_config("gossip_period", "0") # Re-gossip in every message
|
|
||||||
acfactory.bring_accounts_online()
|
|
||||||
dir = tmp_path / "exportdir"
|
|
||||||
dir.mkdir()
|
|
||||||
ac2.export_self_keys(str(dir))
|
|
||||||
ac2_offl.import_self_keys(str(dir))
|
|
||||||
ac2_offl.stop_io()
|
|
||||||
|
|
||||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
|
||||||
chat1 = ac1.create_group_chat("hello")
|
|
||||||
qr = chat1.get_join_qr()
|
|
||||||
lp.sec("ac2: start QR-code based join-group protocol")
|
|
||||||
chat2 = ac2.qr_join_chat(qr)
|
|
||||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
|
||||||
# Wait for "Member Me (<addr>) added by <addr>." message.
|
|
||||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
|
||||||
assert msg_in.is_system_message()
|
|
||||||
|
|
||||||
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
|
|
||||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
|
||||||
|
|
||||||
lp.sec("ac1: sending 'hi' to the group")
|
|
||||||
ac2.set_config("delete_server_after", "0")
|
|
||||||
chat1.send_text("hi")
|
|
||||||
|
|
||||||
lp.sec("ac2_offl: going online, checking the 'hi' message")
|
|
||||||
ac2_offl.start_io()
|
|
||||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
|
||||||
assert not msg_in.is_system_message()
|
|
||||||
assert msg_in.text == "hi"
|
|
||||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
|
||||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
|
||||||
assert not ac2_offl_ac1_contact.is_verified()
|
|
||||||
chat2_offl = msg_in.chat
|
|
||||||
|
|
||||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
|
||||||
chat2.send_text("hi2")
|
|
||||||
|
|
||||||
lp.sec("ac2_offl: receiving message")
|
|
||||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
|
||||||
assert not msg_in.is_system_message()
|
|
||||||
assert msg_in.text == "hi2"
|
|
||||||
assert msg_in.chat == chat2_offl
|
|
||||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
|
||||||
# Until we reset verifications and then send the _verified header,
|
|
||||||
# verification is not gossiped here:
|
|
||||||
assert not ac2_offl_ac1_contact.is_verified()
|
|
||||||
|
|
||||||
|
|
||||||
def test_deleted_msgs_dont_reappear(acfactory):
|
def test_deleted_msgs_dont_reappear(acfactory):
|
||||||
ac1 = acfactory.new_online_configuring_account()
|
ac1 = acfactory.new_online_configuring_account()
|
||||||
acfactory.bring_accounts_online()
|
acfactory.bring_accounts_online()
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ def test_basic_imap_api(acfactory, tmp_path):
|
|||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
|
||||||
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
|
|
||||||
imap2 = ac2.direct_imap
|
imap2 = ac2.direct_imap
|
||||||
|
|
||||||
with imap2.idle() as idle2:
|
with imap2.idle() as idle2:
|
||||||
@@ -162,6 +165,9 @@ def test_webxdc_message(acfactory, data, lp):
|
|||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
|
||||||
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
|
|
||||||
lp.sec("ac1: prepare and send text message to ac2")
|
lp.sec("ac1: prepare and send text message to ac2")
|
||||||
msg1 = chat.send_text("message0")
|
msg1 = chat.send_text("message0")
|
||||||
assert not msg1.is_webxdc()
|
assert not msg1.is_webxdc()
|
||||||
@@ -362,6 +368,10 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
|||||||
# make DC's life harder wrt to encodings
|
# make DC's life harder wrt to encodings
|
||||||
ac1.set_config("displayname", "ä name")
|
ac1.set_config("displayname", "ä name")
|
||||||
|
|
||||||
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
|
ac1.set_config("bcc_self", "1")
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
|
|
||||||
# clear any fresh device messages
|
# clear any fresh device messages
|
||||||
ac1.get_device_chat().mark_noticed()
|
ac1.get_device_chat().mark_noticed()
|
||||||
ac2.get_device_chat().mark_noticed()
|
ac2.get_device_chat().mark_noticed()
|
||||||
@@ -506,9 +516,15 @@ def test_mdn_asymmetric(acfactory, lp):
|
|||||||
ac1.set_config("mdns_enabled", "1")
|
ac1.set_config("mdns_enabled", "1")
|
||||||
ac2.set_config("mdns_enabled", "1")
|
ac2.set_config("mdns_enabled", "1")
|
||||||
|
|
||||||
|
# Make sure that the mdn is not immediately auto-deleted on the server:
|
||||||
|
ac1.set_config("bcc_self", "1")
|
||||||
|
|
||||||
lp.sec("sending text message from ac1 to ac2")
|
lp.sec("sending text message from ac1 to ac2")
|
||||||
msg_out = chat.send_text("message1")
|
msg_out = chat.send_text("message1")
|
||||||
|
|
||||||
|
# Wait for the message to be marked as seen on IMAP.
|
||||||
|
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||||
|
|
||||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||||
|
|
||||||
lp.sec("disable ac1 MDNs")
|
lp.sec("disable ac1 MDNs")
|
||||||
@@ -525,7 +541,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
|||||||
lp.sec("ac1: waiting for incoming activity")
|
lp.sec("ac1: waiting for incoming activity")
|
||||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||||
|
|
||||||
# Wait for the message to be marked as seen on IMAP.
|
# Wait for the mdn to be marked as seen on IMAP.
|
||||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||||
|
|
||||||
# MDN is received even though MDNs are already disabled
|
# MDN is received even though MDNs are already disabled
|
||||||
@@ -1073,6 +1089,8 @@ def test_send_receive_locations(acfactory, lp):
|
|||||||
|
|
||||||
def test_delete_multiple_messages(acfactory, lp):
|
def test_delete_multiple_messages(acfactory, lp):
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
# Make sure that messages are not immediately auto-deleted on the server:
|
||||||
|
ac2.set_config("bcc_self", "1")
|
||||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
|
||||||
lp.sec("ac1: sending seven messages")
|
lp.sec("ac1: sending seven messages")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
|||||||
export TZ=UTC
|
export TZ=UTC
|
||||||
|
|
||||||
# Provider database revision.
|
# Provider database revision.
|
||||||
REV=ad097ee40579c884e7757de2d3bb0a51f481a32a
|
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
|
||||||
|
|
||||||
CORE_ROOT="$PWD"
|
CORE_ROOT="$PWD"
|
||||||
TMP="$(mktemp -d)"
|
TMP="$(mktemp -d)"
|
||||||
|
|||||||
@@ -194,17 +194,6 @@ pub enum Config {
|
|||||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||||
MediaQuality,
|
MediaQuality,
|
||||||
|
|
||||||
/// Timer in seconds after which the message is deleted from the
|
|
||||||
/// server.
|
|
||||||
///
|
|
||||||
/// 0 means messages are never deleted by Delta Chat.
|
|
||||||
///
|
|
||||||
/// Value 1 is treated as "delete at once": messages are deleted
|
|
||||||
/// immediately, without moving to DeltaChat folder.
|
|
||||||
///
|
|
||||||
/// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
|
|
||||||
DeleteServerAfter,
|
|
||||||
|
|
||||||
/// Timer in seconds after which the message is deleted from the
|
/// Timer in seconds after which the message is deleted from the
|
||||||
/// device.
|
/// device.
|
||||||
///
|
///
|
||||||
@@ -554,14 +543,6 @@ impl Context {
|
|||||||
// Default values
|
// Default values
|
||||||
let val = match key {
|
let val = match key {
|
||||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||||
Config::DeleteServerAfter => {
|
|
||||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
|
||||||
&& Box::pin(self.is_chatmail()).await?
|
|
||||||
{
|
|
||||||
true => Some("1".to_string()),
|
|
||||||
false => Some("0".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
|
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
|
||||||
_ => key.get_str("default").map(|s| s.to_string()),
|
_ => key.get_str("default").map(|s| s.to_string()),
|
||||||
};
|
};
|
||||||
@@ -642,23 +623,6 @@ impl Context {
|
|||||||
self.get_config_bool(Config::MdnsEnabled).await
|
self.get_config_bool(Config::MdnsEnabled).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets configured "delete_server_after" value.
|
|
||||||
///
|
|
||||||
/// `None` means never delete the message, `Some(0)` means delete
|
|
||||||
/// at once, `Some(x)` means delete after `x` seconds.
|
|
||||||
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
|
|
||||||
let val = match self
|
|
||||||
.get_config_parsed::<i64>(Config::DeleteServerAfter)
|
|
||||||
.await?
|
|
||||||
.unwrap_or(0)
|
|
||||||
{
|
|
||||||
0 => None,
|
|
||||||
1 => Some(0),
|
|
||||||
x => Some(x),
|
|
||||||
};
|
|
||||||
Ok(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the configured provider.
|
/// Gets the configured provider.
|
||||||
///
|
///
|
||||||
/// The provider is determined by the current primary transport.
|
/// The provider is determined by the current primary transport.
|
||||||
|
|||||||
@@ -142,28 +142,6 @@ async fn test_mdns_default_behaviour() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_delete_server_after_default() -> Result<()> {
|
|
||||||
let t = &TestContext::new_alice().await;
|
|
||||||
|
|
||||||
// Check that the settings are displayed correctly.
|
|
||||||
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::DeleteServerAfter).await?,
|
|
||||||
Some("0".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
|
|
||||||
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
|
|
||||||
// does).
|
|
||||||
t.set_config_bool(Config::BccSelf, false).await?;
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::DeleteServerAfter).await?,
|
|
||||||
Some("0".to_string())
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
|
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
|||||||
@@ -973,12 +973,6 @@ impl Context {
|
|||||||
.await?
|
.await?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
res.insert(
|
|
||||||
"delete_server_after",
|
|
||||||
self.get_config_int(Config::DeleteServerAfter)
|
|
||||||
.await?
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
res.insert(
|
res.insert(
|
||||||
"last_housekeeping",
|
"last_housekeeping",
|
||||||
self.get_config_int(Config::LastHousekeeping)
|
self.get_config_int(Config::LastHousekeeping)
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ use crate::{EventType, chatlist_events};
|
|||||||
pub(crate) mod post_msg_metadata;
|
pub(crate) mod post_msg_metadata;
|
||||||
pub(crate) use post_msg_metadata::PostMsgMetadata;
|
pub(crate) use post_msg_metadata::PostMsgMetadata;
|
||||||
|
|
||||||
/// If a message is downloaded only partially
|
|
||||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
|
||||||
/// the user might have no chance to actually download that message.
|
|
||||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
|
||||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
|
||||||
|
|
||||||
/// From this point onward outgoing messages are considered large
|
/// From this point onward outgoing messages are considered large
|
||||||
/// and get a Pre-Message, which announces the Post-Message.
|
/// and get a Pre-Message, which announces the Post-Message.
|
||||||
/// This is only about sending so we can modify it any time.
|
/// This is only about sending so we can modify it any time.
|
||||||
|
|||||||
152
src/ephemeral.rs
152
src/ephemeral.rs
@@ -23,16 +23,15 @@
|
|||||||
//! ## Device settings
|
//! ## Device settings
|
||||||
//!
|
//!
|
||||||
//! In addition to per-chat ephemeral message setting, each device has
|
//! In addition to per-chat ephemeral message setting, each device has
|
||||||
//! two global user-configured settings that complement per-chat
|
//! a global user-configured setting that complements per-chat
|
||||||
//! settings: `delete_device_after` and `delete_server_after`. These
|
//! settings, `delete_device_after`.
|
||||||
//! settings are not synchronized among devices and apply to all
|
//! This setting is not synchronized among devices and applies to all
|
||||||
//! messages known to the device, including messages sent or received
|
//! messages known to the device, including messages sent or received
|
||||||
//! before configuring the setting.
|
//! before configuring the setting.
|
||||||
//!
|
//!
|
||||||
//! `delete_device_after` configures the maximum time device is
|
//! `delete_device_after` configures the maximum time device is
|
||||||
//! storing the messages locally. `delete_server_after` configures the
|
//! storing the messages locally,
|
||||||
//! time after which device will delete the messages it knows about
|
//! but does not delete messages from the server.
|
||||||
//! from the server.
|
|
||||||
//!
|
//!
|
||||||
//! ## How messages are deleted
|
//! ## How messages are deleted
|
||||||
//!
|
//!
|
||||||
@@ -60,9 +59,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Server deletion happens by updating the `imap` table based on
|
//! Server deletion happens by updating the `imap` table based on
|
||||||
//! the database entries which are expired either according to their
|
//! the database entries which are expired either according to their
|
||||||
//! ephemeral message timers or global `delete_server_after` setting.
|
//! ephemeral message timers.
|
||||||
|
|
||||||
use std::cmp::max;
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
@@ -75,10 +73,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
use crate::chat::{ChatId, ChatIdBlocked, send_msg};
|
use crate::chat::{ChatId, ChatIdBlocked, send_msg};
|
||||||
|
use crate::config::Config;
|
||||||
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
|
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
|
||||||
use crate::contact::ContactId;
|
use crate::contact::ContactId;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
use crate::download::DownloadState;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::log::{LogExt, warn};
|
use crate::log::{LogExt, warn};
|
||||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||||
@@ -652,36 +651,115 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
|||||||
|
|
||||||
/// Schedules expired IMAP messages for deletion.
|
/// Schedules expired IMAP messages for deletion.
|
||||||
#[expect(clippy::arithmetic_side_effects)]
|
#[expect(clippy::arithmetic_side_effects)]
|
||||||
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
|
pub(crate) async fn delete_expired_imap_messages(
|
||||||
|
context: &Context,
|
||||||
|
transport_id: u32,
|
||||||
|
is_chatmail: bool,
|
||||||
|
) -> Result<()> {
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
if !context.get_config_bool(Config::BccSelf).await? && is_chatmail {
|
||||||
match context.get_config_delete_server_after().await? {
|
info!(
|
||||||
None => (0, 0),
|
context,
|
||||||
Some(delete_server_after) => (
|
"dbg marking all as deleted 1 - rfc724_mids: {:#?}",
|
||||||
match delete_server_after {
|
context
|
||||||
// Guarantee immediate deletion.
|
.sql
|
||||||
0 => i64::MAX,
|
.query_map_vec(
|
||||||
_ => now - delete_server_after,
|
"SELECT rfc724_mid FROM msgs
|
||||||
},
|
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?)
|
||||||
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
OR download_state=?",
|
||||||
),
|
(now, DownloadState::Done),
|
||||||
};
|
|row| Ok(row.get::<_, String>(0)?)
|
||||||
|
)
|
||||||
context
|
.await
|
||||||
.sql
|
);
|
||||||
.execute(
|
info!(
|
||||||
"UPDATE imap
|
context,
|
||||||
SET target=''
|
"dbg marking all as deleted 1 - pre_rfc724_mids: {:#?}",
|
||||||
WHERE rfc724_mid IN (
|
context
|
||||||
SELECT rfc724_mid FROM msgs
|
.sql
|
||||||
WHERE ((download_state = 0 AND timestamp < ?) OR
|
.query_map_vec(
|
||||||
(download_state != 0 AND timestamp < ?) OR
|
"SELECT pre_rfc724_mid FROM msgs
|
||||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
WHERE pre_rfc724_mid!=''",
|
||||||
)",
|
(),
|
||||||
(threshold_timestamp, threshold_timestamp_extended, now),
|
|row| Ok(row.get::<_, String>(0)?)
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
);
|
||||||
|
// This the only device using this relay.
|
||||||
|
// Mark all downloaded messages for deletion, because they are not needed anymore.
|
||||||
|
//
|
||||||
|
// For pre- and post-messages, `rfc724_mid` contains the post-message's Message-Id.
|
||||||
|
// The pre-message's Message-Id is in pre_rfc724_mid, if it exists.
|
||||||
|
//
|
||||||
|
// Pre-messages can be deleted even if the message wasn't fully downloaded yet,
|
||||||
|
// because it's only the post-message that hasn't been downloaded.
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"UPDATE imap
|
||||||
|
SET target=''
|
||||||
|
WHERE transport_id=?1
|
||||||
|
AND rfc724_mid IN (
|
||||||
|
SELECT rfc724_mid FROM msgs
|
||||||
|
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||||
|
OR download_state=?3
|
||||||
|
UNION
|
||||||
|
SELECT pre_rfc724_mid FROM msgs
|
||||||
|
WHERE pre_rfc724_mid!=''
|
||||||
|
)",
|
||||||
|
(transport_id, now, DownloadState::Done),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"dbg marking ephemeral as deleted 1 - rfc724_mids: {:#?}",
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.query_map_vec(
|
||||||
|
"SELECT rfc724_mid FROM msgs
|
||||||
|
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)",
|
||||||
|
(transport_id, now),
|
||||||
|
|row| Ok(row.get::<_, String>(0)?)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"dbg marking ephemeral as deleted 1 - pre_rfc724_mids: {:#?}",
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.query_map_vec(
|
||||||
|
"SELECT pre_rfc724_mid FROM msgs
|
||||||
|
WHERE pre_rfc724_mid!=''
|
||||||
|
AND (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?)",
|
||||||
|
(now,),
|
||||||
|
|row| Ok(row.get::<_, String>(0)?)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
// There may be other devices using this relay,
|
||||||
|
// either because there is multi-relay or because this is a classical email server.
|
||||||
|
// Only delete expired ephemeral messages.
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"UPDATE imap
|
||||||
|
SET target=''
|
||||||
|
WHERE transport_id=?1
|
||||||
|
AND rfc724_mid IN (
|
||||||
|
SELECT rfc724_mid FROM msgs
|
||||||
|
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||||
|
UNION
|
||||||
|
SELECT pre_rfc724_mid FROM msgs
|
||||||
|
WHERE pre_rfc724_mid!=''
|
||||||
|
AND (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||||
|
)",
|
||||||
|
(transport_id, now),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,105 +451,178 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
|||||||
let t = TestContext::new_alice().await;
|
let t = TestContext::new_alice().await;
|
||||||
const HOUR: i64 = 60 * 60;
|
const HOUR: i64 = 60 * 60;
|
||||||
let now = time();
|
let now = time();
|
||||||
let transport_id = 1;
|
let transport_id: u32 = 1;
|
||||||
let uidvalidity = 12345;
|
let other_transport_id: u32 = 2;
|
||||||
for (id, timestamp, ephemeral_timestamp) in &[
|
let uidvalidity = 12345u32;
|
||||||
(900, now - 2 * HOUR, 0),
|
|
||||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
async fn is_deleted(context: &Context, mid: &str) -> Result<bool> {
|
||||||
(1010, now - 23 * HOUR, 0),
|
Ok(context
|
||||||
(1020, now - 21 * HOUR, 0),
|
.sql
|
||||||
(1030, now - 19 * HOUR, 0),
|
.count(
|
||||||
(2000, now - 18 * HOUR, now - HOUR),
|
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||||
(2020, now - 17 * HOUR, now + HOUR),
|
(mid,),
|
||||||
(3000, now + HOUR, 0),
|
)
|
||||||
] {
|
.await?
|
||||||
let message_id = id.to_string();
|
== 1)
|
||||||
t.sql
|
}
|
||||||
.execute(
|
|
||||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
async fn reset_targets(context: &Context) {
|
||||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
context
|
||||||
)
|
.sql
|
||||||
.await?;
|
.execute("UPDATE imap SET target='INBOX'", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test messages ────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// (id, rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid)
|
||||||
|
//
|
||||||
|
// "expired" – expired ephemeral, no pre-msg
|
||||||
|
// "no_expire" – ephemeral_timestamp=0, not Done → never deleted
|
||||||
|
// "future" – future ephemeral, not Done → never deleted
|
||||||
|
// "done" – Done, no ephemeral → branch 1 only
|
||||||
|
// "pre_no_expire_*" – has pre-msg, but no expiry/Done
|
||||||
|
// "pre_expired_*" – has pre-msg, expired ephemeral
|
||||||
|
// "pre_future_*" – has pre-msg, future ephemeral
|
||||||
|
// "wrong_tid" – expired+Done, but wrong transport_id in imap
|
||||||
|
let msgs: &[(&str, i64, DownloadState, &str)] = &[
|
||||||
|
("expired", now - HOUR, DownloadState::Available, ""),
|
||||||
|
("no_expire", 0, DownloadState::Available, ""),
|
||||||
|
("future", now + HOUR, DownloadState::Available, ""),
|
||||||
|
("done", 0, DownloadState::Done, ""),
|
||||||
|
(
|
||||||
|
"pre_no_expire_post",
|
||||||
|
0,
|
||||||
|
DownloadState::Available,
|
||||||
|
"pre_no_expire_pre",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pre_expired_post",
|
||||||
|
now - HOUR,
|
||||||
|
DownloadState::Available,
|
||||||
|
"pre_expired_pre",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pre_future_post",
|
||||||
|
now + HOUR,
|
||||||
|
DownloadState::Available,
|
||||||
|
"pre_future_pre",
|
||||||
|
),
|
||||||
|
("wrong_tid", now - HOUR, DownloadState::Done, ""),
|
||||||
|
];
|
||||||
|
for (mid, eph_ts, dl_state, pre_mid) in msgs {
|
||||||
t.sql
|
t.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
|
"INSERT INTO msgs \
|
||||||
(transport_id, &message_id, id, uidvalidity),
|
(rfc724_mid, timestamp, ephemeral_timestamp, download_state, pre_rfc724_mid) \
|
||||||
|
VALUES (?,?,0,?,?,?)",
|
||||||
|
(*mid, *eph_ts, *dl_state, *pre_mid),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
// One imap row per mid (including separate rows for pre-messages),
|
||||||
assert_eq!(
|
// plus "wrong_tid" on a different transport_id.
|
||||||
context
|
let imap_rows: &[(&str, u32)] = &[
|
||||||
.sql
|
("expired", transport_id),
|
||||||
.count(
|
("no_expire", transport_id),
|
||||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
("future", transport_id),
|
||||||
(id.to_string(),),
|
("done", transport_id),
|
||||||
)
|
("pre_no_expire_post", transport_id),
|
||||||
.await?,
|
("pre_no_expire_pre", transport_id), // the pre-message's own imap row
|
||||||
1
|
("pre_expired_post", transport_id),
|
||||||
);
|
("pre_expired_pre", transport_id),
|
||||||
Ok(())
|
("pre_future_post", transport_id),
|
||||||
}
|
("pre_future_pre", transport_id),
|
||||||
|
("wrong_tid", other_transport_id), // transport_id filter test
|
||||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
];
|
||||||
context
|
for (i, (mid, tid)) in imap_rows.iter().enumerate() {
|
||||||
.sql
|
t.sql
|
||||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
.execute(
|
||||||
|
"INSERT INTO imap \
|
||||||
|
(transport_id, rfc724_mid, folder, uid, target, uidvalidity) \
|
||||||
|
VALUES (?,?,'INBOX',?,'INBOX',?)",
|
||||||
|
(*tid, *mid, (i + 1) as u32, uidvalidity),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should mark message 2000 for deletion.
|
// ── Branch 1: is_chatmail=true, BccSelf=false (default) ─────────────────
|
||||||
delete_expired_imap_messages(&t).await?;
|
//
|
||||||
test_marked_for_deletion(&t, 2000).await?;
|
// SQL deletes: (ephemeral_timestamp!=0 AND <=now) OR download_state=Done
|
||||||
remove_uid(&t, 2000).await?;
|
// Pre-messages: ALL with pre_rfc724_mid!='' unconditionally.
|
||||||
// No other messages are marked for deletion.
|
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||||
assert_eq!(
|
|
||||||
t.sql
|
|
||||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
|
||||||
.await?,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
// Tests (ephemeral_timestamp!=0 AND ephemeral_timestamp<=now) path.
|
||||||
.await?;
|
assert!(is_deleted(&t, "expired").await?);
|
||||||
delete_expired_imap_messages(&t).await?;
|
// Tests the ephemeral_timestamp!=0 guard: timestamp=0 satisfies <=now but must not match.
|
||||||
test_marked_for_deletion(&t, 1000).await?;
|
assert!(!is_deleted(&t, "no_expire").await?);
|
||||||
|
// Tests the ephemeral_timestamp<=now guard.
|
||||||
|
assert!(!is_deleted(&t, "future").await?);
|
||||||
|
// Tests the OR download_state=Done clause.
|
||||||
|
assert!(is_deleted(&t, "done").await?);
|
||||||
|
// Post-message: no expiry, not Done → not deleted.
|
||||||
|
assert!(!is_deleted(&t, "pre_no_expire_post").await?);
|
||||||
|
// Pre-message: deleted unconditionally (tests UNION SELECT pre_rfc724_mid ... WHERE pre_rfc724_mid!='').
|
||||||
|
assert!(is_deleted(&t, "pre_no_expire_pre").await?);
|
||||||
|
// Post-message with expired ephemeral → deleted.
|
||||||
|
assert!(is_deleted(&t, "pre_expired_post").await?);
|
||||||
|
// Pre-message of expired post → deleted (unconditional pre path).
|
||||||
|
assert!(is_deleted(&t, "pre_expired_pre").await?);
|
||||||
|
// Post-message with future ephemeral → not deleted.
|
||||||
|
assert!(!is_deleted(&t, "pre_future_post").await?);
|
||||||
|
// Pre-message of future post → still deleted (branch 1 pre path has NO ephemeral condition).
|
||||||
|
// If the pre UNION clause gains an ephemeral condition, this would wrongly not be deleted.
|
||||||
|
assert!(is_deleted(&t, "pre_future_pre").await?);
|
||||||
|
// Tests transport_id=?1: expired+Done but on wrong transport_id → not deleted.
|
||||||
|
assert!(!is_deleted(&t, "wrong_tid").await?);
|
||||||
|
|
||||||
MsgId::new(1000)
|
reset_targets(&t).await;
|
||||||
.update_download_state(&t, DownloadState::Available)
|
|
||||||
.await?;
|
|
||||||
t.sql
|
|
||||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
|
||||||
.await?;
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
|
||||||
remove_uid(&t, 1000).await?;
|
|
||||||
|
|
||||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
// ── Branch 2: is_chatmail=false ──────────────────────────────────────────
|
||||||
.await?;
|
//
|
||||||
delete_expired_imap_messages(&t).await?;
|
// SQL deletes: ephemeral_timestamp!=0 AND <=now only (no Done).
|
||||||
test_marked_for_deletion(&t, 1010).await?;
|
// Pre-messages: only when the post also satisfies the ephemeral condition.
|
||||||
t.sql
|
delete_expired_imap_messages(&t, transport_id, false).await?;
|
||||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
MsgId::new(1010)
|
// Expired ephemeral → deleted.
|
||||||
.update_download_state(&t, DownloadState::Available)
|
assert!(is_deleted(&t, "expired").await?);
|
||||||
.await?;
|
// ephemeral_timestamp=0 → not deleted (tests !=0 guard in branch 2).
|
||||||
delete_expired_imap_messages(&t).await?;
|
assert!(!is_deleted(&t, "no_expire").await?);
|
||||||
// Keep downloadable for now.
|
// Future ephemeral → not deleted (tests <=now guard in branch 2).
|
||||||
assert_eq!(
|
assert!(!is_deleted(&t, "future").await?);
|
||||||
t.sql
|
// Done without expired ephemeral → NOT deleted (key branch 1 vs 2 difference).
|
||||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
// If download_state=Done were added to branch 2, this would wrongly be deleted.
|
||||||
.await?,
|
assert!(!is_deleted(&t, "done").await?);
|
||||||
0
|
// Post-message: no expiry → not deleted.
|
||||||
);
|
assert!(!is_deleted(&t, "pre_no_expire_post").await?);
|
||||||
|
// Pre-message of non-expiring post → NOT deleted
|
||||||
|
// (tests ephemeral_timestamp!=0 in branch 2's pre subquery).
|
||||||
|
assert!(!is_deleted(&t, "pre_no_expire_pre").await?);
|
||||||
|
// Post-message with expired ephemeral → deleted.
|
||||||
|
assert!(is_deleted(&t, "pre_expired_post").await?);
|
||||||
|
// Pre-message of expired post → deleted (tests full ephemeral condition in pre subquery).
|
||||||
|
assert!(is_deleted(&t, "pre_expired_pre").await?);
|
||||||
|
// Post-message with future ephemeral → not deleted.
|
||||||
|
assert!(!is_deleted(&t, "pre_future_post").await?);
|
||||||
|
// Pre-message of future post → NOT deleted
|
||||||
|
// (tests ephemeral_timestamp<=now in branch 2's pre subquery).
|
||||||
|
// If the <=now guard were removed there, this would wrongly be deleted.
|
||||||
|
assert!(!is_deleted(&t, "pre_future_pre").await?);
|
||||||
|
// Wrong transport_id → not deleted.
|
||||||
|
assert!(!is_deleted(&t, "wrong_tid").await?);
|
||||||
|
|
||||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
reset_targets(&t).await;
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 3000).await?;
|
// ── BccSelf=true forces branch 2 even when is_chatmail=true ─────────────
|
||||||
|
//
|
||||||
|
// Tests the `!BccSelf` part of the Rust condition.
|
||||||
|
// If `!BccSelf` were dropped, Done would be deleted here (branch 1 behaviour).
|
||||||
|
t.set_config(Config::BccSelf, Some("1")).await?;
|
||||||
|
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||||
|
assert!(!is_deleted(&t, "done").await?); // must stay on branch 2
|
||||||
|
assert!(is_deleted(&t, "expired").await?); // branch 2 still runs normally
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/imap.rs
28
src/imap.rs
@@ -21,9 +21,6 @@ use futures_lite::FutureExt;
|
|||||||
use ratelimit::Ratelimit;
|
use ratelimit::Ratelimit;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::calls::{
|
|
||||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
|
||||||
};
|
|
||||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||||
use crate::chatlist_events;
|
use crate::chatlist_events;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -49,6 +46,10 @@ use crate::tools::{self, create_id, duration_to_str, time};
|
|||||||
use crate::transport::{
|
use crate::transport::{
|
||||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
calls::{UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata},
|
||||||
|
ephemeral::delete_expired_imap_messages,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) mod capabilities;
|
pub(crate) mod capabilities;
|
||||||
mod client;
|
mod client;
|
||||||
@@ -525,6 +526,12 @@ impl Imap {
|
|||||||
context.scheduler.interrupt_ephemeral_task().await;
|
context.scheduler.interrupt_ephemeral_task().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is not
|
||||||
|
// not well optimized and should not be called before fetching.
|
||||||
|
delete_expired_imap_messages(context, session.transport_id(), session.is_chatmail())
|
||||||
|
.await
|
||||||
|
.context("delete_expired_imap_messages")?;
|
||||||
|
|
||||||
session
|
session
|
||||||
.move_delete_messages(context, watch_folder)
|
.move_delete_messages(context, watch_folder)
|
||||||
.await
|
.await
|
||||||
@@ -1379,6 +1386,21 @@ impl Session {
|
|||||||
"Passing message UID {} to receive_imf().", request_uid
|
"Passing message UID {} to receive_imf().", request_uid
|
||||||
);
|
);
|
||||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||||
|
|
||||||
|
// TODO I don't think this code is needed anymore:
|
||||||
|
// // If the message is not needed anymore on the server, mark it for deletion:
|
||||||
|
// if !context.get_config_bool(Config::BccSelf).await? && is_chatmail {
|
||||||
|
// context
|
||||||
|
// .sql
|
||||||
|
// .execute(
|
||||||
|
// "UPDATE imap SET target='' WHERE rfc724_mid=?",
|
||||||
|
// (rfc724_mid,),
|
||||||
|
// )
|
||||||
|
// .await?;
|
||||||
|
// context.scheduler.interrupt_inbox().await;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// If there was an error receiving the message, show a device message:
|
||||||
let received_msg = match res {
|
let received_msg = match res {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(context, "receive_imf error: {err:#}.");
|
warn!(context, "receive_imf error: {err:#}.");
|
||||||
|
|||||||
27
src/imex.rs
27
src/imex.rs
@@ -979,21 +979,16 @@ mod tests {
|
|||||||
context1.set_config(Config::BccSelf, None).await?;
|
context1.set_config(Config::BccSelf, None).await?;
|
||||||
|
|
||||||
// Check that the settings are displayed correctly.
|
// Check that the settings are displayed correctly.
|
||||||
assert_eq!(
|
|
||||||
context1.get_config(Config::DeleteServerAfter).await?,
|
|
||||||
Some("0".to_string())
|
|
||||||
);
|
|
||||||
context1.set_config_bool(Config::IsChatmail, true).await?;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
context1.get_config(Config::BccSelf).await?,
|
context1.get_config(Config::BccSelf).await?,
|
||||||
Some("0".to_string())
|
Some("0".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
context1.set_config_bool(Config::IsChatmail, true).await?;
|
||||||
context1.get_config(Config::DeleteServerAfter).await?,
|
|
||||||
Some("1".to_string())
|
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false);
|
||||||
);
|
context1.set_config_bool(Config::IsMuted, true).await?;
|
||||||
|
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, true);
|
||||||
|
|
||||||
assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
|
|
||||||
imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
|
imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
|
||||||
let _event = context1
|
let _event = context1
|
||||||
.evtracker
|
.evtracker
|
||||||
@@ -1010,15 +1005,9 @@ mod tests {
|
|||||||
assert!(context2.is_configured().await?);
|
assert!(context2.is_configured().await?);
|
||||||
assert!(context2.is_chatmail().await?);
|
assert!(context2.is_chatmail().await?);
|
||||||
for ctx in [context1, context2] {
|
for ctx in [context1, context2] {
|
||||||
assert_eq!(
|
// BccSelf should be enabled automatically when exporting a backup
|
||||||
ctx.get_config(Config::BccSelf).await?,
|
assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true);
|
||||||
Some("1".to_string())
|
assert_eq!(ctx.get_config_bool(Config::IsMuted).await?, true);
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.get_config(Config::DeleteServerAfter).await?,
|
|
||||||
Some("0".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(ctx.get_config_delete_server_after().await?, None);
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2099,63 +2099,52 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Estimates the number of messages that will be deleted
|
/// Estimates the number of messages that will be deleted
|
||||||
/// by the options `delete_device_after` or `delete_server_after`.
|
/// by the `set_config()`-option `delete_device_after`.
|
||||||
///
|
///
|
||||||
/// This is typically used to show the estimated impact to the user
|
/// This is typically used to show the estimated impact to the user
|
||||||
/// before actually enabling deletion of old messages.
|
/// before actually enabling deletion of old messages.
|
||||||
///
|
///
|
||||||
/// If `from_server` is true,
|
/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||||
/// estimate deletion count for server,
|
|
||||||
/// otherwise estimate deletion count for device.
|
|
||||||
///
|
///
|
||||||
/// Count messages older than the given number of `seconds`.
|
/// Parameters:
|
||||||
|
/// - `from_server`: Deprecated, pass `false` here
|
||||||
|
/// - `seconds`: Count messages older than the given number of seconds.
|
||||||
///
|
///
|
||||||
/// Returns the number of messages that are older than the given number of seconds.
|
/// Returns the number of messages that are older than the given number of seconds.
|
||||||
/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
|
||||||
#[expect(clippy::arithmetic_side_effects)]
|
#[expect(clippy::arithmetic_side_effects)]
|
||||||
pub async fn estimate_deletion_cnt(
|
pub async fn estimate_deletion_cnt(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
from_server: bool,
|
from_server: bool,
|
||||||
seconds: i64,
|
seconds: i64,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
|
ensure!(
|
||||||
|
!from_server,
|
||||||
|
"The `delete_server_after` config option was removed. You need to pass `false` for `from_server`."
|
||||||
|
);
|
||||||
|
|
||||||
let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
|
let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
|
||||||
.await?
|
.await?
|
||||||
.map(|c| c.id)
|
.map(|c| c.id)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let threshold_timestamp = time() - seconds;
|
let threshold_timestamp = time() - seconds;
|
||||||
|
|
||||||
let cnt = if from_server {
|
let cnt = context
|
||||||
context
|
.sql
|
||||||
.sql
|
.count(
|
||||||
.count(
|
"SELECT COUNT(*)
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM msgs m
|
|
||||||
WHERE m.id > ?
|
|
||||||
AND timestamp < ?
|
|
||||||
AND chat_id != ?
|
|
||||||
AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
|
|
||||||
(DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.count(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM msgs m
|
FROM msgs m
|
||||||
WHERE m.id > ?
|
WHERE m.id > ?
|
||||||
AND timestamp < ?
|
AND timestamp < ?
|
||||||
AND chat_id != ?
|
AND chat_id != ?
|
||||||
AND chat_id != ? AND hidden = 0;",
|
AND chat_id != ? AND hidden = 0;",
|
||||||
(
|
(
|
||||||
DC_MSG_ID_LAST_SPECIAL,
|
DC_MSG_ID_LAST_SPECIAL,
|
||||||
threshold_timestamp,
|
threshold_timestamp,
|
||||||
self_chat_id,
|
self_chat_id,
|
||||||
DC_CHAT_ID_TRASH,
|
DC_CHAT_ID_TRASH,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
};
|
|
||||||
Ok(cnt)
|
Ok(cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -890,16 +890,10 @@ static P_NAUTA_CU: Provider = Provider {
|
|||||||
strict_tls: false,
|
strict_tls: false,
|
||||||
..ProviderOptions::new()
|
..ProviderOptions::new()
|
||||||
},
|
},
|
||||||
config_defaults: Some(&[
|
config_defaults: Some(&[ConfigDefault {
|
||||||
ConfigDefault {
|
key: Config::MediaQuality,
|
||||||
key: Config::DeleteServerAfter,
|
value: "1",
|
||||||
value: "1",
|
}]),
|
||||||
},
|
|
||||||
ConfigDefault {
|
|
||||||
key: Config::MediaQuality,
|
|
||||||
value: "1",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
oauth2_authorizer: None,
|
oauth2_authorizer: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2382,4 +2376,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
|||||||
});
|
});
|
||||||
|
|
||||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 4, 21).unwrap());
|
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 5, 6).unwrap());
|
||||||
|
|||||||
@@ -902,10 +902,8 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get user-configured server deletion
|
// Get user-configured server deletion
|
||||||
let delete_server_after = context.get_config_delete_server_after().await?;
|
|
||||||
|
|
||||||
if !received_msg.msg_ids.is_empty() {
|
if !received_msg.msg_ids.is_empty() {
|
||||||
let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
|
let target = if received_msg.needs_delete_job {
|
||||||
Some("".to_string())
|
Some("".to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -1976,15 +1976,10 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
|
|||||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||||
|
|
||||||
bob.set_config_bool(Config::BccSelf, true).await?;
|
bob.set_config_bool(Config::BccSelf, true).await?;
|
||||||
bob.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
|
||||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
|
||||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
|
||||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
|
||||||
|
|
||||||
bob.set_config(Config::DeleteServerAfter, None).await?;
|
|
||||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some());
|
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -484,14 +484,6 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session)
|
|||||||
.await
|
.await
|
||||||
.context("fetch_move_delete")?;
|
.context("fetch_move_delete")?;
|
||||||
|
|
||||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
|
||||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
|
||||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
|
||||||
// otherwise slow down message fetching.
|
|
||||||
delete_expired_imap_messages(ctx)
|
|
||||||
.await
|
|
||||||
.context("delete_expired_imap_messages")?;
|
|
||||||
|
|
||||||
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
|
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
|
||||||
download_msgs(ctx, &mut session)
|
download_msgs(ctx, &mut session)
|
||||||
.await
|
.await
|
||||||
|
|||||||
34
src/smtp.rs
34
src/smtp.rs
@@ -699,26 +699,22 @@ pub(crate) async fn add_self_recipients(
|
|||||||
recipients: &mut Vec<String>,
|
recipients: &mut Vec<String>,
|
||||||
encrypted: bool,
|
encrypted: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Previous versions of Delta Chat did not send BCC self
|
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||||
// if DeleteServerAfter was set to immediately delete messages
|
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||||
// from the server. This is not the case anymore
|
// messages.
|
||||||
// because BCC-self messages are also used to detect
|
if encrypted {
|
||||||
// that message was sent if SMTP server is slow to respond
|
for addr in context.get_published_secondary_self_addrs().await? {
|
||||||
// and connection is frequently lost
|
recipients.push(addr);
|
||||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
|
||||||
// disabled by default is fine.
|
|
||||||
if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() {
|
|
||||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
|
||||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
|
||||||
// messages.
|
|
||||||
if encrypted {
|
|
||||||
for addr in context.get_published_secondary_self_addrs().await? {
|
|
||||||
recipients.push(addr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
|
||||||
let from = context.get_primary_self_addr().await?;
|
|
||||||
recipients.push(from);
|
|
||||||
}
|
}
|
||||||
|
// `from` must be the last addr
|
||||||
|
// because `receive_imf_inner()` marks the message as 'delivered'
|
||||||
|
// if it arrives to the self-server via `bcc_self`.
|
||||||
|
// This helps with marking messages as delivered
|
||||||
|
// if the server is slow and we never get an `OK` response
|
||||||
|
// before the connection times out.
|
||||||
|
let from = context.get_primary_self_addr().await?;
|
||||||
|
recipients.push(from);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user