mirror of
https://github.com/chatmail/core.git
synced 2026-05-02 21:06:31 +03:00
Compare commits
1 Commits
iequidoo/t
...
iequidoo/S
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
962fed1c92 |
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@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
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@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
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@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -3933,9 +3933,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.78"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
@@ -3974,9 +3974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.114"
|
||||
version = "0.9.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
|
||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -5164,7 +5164,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.13",
|
||||
"rustls-webpki 0.103.12",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5201,9 +5201,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
version = "0.103.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
||||
@@ -390,9 +390,27 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
/**
|
||||
* Configure the context. The configuration is handled by key=value pairs as:
|
||||
*
|
||||
* - `configured_addr` = Email address in use.
|
||||
* - `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.
|
||||
* 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
|
||||
@@ -408,6 +426,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
@@ -416,7 +440,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `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.
|
||||
* "Saved messages" are deleted from the server as well as
|
||||
* e-mails matching the `show_emails` settings above, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||
@@ -488,27 +513,6 @@ 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
|
||||
@@ -534,6 +538,9 @@ int dc_set_config (dc_context_t* context, const char*
|
||||
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
|
||||
* - `sys.config_keys` = get a space-separated list of all config-keys available.
|
||||
* The config-keys are the keys that can be passed to the parameter `key` of this function.
|
||||
* - `quota_exceeding` = 0: quota is unknown or in normal range;
|
||||
* >=80: quota is about to exceed, the value is the concrete percentage,
|
||||
* a device message is added when that happens, however, that value may still be interesting for bots.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object. For querying system values, this can be NULL.
|
||||
@@ -692,12 +699,6 @@ 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,
|
||||
@@ -1387,6 +1388,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
#define DC_GCM_ADDDAYMARKER 0x01
|
||||
#define DC_GCM_INFO_ONLY 0x02
|
||||
|
||||
|
||||
/**
|
||||
@@ -1407,6 +1409,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||
* @param marker1before Deprecated, set this to 0.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
*/
|
||||
@@ -1470,6 +1473,7 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* This includes e-mails downloaded due to the `show_emails` option.
|
||||
* Messages in the "saved messages" folder 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);
|
||||
@@ -2815,6 +2819,19 @@ int dc_set_location (dc_context_t* context, double latit
|
||||
dc_array_t* dc_get_locations (dc_context_t* context, uint32_t chat_id, uint32_t contact_id, int64_t timestamp_begin, int64_t timestamp_end);
|
||||
|
||||
|
||||
/**
|
||||
* Delete all locations on the current device.
|
||||
* Locations already sent cannot be deleted.
|
||||
*
|
||||
* Typically results in the event #DC_EVENT_LOCATION_CHANGED
|
||||
* with contact_id set to 0.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
*/
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
// misc
|
||||
|
||||
/**
|
||||
@@ -5787,7 +5804,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 key "imap_certificate_checks".
|
||||
* using keys "imap_certificate_checks" and "smtp_certificate_checks".
|
||||
*
|
||||
* @addtogroup DC_CERTCK
|
||||
* @{
|
||||
@@ -6400,7 +6417,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Location of one or more contact has changed.
|
||||
*
|
||||
* @param data1 (int) contact_id of the contact for which the location has changed.
|
||||
* If the locations of several contacts have been changed, this parameter is set to 0.
|
||||
* If the locations of several contacts have been changed,
|
||||
* e.g. after calling dc_delete_all_locations(), this parameter is set to 0.
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_LOCATION_CHANGED 2035
|
||||
@@ -6671,6 +6689,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_CONFIGURE_PROGRESS || (e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("show_emails")
|
||||
*/
|
||||
#define DC_SHOW_EMAILS_OFF 0
|
||||
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
||||
#define DC_SHOW_EMAILS_ALL 2
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("media_quality")
|
||||
*/
|
||||
@@ -7005,7 +7031,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in message summary text for notifications and chatlist.
|
||||
#define DC_STR_FORWARDED 97
|
||||
|
||||
/// @deprecated 2026-04-25
|
||||
/// "Quota exceeding, already %1$s%% used."
|
||||
///
|
||||
/// Used as device message text.
|
||||
///
|
||||
/// `%1$s` will be replaced by the percentage used
|
||||
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
|
||||
|
||||
/// "Multi Device Synchronization"
|
||||
|
||||
@@ -60,6 +60,7 @@ use self::string::*;
|
||||
// - finally, this behaviour matches the old core-c API and UIs already depend on it
|
||||
|
||||
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
|
||||
// dc_context_t
|
||||
|
||||
@@ -1337,13 +1338,17 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let info_only = (flags & DC_GCM_INFO_ONLY) != 0;
|
||||
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs_ex(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||
@@ -2541,7 +2546,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::send_to_chat(
|
||||
block_on(location::send_locations_to_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
seconds as i64,
|
||||
@@ -2561,14 +2566,14 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
if chat_id == 0 {
|
||||
block_on(location::is_sending(ctx))
|
||||
.unwrap_or_log_default(ctx, "Failed is_sending_locations()") as libc::c_int
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
block_on(location::is_sending_to_chat(ctx, ChatId::new(chat_id)))
|
||||
.unwrap_or_log_default(ctx, "Failed is_sending_locations_to_chat()")
|
||||
as libc::c_int
|
||||
}
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
|
||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2584,9 +2589,12 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2621,6 +2629,23 @@ pub unsafe extern "C" fn dc_get_locations(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_delete_all_locations()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
location::delete_all(ctx)
|
||||
.await
|
||||
.context("Failed to delete locations")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||
if payload.is_null() {
|
||||
|
||||
@@ -318,6 +318,15 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Requests to clear storage on all chatmail relays.
|
||||
///
|
||||
/// I/O must be started for this request to take effect.
|
||||
async fn clear_all_relay_storage(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.clear_all_relay_storage().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get top-level info for an account.
|
||||
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
|
||||
let context_option = self.accounts.read().await.get_account(account_id);
|
||||
@@ -1366,22 +1375,8 @@ impl CommandApi {
|
||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||
}
|
||||
|
||||
/// Get all message IDs belonging to a chat.
|
||||
/// Returns all messages of a particular chat.
|
||||
///
|
||||
/// The list is already sorted and starts with the oldest message.
|
||||
/// Clients should not try to re-sort the list as this would be an expensive action
|
||||
/// and would result in inconsistencies between clients.
|
||||
/// Note that the messages are not necessarily sorted by their ID or by their displayed timestamp;
|
||||
/// UIs need to handle both the case of descending message IDs
|
||||
/// and of decreasing timestamps.
|
||||
///
|
||||
/// Optionally, 'daymarkers' added to the ID array may help to
|
||||
/// implement virtual lists.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * chat_id The chat ID of which the messages IDs should be queried.
|
||||
/// * _info_only: Deprecated, pass `false` here.
|
||||
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||
/// corresponding (following) day in the local timezone.
|
||||
@@ -1389,14 +1384,17 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
_info_only: bool,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg
|
||||
@@ -1428,24 +1426,21 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all messages belonging to a chat.
|
||||
///
|
||||
/// Similar to `get_message_ids` / `getMessageIds`,
|
||||
/// see that function for details.
|
||||
/// The difference is that this function here returns a list of `MessageListItem`,
|
||||
/// which is an enum of a message or a daymarker.
|
||||
async fn get_message_list_items(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
_info_only: bool,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg
|
||||
@@ -2120,21 +2115,6 @@ impl CommandApi {
|
||||
// locations
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Sets current location.
|
||||
///
|
||||
/// Returns true if location streaming is currently
|
||||
/// enabled and locations should be updated.
|
||||
///
|
||||
/// Location is represented as latitude and longitude in degrees
|
||||
/// and horizontal accuracy in meters.
|
||||
async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
self.accounts
|
||||
.read()
|
||||
.await
|
||||
.set_location(latitude, longitude, accuracy)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_locations(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2157,39 +2137,6 @@ impl CommandApi {
|
||||
Ok(locations.into_iter().map(|l| l.into()).collect())
|
||||
}
|
||||
|
||||
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
///
|
||||
/// Pass 0 as the number of seconds to disable location streaming in the chat.
|
||||
async fn send_locations_to_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
seconds: i64,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
location::send_to_chat(&ctx, chat_id, seconds).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether any chat is sending locations.
|
||||
async fn is_sending_locations(&self, account_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
location::is_sending(&ctx).await
|
||||
}
|
||||
|
||||
/// Returns whether `chat_id` is sending locations.
|
||||
async fn is_sending_locations_to_chat(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
location::is_sending_to_chat(&ctx, chat_id).await
|
||||
}
|
||||
|
||||
/// Stops sending locations to all chats.
|
||||
async fn stop_sending_locations(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_sending_locations().await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// webxdc
|
||||
// ---------------------------------------------
|
||||
@@ -2432,6 +2379,9 @@ impl CommandApi {
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?;
|
||||
|
||||
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||
msg.force_sticker();
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
|
||||
@@ -287,6 +287,8 @@ pub enum MessageViewtype {
|
||||
Gif,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
|
||||
@@ -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":""}]}"#;
|
||||
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 response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||
session.handle_incoming(request).await;
|
||||
let result = receiver.recv().await?;
|
||||
|
||||
@@ -345,6 +345,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chatinfo\n\
|
||||
sendlocations <seconds>\n\
|
||||
setlocation <lat> <lng>\n\
|
||||
dellocations\n\
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
send-sync <text>\n\
|
||||
@@ -573,7 +574,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending(&context).await? {
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
@@ -622,6 +623,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
&context,
|
||||
sel_chat.get_id(),
|
||||
chat::MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: true,
|
||||
},
|
||||
)
|
||||
@@ -780,7 +782,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!(
|
||||
"Location streaming: {}",
|
||||
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -820,7 +826,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "No timeout given.");
|
||||
|
||||
let seconds = arg1.parse()?;
|
||||
location::send_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), seconds).await?;
|
||||
location::send_locations_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -842,6 +853,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Success, streaming can be stopped.");
|
||||
}
|
||||
}
|
||||
"dellocations" => {
|
||||
location::delete_all(&context).await?;
|
||||
}
|
||||
"send" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No message text given.");
|
||||
|
||||
@@ -176,7 +176,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 40] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -194,6 +194,7 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"chatinfo",
|
||||
"sendlocations",
|
||||
"setlocation",
|
||||
"dellocations",
|
||||
"getlocations",
|
||||
"send",
|
||||
"send-sync",
|
||||
|
||||
@@ -495,7 +495,3 @@ class Account:
|
||||
"""Return ICE servers for WebRTC configuration."""
|
||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||
return json.loads(ice_servers_json)
|
||||
|
||||
def is_sending_locations(self) -> bool:
|
||||
"""Return True if sending locations to any chat."""
|
||||
return self._rpc.is_sending_locations(self.id)
|
||||
|
||||
@@ -206,9 +206,9 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, add_daymarker: bool = False) -> list[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""Get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
def get_fresh_message_count(self) -> int:
|
||||
@@ -277,16 +277,6 @@ class Chat:
|
||||
"""Remove profile image of this chat."""
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
|
||||
def send_locations(self, seconds) -> None:
|
||||
"""Enable location streaming in the chat for the given number of seconds.
|
||||
|
||||
Pass 0 to disable location streaming."""
|
||||
self._rpc.send_locations_to_chat(self.account.id, self.id, seconds)
|
||||
|
||||
def is_sending_locations(self) -> bool:
|
||||
"""Return True if sending locations to this chat."""
|
||||
return self._rpc.is_sending_locations_to_chat(self.account.id, self.id)
|
||||
|
||||
def get_locations(
|
||||
self,
|
||||
contact: Optional[Contact] = None,
|
||||
|
||||
@@ -59,11 +59,3 @@ class DeltaChat:
|
||||
def set_translations(self, translations: dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
def set_location(self, latitude, longitude, accuracy) -> bool:
|
||||
"""Set location, return True if location streaming should continue."""
|
||||
return self.rpc.set_location(latitude, longitude, accuracy)
|
||||
|
||||
def stop_sending_locations(self) -> None:
|
||||
"""Stop sending locations to all chats."""
|
||||
return self.rpc.stop_sending_locations()
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
def test_set_location(dc, acfactory) -> None:
|
||||
# Try setting location without any accounts.
|
||||
assert not dc.set_location(1.0, 2.0, 0.1)
|
||||
|
||||
# Create one account that does not stream,
|
||||
# set location.
|
||||
acfactory.new_configured_account()
|
||||
assert not dc.set_location(3.0, 4.0, 0.1)
|
||||
|
||||
|
||||
def test_send_locations_to_chat(dc, acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
assert not alice.is_sending_locations()
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
|
||||
# Test starting and stopping location streaming in a chat.
|
||||
alice_chat_bob.send_locations(3600)
|
||||
assert alice.is_sending_locations()
|
||||
assert alice_chat_bob.is_sending_locations()
|
||||
alice_chat_bob.send_locations(0)
|
||||
assert not alice.is_sending_locations()
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
|
||||
# Test stop_sending_locations() for all accounts and chats.
|
||||
alice_chat_bob.send_locations(3600)
|
||||
assert alice.is_sending_locations()
|
||||
assert alice_chat_bob.is_sending_locations()
|
||||
dc.stop_sending_locations()
|
||||
assert not alice.is_sending_locations()
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
@@ -9,6 +9,8 @@ def test_add_second_address(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 2
|
||||
@@ -26,6 +28,22 @@ def test_add_second_address(acfactory) -> None:
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
# show_emails does not matter for multi-relay, can be set to anything
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
|
||||
def test_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport can be configured if classic emails are not fetched."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
|
||||
@@ -27,13 +27,7 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||
"RUSTSEC-2026-0049",
|
||||
"RUSTSEC-2026-0098",
|
||||
"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"
|
||||
"RUSTSEC-2026-0099"
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -433,6 +433,7 @@ 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
|
||||
@@ -504,6 +505,7 @@ 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=ad097ee40579c884e7757de2d3bb0a51f481a32a
|
||||
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::sync::Arc;
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use futures::FutureExt as _;
|
||||
use futures::future;
|
||||
use futures_lite::FutureExt as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
@@ -23,7 +22,6 @@ use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::location;
|
||||
use crate::log::warn;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
@@ -538,38 +536,6 @@ impl Accounts {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets location for all accounts.
|
||||
///
|
||||
/// Returns true if location should still be streamed.
|
||||
pub async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
let continue_streaming = future::try_join_all(self.accounts.iter().map(
|
||||
|(account_id, account)| async move {
|
||||
location::set(account, latitude, longitude, accuracy)
|
||||
.await
|
||||
.with_context(|| format!("Failed to set location for account {account_id}"))
|
||||
},
|
||||
))
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|continue_streaming| continue_streaming);
|
||||
Ok(continue_streaming)
|
||||
}
|
||||
|
||||
/// Stops sending locations to all chats.
|
||||
pub async fn stop_sending_locations(&self) -> Result<()> {
|
||||
future::try_join_all(
|
||||
self.accounts
|
||||
.iter()
|
||||
.map(|(account_id, account)| async move {
|
||||
location::stop_sending(account).await.with_context(|| {
|
||||
format!("Failed to stop sending locations for account {account_id}")
|
||||
})
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
|
||||
561
src/authres.rs
Normal file
561
src/authres.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
//! Parsing and handling of the Authentication-Results header.
|
||||
//! See the comment on [`handle_authres`] for more.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
/// about whether DKIM and SPF passed.
|
||||
///
|
||||
/// To mitigate From forgery, we remember for each sending domain whether it is known
|
||||
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
|
||||
/// we don't allow changing the autocrypt key.
|
||||
///
|
||||
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
|
||||
pub(crate) async fn handle_authres(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
) -> Result<DkimResults> {
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
|
||||
}
|
||||
};
|
||||
|
||||
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
|
||||
update_authservid_candidates(context, &authres).await?;
|
||||
compute_dkim_results(context, authres).await
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DkimResults {
|
||||
/// Whether DKIM passed for this particular e-mail.
|
||||
pub dkim_passed: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for DkimResults {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type AuthservId = String;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DkimResult {
|
||||
/// The header explicitly said that DKIM passed
|
||||
Passed,
|
||||
/// The header explicitly said that DKIM failed
|
||||
Failed,
|
||||
/// The header didn't say anything about DKIM; this might mean that it wasn't
|
||||
/// checked, but it might also mean that it failed. This is because some providers
|
||||
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
|
||||
/// Authentication-Results if there was no DKIM.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
|
||||
|
||||
fn parse_authres_headers(
|
||||
headers: &mailparse::headers::Headers<'_>,
|
||||
from_domain: &str,
|
||||
) -> ParsedAuthresHeaders {
|
||||
let mut res = Vec::new();
|
||||
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
|
||||
let header_value = remove_comments(&header_value);
|
||||
|
||||
if let Some(mut authserv_id) = header_value.split(';').next() {
|
||||
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
|
||||
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
|
||||
// because there is whitespace in the first identifier before the ';'.
|
||||
// Authentication-Results-parsing still works securely because they remove incoming
|
||||
// Authentication-Results headers.
|
||||
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
|
||||
// with providers not implementing the RFC correctly, someone can trick us
|
||||
// into thinking that an incoming email is DKIM-correct, anyway.
|
||||
// The most important thing here is that we have some valid `authserv_id`.
|
||||
authserv_id = "invalidAuthservId";
|
||||
}
|
||||
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
|
||||
res.push((authserv_id.to_string(), dkim_passed));
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// The headers can contain comments that look like this:
|
||||
/// ```text
|
||||
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
|
||||
/// ```
|
||||
fn remove_comments(header: &str) -> Cow<'_, str> {
|
||||
// In Pomsky, this is:
|
||||
// "(" Codepoint* lazy ")"
|
||||
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
|
||||
static RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
|
||||
|
||||
RE.replace_all(header, " ")
|
||||
}
|
||||
|
||||
/// Parses a single Authentication-Results header, like:
|
||||
///
|
||||
/// ```text
|
||||
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
/// ```
|
||||
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
|
||||
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
|
||||
// Check that the character right before `dkim=` is a space or a tab
|
||||
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
|
||||
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
|
||||
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
|
||||
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
|
||||
if let Some(&"pass") = dkim_parts.first() {
|
||||
// DKIM headers contain a header.d or header.i field
|
||||
// that says which domain signed. We have to check ourselves
|
||||
// that this is the same domain as in the From header.
|
||||
let header_d: &str = &format!("header.d={}", &from_domain);
|
||||
let header_i: &str = &format!("header.i=@{}", &from_domain);
|
||||
|
||||
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
|
||||
// We have found a `dkim=pass` header!
|
||||
return DkimResult::Passed;
|
||||
}
|
||||
} else {
|
||||
// dkim=fail, dkim=none, ...
|
||||
return DkimResult::Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DkimResult::Nothing
|
||||
}
|
||||
|
||||
/// ## About authserv-ids
|
||||
///
|
||||
/// After having checked DKIM, our email server adds an Authentication-Results header.
|
||||
///
|
||||
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
|
||||
/// in order to make us think that DKIM was correct in their From-forged email.
|
||||
///
|
||||
/// In order to prevent this, each email server adds its authserv-id to the
|
||||
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
|
||||
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
|
||||
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
|
||||
///
|
||||
/// We need to somehow find out the authserv-id(s) of our email server, so that
|
||||
/// we can use the Authentication-Results with the right authserv-id.
|
||||
///
|
||||
/// ## What this function does
|
||||
///
|
||||
/// When receiving an email, this function is called and updates the candidates for
|
||||
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
|
||||
///
|
||||
/// Usually, every incoming email has Authentication-Results with our server's
|
||||
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
|
||||
/// authserv-ids for our server's authserv-id is a good guess for our server's
|
||||
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
|
||||
/// changed and start over with the new authserv-ids.
|
||||
///
|
||||
/// See [`handle_authres`].
|
||||
async fn update_authservid_candidates(
|
||||
context: &Context,
|
||||
authres: &ParsedAuthresHeaders,
|
||||
) -> Result<()> {
|
||||
let mut new_ids: BTreeSet<&str> = authres
|
||||
.iter()
|
||||
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
|
||||
.collect();
|
||||
if new_ids.is_empty() {
|
||||
// The incoming message doesn't contain any authentication results, maybe it's a
|
||||
// self-sent or a mailer-daemon message
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let old_ids = parse_authservid_candidates_config(&old_config);
|
||||
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
|
||||
if !intersection.is_empty() {
|
||||
new_ids = intersection;
|
||||
}
|
||||
// If there were no AuthservIdCandidates previously, just start with
|
||||
// the ones from the incoming email
|
||||
|
||||
if old_ids != new_ids {
|
||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||
context
|
||||
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
|
||||
/// and whether a keychange should be allowed.
|
||||
///
|
||||
/// We track in the `sending_domains` table whether we get positive Authentication-Results
|
||||
/// for mails from a contact (meaning that their provider properly authenticates against
|
||||
/// our provider).
|
||||
///
|
||||
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
|
||||
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
|
||||
async fn compute_dkim_results(
|
||||
context: &Context,
|
||||
mut authres: ParsedAuthresHeaders,
|
||||
) -> Result<DkimResults> {
|
||||
let mut dkim_passed = false;
|
||||
|
||||
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let ids = parse_authservid_candidates_config(&ids_config);
|
||||
|
||||
// Remove all foreign authentication results
|
||||
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
|
||||
|
||||
if authres.is_empty() {
|
||||
// If the authentication results are empty, then our provider doesn't add them
|
||||
// and an attacker could just add their own Authentication-Results, making us
|
||||
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
|
||||
dkim_passed = true;
|
||||
} else {
|
||||
for (_authserv_id, current_dkim_passed) in authres {
|
||||
match current_dkim_passed {
|
||||
DkimResult::Passed => {
|
||||
dkim_passed = true;
|
||||
break;
|
||||
}
|
||||
DkimResult::Failed => {
|
||||
dkim_passed = false;
|
||||
break;
|
||||
}
|
||||
DkimResult::Nothing => {
|
||||
// Continue looking for an Authentication-Results header
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DkimResults { dkim_passed })
|
||||
}
|
||||
|
||||
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
|
||||
config
|
||||
.as_deref()
|
||||
.map(|c| c.split_whitespace().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
use crate::mimeparser;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
|
||||
#[test]
|
||||
fn test_remove_comments() {
|
||||
let header = "Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
remove_comments(&header),
|
||||
"Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass header.d=riseup.net;"
|
||||
);
|
||||
|
||||
let header = ") aaa (".to_string();
|
||||
assert_eq!(remove_comments(&header), ") aaa (");
|
||||
|
||||
let header = "((something weird) no comment".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment");
|
||||
|
||||
let header = "🎉(🎉(🎉))🎉(".to_string();
|
||||
assert_eq!(remove_comments(&header), "🎉 )🎉(");
|
||||
|
||||
// Comments are allowed to include whitespace
|
||||
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment ");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_authentication_results() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@gmx.net").await;
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Passed),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Nothing),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
|
||||
|
||||
// Weird Authentication-Results from Outlook without an authserv-id
|
||||
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
|
||||
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
|
||||
header.d=hotmail.com;dmarc=pass action=none
|
||||
header.from=hotmail.com;compauth=pass reason=100";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
|
||||
// At this point, the most important thing to test is that there are no
|
||||
// authserv-ids with whitespace in them.
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Failed),
|
||||
("gmx.net".to_string(), DkimResult::Passed)
|
||||
]
|
||||
);
|
||||
|
||||
// ';' in comments
|
||||
let bytes = b"Authentication-Results: mx1.riseup.net;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
|
||||
dkim-atps=neutral";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = br#"Authentication-Results: box.hispanilandia.net;
|
||||
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
|
||||
dkim-atps=neutral
|
||||
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
|
||||
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Failed),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_authservid_candidates() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx3.messagingengine.com");
|
||||
|
||||
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
// A message without any Authentication-Results headers shouldn't remove all
|
||||
// candidates since it could be a mailer-daemon message or so
|
||||
update_authservid_candidates_test(&t, &[]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
|
||||
.await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls update_authservid_candidates(), meant for using in a test.
|
||||
///
|
||||
/// update_authservid_candidates() only looks at the keys of its
|
||||
/// `authentication_results` parameter. So, this function takes `incoming_ids`
|
||||
/// and adds some AuthenticationResults to get the HashMap we need.
|
||||
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
|
||||
let v = incoming_ids
|
||||
.iter()
|
||||
.map(|id| (id.to_string(), DkimResult::Passed))
|
||||
.collect();
|
||||
update_authservid_candidates(context, &v).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_realworld_authentication_results() -> Result<()> {
|
||||
let mut test_failed = false;
|
||||
|
||||
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
for entry in dir {
|
||||
if !entry.file_type().await.unwrap().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let self_addr = entry.file_name().into_string().unwrap();
|
||||
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
|
||||
let authres_parsing_works = [
|
||||
"ik.me",
|
||||
"web.de",
|
||||
"posteo.de",
|
||||
"gmail.com",
|
||||
"hotmail.com",
|
||||
"mail.ru",
|
||||
"aol.com",
|
||||
"yahoo.com",
|
||||
"icloud.com",
|
||||
"fastmail.com",
|
||||
"mail.de",
|
||||
"outlook.com",
|
||||
"gmx.de",
|
||||
"testrun.org",
|
||||
]
|
||||
.contains(&self_domain.as_str());
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr(&self_addr).await;
|
||||
if !authres_parsing_works {
|
||||
println!("========= Receiving as {} =========", &self_addr);
|
||||
}
|
||||
|
||||
// Simulate receiving all emails once, so that we have the correct authserv-ids
|
||||
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
|
||||
|
||||
// The ordering in which the emails are received can matter;
|
||||
// the test _should_ pass for every ordering.
|
||||
dir.sort_by_key(|d| d.file_name());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
bytes.clear();
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from).await?;
|
||||
let from_domain = EmailAddress::new(from).unwrap().domain;
|
||||
|
||||
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
|
||||
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
|
||||
// These are (fictional) forged emails where the attacker added a fake
|
||||
// Authentication-Results before sending the email
|
||||
&& from != "forged-authres-added@example.com"
|
||||
// Other forged emails
|
||||
&& !from.starts_with("forged");
|
||||
|
||||
if res.dkim_passed != expected_result {
|
||||
if authres_parsing_works {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
|
||||
entry.path(),
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
println!("From {}: {}", from_domain, res.dkim_passed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!test_failed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_authres() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
|
||||
// return an Err because this would prevent the message from being added
|
||||
// to the database and downloaded again and again
|
||||
let bytes = b"From: invalid@from.com
|
||||
Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Bob knows his server's authserv-id
|
||||
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
|
||||
.await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.error.is_none());
|
||||
|
||||
// Do the same without the mailing list header, this time the failed
|
||||
// authres isn't ignored
|
||||
let mut sent = alice
|
||||
.send_text(alice_bob_chat.id, "hellooo without mailing list")
|
||||
.await;
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
// The message info should contain a warning:
|
||||
assert!(
|
||||
rcvd.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("DKIM Results: Passed=false")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -284,6 +284,10 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
|
||||
/// image, `*viewtype` is set to [`Viewtype::Image`].
|
||||
///
|
||||
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
|
||||
/// image is a true sticker assuming that it must have at least one fully transparent corner,
|
||||
/// otherwise `*viewtype` is set to [`Viewtype::Image`].
|
||||
pub async fn check_or_recode_image(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
|
||||
@@ -445,6 +445,7 @@ async fn test_recode_image_balanced_png() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
@@ -452,7 +453,6 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
res_viewtype: Some(Viewtype::Sticker),
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
@@ -734,6 +734,8 @@ async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
let chat = alice.get_self_chat().await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
||||
// extension.
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
193
src/chat.rs
193
src/chat.rs
@@ -6,6 +6,7 @@ use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::marker::Sync;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
@@ -22,9 +23,8 @@ use crate::chatlist_events;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
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,
|
||||
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 +34,7 @@ use crate::download::{
|
||||
};
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
@@ -2466,7 +2466,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let mut maybe_image = false;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::File
|
||||
|| msg.viewtype == Viewtype::Image
|
||||
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
|
||||
{
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
@@ -2474,7 +2477,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
|
||||
if better_type == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::Sticker {
|
||||
if better_type != Viewtype::Image {
|
||||
// UIs don't want conversions of `Sticker` to anything other than `Image`.
|
||||
msg.param.set_int(Param::ForceSticker, 1);
|
||||
}
|
||||
} else if better_type == Viewtype::Image {
|
||||
maybe_image = true;
|
||||
} else if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
@@ -2494,7 +2502,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::File && maybe_image
|
||||
|| msg.viewtype == Viewtype::Image
|
||||
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
|
||||
{
|
||||
let new_name = blob
|
||||
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
|
||||
.await?;
|
||||
@@ -2935,19 +2946,17 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
SELECT MAX(timestamp) FROM msgs 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=? AND
|
||||
id<=?
|
||||
chat_id=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
@@ -3096,6 +3105,9 @@ async fn donation_request_maybe(context: &Context) -> Result<()> {
|
||||
/// Chat message list request options.
|
||||
#[derive(Debug)]
|
||||
pub struct MessageListOptions {
|
||||
/// Return only info messages.
|
||||
pub info_only: bool,
|
||||
|
||||
/// Add day markers before each date regarding the local timezone.
|
||||
pub add_daymarker: bool,
|
||||
}
|
||||
@@ -3106,27 +3118,56 @@ pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<Cha
|
||||
context,
|
||||
chat_id,
|
||||
MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns messages belonging to the chat according to the given options,
|
||||
/// sorted by oldest message first.
|
||||
/// Returns messages belonging to the chat according to the given options.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn get_chat_msgs_ex(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
options: MessageListOptions,
|
||||
) -> Result<Vec<ChatItem>> {
|
||||
let MessageListOptions { add_daymarker } = options;
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
Ok((
|
||||
row.get::<_, i64>("timestamp")?,
|
||||
row.get::<_, MsgId>("id")?,
|
||||
false,
|
||||
))
|
||||
let MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
} = options;
|
||||
let process_row = if info_only {
|
||||
|row: &rusqlite::Row| {
|
||||
// is_info logic taken from Message.is_info()
|
||||
let params = row.get::<_, String>("param")?;
|
||||
let (from_id, to_id) = (
|
||||
row.get::<_, ContactId>("from_id")?,
|
||||
row.get::<_, ContactId>("to_id")?,
|
||||
);
|
||||
let is_info_msg: bool = from_id == ContactId::INFO
|
||||
|| to_id == ContactId::INFO
|
||||
|| match Params::from_str(¶ms) {
|
||||
Ok(p) => {
|
||||
let cmd = p.get_cmd();
|
||||
cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
Ok((
|
||||
row.get::<_, i64>("timestamp")?,
|
||||
row.get::<_, MsgId>("id")?,
|
||||
!is_info_msg,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
|row: &rusqlite::Row| {
|
||||
Ok((
|
||||
row.get::<_, i64>("timestamp")?,
|
||||
row.get::<_, MsgId>("id")?,
|
||||
false,
|
||||
))
|
||||
}
|
||||
};
|
||||
let process_rows = |rows: rusqlite::AndThenRows<_>| {
|
||||
// It is faster to sort here rather than
|
||||
@@ -3161,18 +3202,39 @@ pub async fn get_chat_msgs_ex(
|
||||
Ok(ret)
|
||||
};
|
||||
|
||||
let items = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
let items = if info_only {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
// GLOB is used here instead of LIKE because it is case-sensitive
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND (
|
||||
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
|
||||
OR m.from_id == ?
|
||||
OR m.to_id == ?
|
||||
);",
|
||||
(chat_id, ContactId::INFO, ContactId::INFO),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0;",
|
||||
(chat_id,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?;
|
||||
(chat_id,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
@@ -3947,47 +4009,9 @@ 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.
|
||||
@@ -4651,26 +4675,10 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
Ok(msg.rfc724_mid)
|
||||
}
|
||||
|
||||
/// Resends given messages to members of the corresponding chats.
|
||||
/// Resends given messages with the same Message-ID.
|
||||
///
|
||||
/// 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?;
|
||||
@@ -4689,17 +4697,10 @@ pub(crate) async fn resend_msgs_ex(
|
||||
| MessageState::OutFailed
|
||||
| MessageState::OutDelivered
|
||||
| MessageState::OutMdnRcvd => {
|
||||
// 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?;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -4711,8 +4712,7 @@ pub(crate) async fn resend_msgs_ex(
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
// 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.
|
||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
@@ -4905,6 +4905,8 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
|
||||
// no wrong information are shown in the device chat
|
||||
// - deletion in `devmsglabels` makes sure,
|
||||
// deleted messages are reset and useful messages can be added again
|
||||
// - we reset the config-option `QuotaExceeding`
|
||||
// that is used as a helper to drive the corresponding device message.
|
||||
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
@@ -4920,6 +4922,9 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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, N_MSGS_TO_NEW_BROADCAST_MEMBER};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
@@ -2032,6 +2032,12 @@ async fn test_classic_email_chat() -> Result<()> {
|
||||
let msgs = get_chat_msgs(&alice, chat_id).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// Alice disables receiving classic emails.
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("0"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Already received classic email should still be in the chat.
|
||||
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
|
||||
@@ -2069,7 +2075,13 @@ async fn test_chat_get_color_encrypted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
|
||||
async fn test_sticker(
|
||||
filename: &str,
|
||||
bytes: &[u8],
|
||||
res_viewtype: Viewtype,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
@@ -2085,7 +2097,7 @@ async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_viewtype(), res_viewtype);
|
||||
assert_eq!(msg.get_filename().unwrap(), filename);
|
||||
assert_eq!(msg.get_width(), w);
|
||||
assert_eq!(msg.get_height(), h);
|
||||
@@ -2099,6 +2111,7 @@ async fn test_sticker_png() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.png",
|
||||
include_bytes!("../../test-data/image/logo.png"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
@@ -2110,6 +2123,7 @@ async fn test_sticker_jpeg() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.jpg",
|
||||
include_bytes!("../../test-data/image/avatar1000x1000.jpg"),
|
||||
Viewtype::Image,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
@@ -2117,33 +2131,10 @@ async fn test_sticker_jpeg() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../../test-data/image/logo.gif"),
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Tests that stickers are sent as stickers.
|
||||
///
|
||||
/// Previously there was heuristic that stickers
|
||||
/// were sometimes turned into non-stickers,
|
||||
/// e.g. when it looked like UI sent
|
||||
/// a screenshot dragged from the gallery into chat
|
||||
/// as a sticker.
|
||||
///
|
||||
/// We have no such heuristic anymore,
|
||||
/// if such heuristic is needed on some platform,
|
||||
/// UI code should implement it.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_no_heuristics() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
async fn test_sticker_jpeg_force() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
let file = alice.get_blobdir().join("sticker.jpg");
|
||||
tokio::fs::write(
|
||||
@@ -2153,38 +2144,53 @@ async fn test_sticker_no_heuristics() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a sticker.
|
||||
// Images without force_sticker should be turned into [Viewtype::Image]
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
let file = msg.get_file(alice).unwrap();
|
||||
let file = msg.get_file(&alice).unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Image);
|
||||
|
||||
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
msg.force_sticker();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
|
||||
// Send a sticker reusing the file.
|
||||
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
|
||||
// even on drafted messages
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
|
||||
// Set sticker as a draft, then send it.
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
msg.force_sticker();
|
||||
alice_chat
|
||||
.id
|
||||
.set_draft(alice, Some(&mut msg))
|
||||
.set_draft(&alice, Some(&mut msg))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut msg = alice_chat.id.get_draft(alice).await.unwrap().unwrap();
|
||||
let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../../test-data/image/logo.gif"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_forward() -> Result<()> {
|
||||
// create chats
|
||||
@@ -2686,49 +2692,6 @@ 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();
|
||||
@@ -2842,15 +2805,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -2993,56 +2947,6 @@ 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
|
||||
@@ -5816,7 +5720,7 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Bob receives both messages and has nothing at the end
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
|
||||
@@ -5824,11 +5728,6 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// ... even if he receives messages in reverse order.
|
||||
let bob2 = &tcm.bob().await;
|
||||
bob2.recv_msg_opt(&sent2).await;
|
||||
assert!(bob2.recv_msg_opt(&sent1).await.is_none());
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
|
||||
139
src/config.rs
139
src/config.rs
@@ -42,85 +42,50 @@ 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(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
/// Deprecated option for backwards compatibility.
|
||||
///
|
||||
/// 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.
|
||||
@@ -190,6 +155,10 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
/// Whether to show classic emails or only chat messages.
|
||||
#[strum(props(default = "2"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
/// Quality of the media files to send.
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
@@ -216,47 +185,32 @@ 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,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server port.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server username.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
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.
|
||||
@@ -265,53 +219,37 @@ 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(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
/// Deprecated, stored for backwards compatibility.
|
||||
///
|
||||
/// ConfiguredImapCertificateChecks is actually used.
|
||||
ConfiguredSmtpCertificateChecks,
|
||||
|
||||
/// Whether OAuth 2 is used with configured provider.
|
||||
ConfiguredServerFlags,
|
||||
|
||||
@@ -324,9 +262,6 @@ 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,
|
||||
|
||||
@@ -367,6 +302,11 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// If a warning about exceeding quota was shown recently,
|
||||
/// this is the percentage of quota at the time the warning was given.
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
@@ -407,6 +347,12 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
/// may be the one of our email server.
|
||||
///
|
||||
/// See `crate::authres::update_authservid_candidates`.
|
||||
AuthservIdCandidates,
|
||||
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
@@ -466,10 +412,6 @@ pub enum Config {
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Make `TestContext::pop_sent_msg_opt()` and related functions pop messages from the `smtp`
|
||||
/// head, i.e. make it a queue. For historical reasons the default is a stack.
|
||||
PopSentMsgFromHead,
|
||||
|
||||
/// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`.
|
||||
/// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
|
||||
/// using this still run unmodified code.
|
||||
@@ -508,7 +450,11 @@ impl Config {
|
||||
pub(crate) fn is_synced(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::ShowEmails
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -949,10 +895,15 @@ impl Context {
|
||||
/// Returns `false` if no addresses are configured.
|
||||
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
Ok(self
|
||||
.get_all_self_addrs()
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
.any(|a| addr_cmp(addr, a))
|
||||
|| self
|
||||
.get_secondary_self_addrs()
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
}
|
||||
|
||||
/// Sets `primary_new` as the new primary self address and saves the old
|
||||
@@ -999,6 +950,14 @@ impl Context {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all secondary self addresses.
|
||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Returns all published secondary self addresses.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
|
||||
@@ -196,6 +196,13 @@ async fn test_sync() -> Result<()> {
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
|
||||
|
||||
{
|
||||
let val = alice0.get_config_bool(Config::ShowEmails).await?;
|
||||
alice0.set_config_bool(Config::ShowEmails, !val).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(Config::ShowEmails).await?, !val);
|
||||
}
|
||||
|
||||
// `Config::SyncMsgs` mustn't be synced.
|
||||
alice0.set_config_bool(Config::SyncMsgs, false).await?;
|
||||
alice0.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
@@ -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_legacy(self).await?;
|
||||
let mut param = EnteredLoginParam::load(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_legacy(self).await?;
|
||||
param.save(self).await?;
|
||||
progress!(self, 1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,17 @@ pub enum Blocked {
|
||||
Request = 2,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
AcceptedContacts = 1,
|
||||
#[default] // also change Config.ShowEmails props(default) on changes
|
||||
All = 2,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
@@ -233,9 +244,6 @@ 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;
|
||||
@@ -251,6 +259,18 @@ mod tests {
|
||||
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(ShowEmails::All, ShowEmails::default());
|
||||
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
ShowEmails::AcceptedContacts,
|
||||
ShowEmails::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocked_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
@@ -568,6 +568,15 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests deletion of all messages from chatmail relays.
|
||||
///
|
||||
/// Non-chatmail relays are excluded
|
||||
/// to avoid accidentally deleting emails
|
||||
/// from shared inboxes.
|
||||
pub async fn clear_all_relay_storage(&self) -> Result<()> {
|
||||
self.scheduler.clear_all_relay_storage().await
|
||||
}
|
||||
|
||||
/// Restarts the IO scheduler if it was running before
|
||||
/// when it is not running this is an no-op
|
||||
pub async fn restart_io_if_running(&self) {
|
||||
@@ -843,7 +852,7 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let all_self_addrs = self.get_all_self_addrs().await?.join(", ");
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -945,7 +954,11 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
res.insert("all_self_addrs", all_self_addrs);
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"who_can_call_me",
|
||||
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
|
||||
@@ -991,6 +1004,18 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"quota_exceeding",
|
||||
self.get_config_int(Config::QuotaExceeding)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"authserv_id_candidates",
|
||||
self.get_config(Config::AuthservIdCandidates)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"sign_unencrypted",
|
||||
self.get_config_int(Config::SignUnencrypted)
|
||||
@@ -1044,13 +1069,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"pop_sent_msg_from_head",
|
||||
self.sql
|
||||
.get_raw_config("pop_sent_msg_from_head")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"test_hooks",
|
||||
self.sql
|
||||
|
||||
@@ -284,6 +284,7 @@ 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",
|
||||
@@ -602,7 +603,10 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(alice.get_config(Config::Displayname).await?, None);
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
@@ -610,21 +614,24 @@ async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('displayname', 'Alice 2')",
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(alice.get_config(Config::Displayname).await?, None);
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::Displayname).await?,
|
||||
Some("Alice 2".to_string())
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -234,7 +234,8 @@ pub enum EventType {
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed, this parameter is set to `None`.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
|
||||
11
src/html.rs
11
src/html.rs
@@ -287,6 +287,7 @@ impl MsgId {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{self, Chat, forward_msgs, save_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
@@ -554,7 +555,13 @@ test some special html-characters as < > and & but also " and &#x
|
||||
async fn test_html_forwarding_encrypted() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
// Alice receives a non-delta html-message
|
||||
// (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known
|
||||
// contacts, the contact is marked as known by creating a chat using `chat_with_contact()`)
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
@@ -572,6 +579,10 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// receive the message on another device
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("0"))
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.recv_msg(&msg).await;
|
||||
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
|
||||
assert_eq!(msg.get_from_id(), ContactId::SELF);
|
||||
|
||||
27
src/imap.rs
27
src/imap.rs
@@ -489,7 +489,7 @@ impl Imap {
|
||||
let session = match self.connect(context, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
self.connectivity.set_err(context, format!("{err:#}"));
|
||||
self.connectivity.set_err(context, &err);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
@@ -945,6 +945,29 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes all messages from IMAP folder.
|
||||
pub(crate) async fn delete_all_messages(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
if self.select_with_uidvalidity(context, folder).await? {
|
||||
self.add_flag_finalized_with_set("1:*", "\\Deleted").await?;
|
||||
self.selected_folder_needs_expunge = true;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves batch of messages identified by their UID from the currently
|
||||
/// selected folder to the target folder.
|
||||
async fn move_message_batch(
|
||||
@@ -2001,7 +2024,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let should_download = !blocked_contact || maybe_ndn;
|
||||
let should_download = (!blocked_contact) || maybe_ndn;
|
||||
Ok(should_download)
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ mod update_helper;
|
||||
pub mod webxdc;
|
||||
#[macro_use]
|
||||
mod dehtml;
|
||||
mod authres;
|
||||
pub mod color;
|
||||
pub mod html;
|
||||
pub mod net;
|
||||
|
||||
103
src/location.rs
103
src/location.rs
@@ -264,11 +264,15 @@ impl Kml {
|
||||
|
||||
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> Result<()> {
|
||||
pub async fn send_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
seconds: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(seconds >= 0);
|
||||
ensure!(!chat_id.is_special());
|
||||
let now = time();
|
||||
let is_sending_locations_before = is_sending_to_chat(context, chat_id).await?;
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -301,49 +305,35 @@ pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether any chat is sending locations.
|
||||
pub async fn is_sending(context: &Context) -> Result<bool> {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?",
|
||||
(time(),),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether `chat_id` is sending locations.
|
||||
pub async fn is_sending_to_chat(context: &Context, chat_id: ChatId) -> Result<bool> {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?",
|
||||
(chat_id, time()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a list of chats in which location streaming is enabled.
|
||||
async fn get_chats_with_location_streaming(context: &Context) -> Result<Vec<ChatId>> {
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?",
|
||||
(time(),),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stop sending locations in all chats.
|
||||
pub async fn stop_sending(context: &Context) -> Result<()> {
|
||||
for chat_id in get_chats_with_location_streaming(context).await? {
|
||||
send_to_chat(context, chat_id, 0).await?;
|
||||
}
|
||||
Ok(())
|
||||
/// Returns whether `chat_id` or any chat is sending locations.
|
||||
///
|
||||
/// If `chat_id` is `Some` only that chat is checked, otherwise returns `true` if any chat
|
||||
/// is sending locations.
|
||||
pub async fn is_sending_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: Option<ChatId>,
|
||||
) -> Result<bool> {
|
||||
let exists = match chat_id {
|
||||
Some(chat_id) => {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
(chat_id, time()),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||
(time(),),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Sets current location of the user device.
|
||||
@@ -469,6 +459,13 @@ fn is_marker(txt: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes all locations from the database.
|
||||
pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
context.sql.execute("DELETE FROM locations;", ()).await?;
|
||||
context.emit_location_changed(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes expired locations.
|
||||
///
|
||||
/// Only path locations are deleted.
|
||||
@@ -498,7 +495,7 @@ pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
|
||||
///
|
||||
/// This function is used when a message is deleted
|
||||
/// that has a corresponding `location_id`.
|
||||
pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()> {
|
||||
pub(crate) async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -510,7 +507,7 @@ pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()
|
||||
}
|
||||
|
||||
/// Deletes POI locations that don't have corresponding message anymore.
|
||||
pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> {
|
||||
pub(crate) async fn delete_orphaned_poi_locations(context: &Context) -> Result<()> {
|
||||
context.sql.execute("
|
||||
DELETE FROM locations
|
||||
WHERE independent=1 AND id NOT IN
|
||||
@@ -719,9 +716,9 @@ pub(crate) async fn save(
|
||||
|
||||
pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
|
||||
loop {
|
||||
let next_event = match maybe_send(context).await {
|
||||
let next_event = match maybe_send_locations(context).await {
|
||||
Err(err) => {
|
||||
warn!(context, "location::maybe_send failed: {:#}", err);
|
||||
warn!(context, "maybe_send_locations failed: {:#}", err);
|
||||
Some(60) // Retry one minute later.
|
||||
}
|
||||
Ok(next_event) => next_event,
|
||||
@@ -759,7 +756,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
|
||||
/// Returns number of seconds until the next time location streaming for some chat ends
|
||||
/// automatically.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn maybe_send(context: &Context) -> Result<Option<u64>> {
|
||||
async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
let mut next_event: Option<u64> = None;
|
||||
|
||||
let now = time();
|
||||
@@ -1054,7 +1051,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
send_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
|
||||
@@ -1106,7 +1103,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
|
||||
// Alice enables location streaming.
|
||||
// Bob receives a message saying that Alice enabled location streaming.
|
||||
send_to_chat(alice, alice_chat.id, 60).await?;
|
||||
send_locations_to_chat(alice, alice_chat.id, 60).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Alice gets new location from GPS.
|
||||
@@ -1116,7 +1113,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
// 10 seconds later location sending stream manages to send location.
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
delete_expired(alice, time()).await?;
|
||||
maybe_send(alice).await?;
|
||||
maybe_send_locations(alice).await?;
|
||||
bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
|
||||
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
|
||||
|
||||
@@ -138,11 +138,10 @@ pub struct EnteredLoginParam {
|
||||
}
|
||||
|
||||
impl EnteredLoginParam {
|
||||
/// Loads entered account settings
|
||||
/// that were set by the deprecated `configured_*` configs.
|
||||
/// Loads entered account settings.
|
||||
///
|
||||
/// This is only needed by tests and clients using the old CFFI API.
|
||||
pub(crate) async fn load_legacy(context: &Context) -> Result<Self> {
|
||||
/// This is a legacy API for loading from separate config parameters.
|
||||
pub(crate) async fn load(context: &Context) -> Result<Self> {
|
||||
let addr = context
|
||||
.get_config(Config::Addr)
|
||||
.await?
|
||||
@@ -179,7 +178,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` has been removed.
|
||||
// while `smtp_certificate_checks` is ignored.
|
||||
let certificate_checks = if let Some(certificate_checks) = context
|
||||
.get_config_parsed::<i32>(Config::ImapCertificateChecks)
|
||||
.await?
|
||||
@@ -242,10 +241,7 @@ impl EnteredLoginParam {
|
||||
|
||||
/// Saves entered account settings,
|
||||
/// so that they can be prefilled if the user wants to configure the server again.
|
||||
///
|
||||
/// 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<()> {
|
||||
pub(crate) async fn save(&self, context: &Context) -> Result<()> {
|
||||
context.set_config(Config::Addr, Some(&self.addr)).await?;
|
||||
|
||||
context
|
||||
@@ -368,7 +364,7 @@ mod tests {
|
||||
.await?;
|
||||
t.set_config(Config::MailPw, Some("foobarbaz")).await?;
|
||||
|
||||
let param = EnteredLoginParam::load_legacy(t).await?;
|
||||
let param = EnteredLoginParam::load(t).await?;
|
||||
assert_eq!(param.addr, "alice@example.org");
|
||||
assert_eq!(
|
||||
param.certificate_checks,
|
||||
@@ -377,13 +373,13 @@ mod tests {
|
||||
|
||||
t.set_config(Config::ImapCertificateChecks, Some("1"))
|
||||
.await?;
|
||||
let param = EnteredLoginParam::load_legacy(t).await?;
|
||||
let param = EnteredLoginParam::load(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_legacy(t).await.is_err());
|
||||
assert!(EnteredLoginParam::load(t).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -411,7 +407,7 @@ mod tests {
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
};
|
||||
param.save_legacy(&t).await?;
|
||||
param.save(&t).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
@@ -420,7 +416,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_legacy(&t).await?, param);
|
||||
assert_eq!(EnteredLoginParam::load(&t).await?, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::location;
|
||||
use crate::location::delete_poi_location;
|
||||
use crate::log::warn;
|
||||
use crate::mimeparser::{SystemMessage, parse_message_id};
|
||||
use crate::param::{Param, Params};
|
||||
@@ -529,7 +529,7 @@ impl Message {
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON c.id=m.chat_id
|
||||
LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE m.id=? AND chat_id!=3 -- DC_CHAT_ID_TRASH
|
||||
WHERE m.id=? AND chat_id!=3
|
||||
LIMIT 1",
|
||||
(id,),
|
||||
|row| {
|
||||
@@ -739,7 +739,7 @@ impl Message {
|
||||
/// at a position different from the self-location.
|
||||
/// You should not call this function
|
||||
/// if you want to bind the current self-location to a message;
|
||||
/// this is done by [`location::set()`] and [`location::send_to_chat()`].
|
||||
/// this is done by [`location::set()`] and [`send_locations_to_chat()`].
|
||||
///
|
||||
/// Typically results in the event [`LocationChanged`] with
|
||||
/// `contact_id` set to [`ContactId::SELF`].
|
||||
@@ -748,7 +748,7 @@ impl Message {
|
||||
/// `longitude` is the East-west position of the location.
|
||||
///
|
||||
/// [`location::set()`]: crate::location::set
|
||||
/// [`location::send_to_chat()`]: crate::location::send_to_chat
|
||||
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
|
||||
/// [`LocationChanged`]: crate::events::EventType::LocationChanged
|
||||
pub fn set_location(&mut self, latitude: f64, longitude: f64) {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
@@ -795,6 +795,12 @@ impl Message {
|
||||
self.viewtype
|
||||
}
|
||||
|
||||
/// Forces the message to **keep** [Viewtype::Sticker]
|
||||
/// e.g the message will not be converted to a [Viewtype::Image].
|
||||
pub fn force_sticker(&mut self) {
|
||||
self.param.set_int(Param::ForceSticker, 1);
|
||||
}
|
||||
|
||||
/// Returns the state of the message.
|
||||
pub fn get_state(&self) -> MessageState {
|
||||
self.state
|
||||
@@ -1649,7 +1655,7 @@ pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result
|
||||
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
|
||||
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
|
||||
if msg.location_id > 0 {
|
||||
location::delete_poi(context, msg.location_id).await?;
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg.id
|
||||
@@ -1901,10 +1907,9 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
//
|
||||
// We also don't send read receipts for contact requests.
|
||||
// Read receipts will not be sent even after accepting the chat.
|
||||
let wants_mdn = curr_param.get_bool(Param::WantsMdn).unwrap_or_default();
|
||||
let to_id = if curr_blocked == Blocked::Not
|
||||
&& !curr_hidden
|
||||
&& wants_mdn
|
||||
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& curr_param.get_cmd() == SystemMessage::Unknown
|
||||
&& context.should_send_mdns().await?
|
||||
{
|
||||
@@ -1926,11 +1931,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
None
|
||||
};
|
||||
if let Some(to_id) = to_id {
|
||||
info!(
|
||||
context,
|
||||
"Queuing MDN to {to_id} for {id} from {curr_from_id}, wants_mdn={wants_mdn}, cmd={}.",
|
||||
curr_param.get_cmd()
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -2123,6 +2123,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
|
||||
/// Count messages older than the given number of `seconds`.
|
||||
///
|
||||
/// Returns the number of messages that are older than the given number of seconds.
|
||||
/// This includes e-mails downloaded due to the `show_emails` option.
|
||||
/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn estimate_deletion_cnt(
|
||||
@@ -2321,6 +2322,8 @@ pub enum Viewtype {
|
||||
Gif = 21,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
|
||||
@@ -194,7 +194,6 @@ 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();
|
||||
@@ -1395,7 +1394,10 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||
if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
{
|
||||
headers.push((
|
||||
"Chat-Group-Name",
|
||||
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
|
||||
@@ -1406,11 +1408,7 @@ 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();
|
||||
@@ -1829,7 +1827,7 @@ impl MimeFactory {
|
||||
parts.push(msg_kml_part);
|
||||
}
|
||||
|
||||
if location::is_sending_to_chat(context, msg.chat_id).await?
|
||||
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
|
||||
&& let Some(part) = self.get_location_kml_part(context).await?
|
||||
{
|
||||
parts.push(part);
|
||||
@@ -2227,18 +2225,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 {
|
||||
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
|
||||
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));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
|
||||
|
||||
@@ -506,6 +506,11 @@ async fn msg_to_subject_str_inner(
|
||||
|
||||
// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`.
|
||||
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
receive_imf(context, imf_raw, false).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -14,8 +14,9 @@ use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo, addrpars
|
||||
use mime::Mime;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::{ContactId, import_public_key};
|
||||
@@ -274,7 +275,7 @@ impl MimeMessage {
|
||||
let timestamp_rcvd = smeared_time(context);
|
||||
let mut timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
let mut hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut headers_removed = HashSet::<String>::new();
|
||||
@@ -365,7 +366,11 @@ impl MimeMessage {
|
||||
|
||||
let mut from = from.context("No from in message")?;
|
||||
|
||||
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
|
||||
|
||||
let mut gossiped_keys = Default::default();
|
||||
hop_info += "\n\n";
|
||||
hop_info += &dkim_results.to_string();
|
||||
|
||||
let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
|
||||
|
||||
@@ -951,16 +956,14 @@ impl MimeMessage {
|
||||
self.parse_attachments();
|
||||
|
||||
// See if an MDN is requested from the other side
|
||||
let mut wants_mdn = false;
|
||||
if self.decryption_error.is_none()
|
||||
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
|
||||
&& !self.parts.is_empty()
|
||||
&& let Some(ref dn_to) = self.chat_disposition_notification_to
|
||||
{
|
||||
// Check that the message is not outgoing.
|
||||
let from = &self.from.addr;
|
||||
if !context.is_self_addr(from).await? {
|
||||
if from.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
wants_mdn = true;
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
@@ -982,9 +985,7 @@ impl MimeMessage {
|
||||
typ: Viewtype::Text,
|
||||
..Default::default()
|
||||
};
|
||||
if wants_mdn {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
|
||||
if let Some(ref subject) = self.get_subject()
|
||||
&& !self.has_chat_version()
|
||||
&& self.webxdc_status_update.is_none()
|
||||
@@ -2496,8 +2497,6 @@ async fn handle_mdn(
|
||||
let Some((msg_id, chat_id, has_mdns, is_dup)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
// MDN on a pre-message references the post-message, see `receive_imf`. So we can't tell
|
||||
// which one was seen, but this is on purpose.
|
||||
"SELECT
|
||||
m.id AS msg_id,
|
||||
c.id AS chat_id,
|
||||
@@ -2583,10 +2582,6 @@ async fn handle_ndn(
|
||||
|
||||
for msg_id in msg_ids {
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.chat_id).await?;
|
||||
if chat.typ == constants::Chattype::OutBroadcast {
|
||||
continue;
|
||||
}
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
|
||||
@@ -136,10 +136,6 @@ 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',
|
||||
|
||||
@@ -247,6 +243,9 @@ pub enum Param {
|
||||
/// For Webxdc Message Instances: Chat to integrate the Webxdc for.
|
||||
WebxdcIntegrateFor = b'2',
|
||||
|
||||
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
|
||||
ForceSticker = b'X',
|
||||
|
||||
/// For messages: Message is a deletion request. The value is a list of rfc724_mid of the messages to delete.
|
||||
DeleteRequestFor = b'M',
|
||||
|
||||
|
||||
@@ -234,6 +234,34 @@ 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",
|
||||
@@ -275,6 +303,48 @@ 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",
|
||||
@@ -426,6 +496,22 @@ 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",
|
||||
@@ -543,10 +629,16 @@ static P_HERMES_RADIO: Provider = Provider {
|
||||
strict_tls: false,
|
||||
..ProviderOptions::new()
|
||||
},
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MdnsEnabled,
|
||||
value: "0",
|
||||
}]),
|
||||
config_defaults: Some(&[
|
||||
ConfigDefault {
|
||||
key: Config::MdnsEnabled,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::ShowEmails,
|
||||
value: "2",
|
||||
},
|
||||
]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -827,6 +919,90 @@ 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",
|
||||
@@ -1074,8 +1250,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::Preparation,
|
||||
before_login_hint: "You must create an app-specific password before you can log in.",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/posteo",
|
||||
server: &[
|
||||
@@ -1386,6 +1562,51 @@ 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",
|
||||
@@ -1783,7 +2004,7 @@ static P_ZOHO: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
|
||||
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
|
||||
("163.com", &P_163),
|
||||
("aktivix.org", &P_AKTIVIX_ORG),
|
||||
("aliyun.com", &P_ALIYUN),
|
||||
@@ -1793,9 +2014,11 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
|
||||
("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),
|
||||
@@ -1922,6 +2145,7 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
|
||||
("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),
|
||||
@@ -2144,6 +2368,15 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
|
||||
("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),
|
||||
@@ -2236,6 +2469,7 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
|
||||
("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),
|
||||
@@ -2318,8 +2552,10 @@ 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),
|
||||
@@ -2327,6 +2563,7 @@ 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),
|
||||
@@ -2344,6 +2581,8 @@ 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),
|
||||
@@ -2363,6 +2602,7 @@ 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),
|
||||
@@ -2382,4 +2622,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, 4, 21).unwrap());
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 1, 28).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` has been removed.
|
||||
// because `smtp_certificate_checks` is deprecated.
|
||||
assert_eq!(param.certificate_checks, EnteredCertificateChecks::Strict);
|
||||
|
||||
Ok(())
|
||||
|
||||
98
src/quota.rs
98
src/quota.rs
@@ -6,17 +6,33 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session as ImapSession;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::{EventType, stock_str};
|
||||
|
||||
/// quota icon in connectivity is "yellow".
|
||||
/// warn about a nearly full mailbox after this usage percentage is reached.
|
||||
/// quota icon is "yellow".
|
||||
pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
|
||||
|
||||
/// quota icon in connectivity is "red".
|
||||
/// warning again after this usage percentage is reached,
|
||||
/// quota icon is "red".
|
||||
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
|
||||
|
||||
/// if quota is below this value (again),
|
||||
/// QuotaExceeding is cleared.
|
||||
///
|
||||
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
|
||||
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
|
||||
///
|
||||
/// We do not repeat warnings on a daily base or so as some provider
|
||||
/// providers report bad values and we would then spam the user.
|
||||
pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
|
||||
|
||||
/// Server quota information with an update timestamp.
|
||||
#[derive(Debug)]
|
||||
pub struct QuotaInfo {
|
||||
@@ -54,6 +70,37 @@ async fn get_unique_quota_roots_and_usage(
|
||||
Ok(unique_quota_roots)
|
||||
}
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &'t QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
for r in resources {
|
||||
let usage_percent = r.get_usage_percentage();
|
||||
match highest {
|
||||
None => {
|
||||
highest = Some((usage_percent, name, r));
|
||||
}
|
||||
Some((up, ..)) => {
|
||||
if up <= usage_percent {
|
||||
highest = Some((usage_percent, name, r));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
highest.context("no quota_resource found, this is unexpected")
|
||||
}
|
||||
|
||||
/// Checks if a quota warning is needed.
|
||||
pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> bool {
|
||||
(curr_percentage >= QUOTA_WARN_THRESHOLD_PERCENTAGE
|
||||
&& warned_at_percentage < QUOTA_WARN_THRESHOLD_PERCENTAGE)
|
||||
|| (curr_percentage >= QUOTA_ERROR_THRESHOLD_PERCENTAGE
|
||||
&& warned_at_percentage < QUOTA_ERROR_THRESHOLD_PERCENTAGE)
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
||||
/// called.
|
||||
@@ -87,6 +134,32 @@ impl Context {
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self)))
|
||||
};
|
||||
|
||||
if let Ok(quota) = "a {
|
||||
match get_highest_usage(quota) {
|
||||
Ok((highest, _, _)) => {
|
||||
if needs_quota_warning(
|
||||
highest,
|
||||
self.get_config_int(Config::QuotaExceeding).await? as u64,
|
||||
) {
|
||||
self.set_config_internal(
|
||||
Config::QuotaExceeding,
|
||||
Some(&highest.to_string()),
|
||||
)
|
||||
.await?;
|
||||
let mut msg = Message::new_text(stock_str::quota_exceeding(self, highest));
|
||||
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
|
||||
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
|
||||
self.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(err) => warn!(
|
||||
self,
|
||||
"Transport {transport_id}: Cannot get highest quota usage: {err:#}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
self.quota.write().await.insert(
|
||||
transport_id,
|
||||
QuotaInfo {
|
||||
@@ -106,10 +179,29 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_needs_quota_warning() -> Result<()> {
|
||||
assert!(!needs_quota_warning(0, 0));
|
||||
assert!(!needs_quota_warning(10, 0));
|
||||
assert!(!needs_quota_warning(70, 0));
|
||||
assert!(!needs_quota_warning(75, 0));
|
||||
assert!(!needs_quota_warning(79, 0));
|
||||
assert!(needs_quota_warning(80, 0));
|
||||
assert!(needs_quota_warning(81, 0));
|
||||
assert!(!needs_quota_warning(85, 80));
|
||||
assert!(!needs_quota_warning(85, 81));
|
||||
assert!(needs_quota_warning(95, 82));
|
||||
assert!(!needs_quota_warning(97, 95));
|
||||
assert!(!needs_quota_warning(97, 96));
|
||||
assert!(!needs_quota_warning(1000, 96));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::assertions_on_constants)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_thresholds() -> anyhow::Result<()> {
|
||||
assert!(0 < QUOTA_WARN_THRESHOLD_PERCENTAGE);
|
||||
assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
|
||||
assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE);
|
||||
assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
|
||||
assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
|
||||
Ok(())
|
||||
|
||||
@@ -11,13 +11,14 @@ use deltachat_contact_tools::{
|
||||
sanitize_single_line,
|
||||
};
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::chat::{
|
||||
self, Chat, ChatId, ChatIdBlocked, ChatVisibility, is_contact_in_chat, save_broadcast_secret,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX};
|
||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
@@ -524,15 +525,52 @@ pub(crate) async fn receive_imf_inner(
|
||||
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
|
||||
);
|
||||
|
||||
// These checks must be done before processing of SecureJoin and other special messages.
|
||||
// check, if the mail is already in our database.
|
||||
// make sure, this check is done eg. before securejoin-processing.
|
||||
let (replace_msg_id, replace_chat_id);
|
||||
if mime_parser.pre_message == mimeparser::PreMessageMode::Post {
|
||||
// Post-Message just replaces the attachment and modifies Params, not the whole message.
|
||||
// This is done in the `handle_post_message` method.
|
||||
} else if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await? {
|
||||
info!(
|
||||
context,
|
||||
"Message {rfc724_mid} is already in some chat or deleted."
|
||||
);
|
||||
replace_msg_id = None;
|
||||
replace_chat_id = None;
|
||||
} else if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
// This code handles the download of old partial download stub messages
|
||||
// It will be removed after a transitioning period,
|
||||
// after we have released a few versions with pre-messages
|
||||
replace_msg_id = Some(old_msg_id);
|
||||
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
|
||||
.await?
|
||||
.filter(|msg| msg.download_state() != DownloadState::Done)
|
||||
{
|
||||
// The message was partially downloaded before.
|
||||
match mime_parser.pre_message {
|
||||
PreMessageMode::Post | PreMessageMode::None => {
|
||||
info!(context, "Message already partly in DB, replacing.");
|
||||
Some(msg.chat_id)
|
||||
}
|
||||
PreMessageMode::Pre { .. } => {
|
||||
info!(context, "Cannot replace pre-message with a pre-message");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The message was already fully downloaded
|
||||
// or cannot be loaded because it is deleted.
|
||||
None
|
||||
};
|
||||
} else {
|
||||
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
|
||||
None
|
||||
} else {
|
||||
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
|
||||
};
|
||||
replace_chat_id = None;
|
||||
}
|
||||
|
||||
if replace_chat_id.is_some() {
|
||||
// Need to update chat id in the db.
|
||||
} else if let Some(msg_id) = replace_msg_id {
|
||||
info!(context, "Message is already downloaded.");
|
||||
if mime_parser.incoming {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -551,7 +589,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let prevent_rename = should_prevent_rename(&mime_parser);
|
||||
|
||||
@@ -601,7 +639,8 @@ pub(crate) async fn receive_imf_inner(
|
||||
mime_parser.get_header(HeaderDef::References),
|
||||
mime_parser.get_header(HeaderDef::InReplyTo),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.filter(|p| Some(p.id) != replace_msg_id);
|
||||
|
||||
let mut chat_assignment =
|
||||
decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
|
||||
@@ -681,8 +720,20 @@ pub(crate) async fn receive_imf_inner(
|
||||
MessengerMessage::No
|
||||
};
|
||||
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default();
|
||||
|
||||
let allow_creation = if mime_parser.decryption_error.is_some() {
|
||||
false
|
||||
} else if is_dc_message == MessengerMessage::No
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
{
|
||||
// the message is a classic email in a classic profile
|
||||
// (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
|
||||
match show_emails {
|
||||
ShowEmails::Off | ShowEmails::AcceptedContacts => false,
|
||||
ShowEmails::All => true,
|
||||
}
|
||||
} else {
|
||||
!mime_parser.parts.iter().all(|part| part.is_reaction)
|
||||
};
|
||||
@@ -717,6 +768,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
rfc724_mid_orig,
|
||||
from_id,
|
||||
seen,
|
||||
replace_msg_id,
|
||||
prevent_rename,
|
||||
chat_id,
|
||||
chat_id_blocked,
|
||||
@@ -805,8 +857,6 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
if transport_changed {
|
||||
info!(context, "Primary transport changed to {from_addr:?}.");
|
||||
context.sql.uncache_raw_config("configured_addr").await;
|
||||
|
||||
// Regenerate User ID in V4 keys.
|
||||
context.self_public_key.lock().await.take();
|
||||
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
@@ -1011,6 +1061,11 @@ UPDATE msgs SET state=? WHERE
|
||||
.await?;
|
||||
} else if received_msg.hidden {
|
||||
// No need to emit an event about the changed message
|
||||
} else if let Some(replace_chat_id) = replace_chat_id {
|
||||
match replace_chat_id == chat_id {
|
||||
false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
|
||||
true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
|
||||
}
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = received_msg.state == MessageState::InFresh
|
||||
&& mime_parser.is_system_message != SystemMessage::CallAccepted
|
||||
@@ -1197,6 +1252,20 @@ async fn decide_chat_assignment(
|
||||
}
|
||||
info!(context, "Outgoing undecryptable message (TRASH).");
|
||||
true
|
||||
} else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
|
||||
&& !mime_parser.has_chat_version()
|
||||
&& parent_message
|
||||
.as_ref()
|
||||
.is_none_or(|p| p.is_dc_message == MessengerMessage::No)
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
&& ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default()
|
||||
== ShowEmails::Off
|
||||
{
|
||||
info!(context, "Classical email not shown (TRASH).");
|
||||
// the message is a classic email in a classic profile
|
||||
// (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
|
||||
true
|
||||
} else if mime_parser
|
||||
.get_header(HeaderDef::XMozillaDraftInfo)
|
||||
.is_some()
|
||||
@@ -1737,6 +1806,7 @@ async fn add_parts(
|
||||
rfc724_mid: &str,
|
||||
from_id: ContactId,
|
||||
seen: bool,
|
||||
mut replace_msg_id: Option<MsgId>,
|
||||
prevent_rename: bool,
|
||||
mut chat_id: ChatId,
|
||||
mut chat_id_blocked: Blocked,
|
||||
@@ -2117,6 +2187,22 @@ async fn add_parts(
|
||||
param.set_int(Param::Cmd, is_system_message as i32);
|
||||
}
|
||||
|
||||
if let Some(replace_msg_id) = replace_msg_id {
|
||||
let placeholder = Message::load_from_db(context, replace_msg_id)
|
||||
.await
|
||||
.context("Failed to load placeholder message")?;
|
||||
for key in [
|
||||
Param::WebxdcSummary,
|
||||
Param::WebxdcSummaryTimestamp,
|
||||
Param::WebxdcDocument,
|
||||
Param::WebxdcDocumentTimestamp,
|
||||
] {
|
||||
if let Some(value) = placeholder.param.get(key) {
|
||||
param.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
@@ -2159,9 +2245,10 @@ async fn add_parts(
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"
|
||||
r#"
|
||||
INSERT INTO msgs
|
||||
(
|
||||
id,
|
||||
rfc724_mid, pre_rfc724_mid, chat_id,
|
||||
from_id, to_id, timestamp, timestamp_sent,
|
||||
timestamp_rcvd, type, state, msgrmsg,
|
||||
@@ -2171,29 +2258,34 @@ INSERT INTO msgs
|
||||
ephemeral_timestamp, download_state, hop_info
|
||||
)
|
||||
VALUES (
|
||||
?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, 1,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?
|
||||
)",
|
||||
)?;
|
||||
let params = params![
|
||||
if let PreMessageMode::Pre {
|
||||
post_msg_rfc724_mid,
|
||||
..
|
||||
} = &mime_parser.pre_message
|
||||
{
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
|
||||
param=excluded.param,
|
||||
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
|
||||
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
|
||||
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
|
||||
RETURNING id
|
||||
"#)?;
|
||||
let row_id: MsgId = stmt.query_row(params![
|
||||
replace_msg_id,
|
||||
if let PreMessageMode::Pre {post_msg_rfc724_mid, ..} = &mime_parser.pre_message {
|
||||
post_msg_rfc724_mid
|
||||
} else {
|
||||
} else { rfc724_mid_orig },
|
||||
if let PreMessageMode::Pre {..} = &mime_parser.pre_message {
|
||||
rfc724_mid_orig
|
||||
},
|
||||
if let PreMessageMode::Pre { .. } = &mime_parser.pre_message {
|
||||
rfc724_mid_orig
|
||||
} else {
|
||||
""
|
||||
},
|
||||
} else { "" },
|
||||
if trash { DC_CHAT_ID_TRASH } else { chat_id },
|
||||
if trash { ContactId::UNDEFINED } else { from_id },
|
||||
if trash { ContactId::UNDEFINED } else { to_id },
|
||||
@@ -2202,27 +2294,13 @@ INSERT INTO msgs
|
||||
if trash { 0 } else { mime_parser.timestamp_rcvd },
|
||||
if trash {
|
||||
Viewtype::Unknown
|
||||
} else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
|
||||
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
|
||||
Viewtype::Text
|
||||
} else {
|
||||
typ
|
||||
},
|
||||
if trash {
|
||||
MessageState::Undefined
|
||||
} else {
|
||||
state
|
||||
},
|
||||
if trash {
|
||||
MessengerMessage::No
|
||||
} else {
|
||||
is_dc_message
|
||||
},
|
||||
} else { typ },
|
||||
if trash { MessageState::Undefined } else { state },
|
||||
if trash { MessengerMessage::No } else { is_dc_message },
|
||||
if trash || hidden { "" } else { msg },
|
||||
if trash || hidden {
|
||||
None
|
||||
} else {
|
||||
normalize_text(msg)
|
||||
},
|
||||
if trash || hidden { None } else { normalize_text(msg) },
|
||||
if trash || hidden { "" } else { &subject },
|
||||
if trash {
|
||||
"".to_string()
|
||||
@@ -2239,28 +2317,33 @@ INSERT INTO msgs
|
||||
if trash { "" } else { mime_in_reply_to },
|
||||
if trash { "" } else { mime_references },
|
||||
!trash && save_mime_modified,
|
||||
if trash {
|
||||
""
|
||||
} else {
|
||||
part.error.as_deref().unwrap_or_default()
|
||||
},
|
||||
if trash { "" } else { part.error.as_deref().unwrap_or_default() },
|
||||
if trash { 0 } else { ephemeral_timer.to_u32() },
|
||||
if trash { 0 } else { ephemeral_timestamp },
|
||||
if trash {
|
||||
DownloadState::Done
|
||||
} else if mime_parser.decryption_error.is_some() {
|
||||
DownloadState::Undecipherable
|
||||
} else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
|
||||
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
|
||||
DownloadState::Available
|
||||
} else {
|
||||
DownloadState::Done
|
||||
},
|
||||
if trash { "" } else { &mime_parser.hop_info },
|
||||
];
|
||||
let row_id = MsgId::new(stmt.insert(params)?.try_into()?);
|
||||
],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
}
|
||||
)?;
|
||||
Ok(row_id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// We only replace placeholder with a first part,
|
||||
// afterwards insert additional parts.
|
||||
replace_msg_id = None;
|
||||
|
||||
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
|
||||
created_db_entries.push(row_id);
|
||||
}
|
||||
@@ -2285,6 +2368,14 @@ INSERT INTO msgs
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(replace_msg_id) = replace_msg_id {
|
||||
// Trash the "replace" placeholder with a message that has no parts. If it has the original
|
||||
// "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
|
||||
// fully downloaded message later, the server-side deletion is issued.
|
||||
let on_server = rfc724_mid == rfc724_mid_orig;
|
||||
replace_msg_id.trash(context, on_server).await?;
|
||||
}
|
||||
|
||||
let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
Some(addr) => context.is_self_addr(addr).await?,
|
||||
None => true,
|
||||
@@ -2388,7 +2479,6 @@ async fn handle_edit_delete(
|
||||
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
for rfc724_mid in rfc724_mid_vec {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
if msg.from_id == from_id {
|
||||
@@ -2403,8 +2493,6 @@ async fn handle_edit_delete(
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
|
||||
insert_tombstone(context, rfc724_mid).await?;
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::imex::{ImexMode, imex};
|
||||
use crate::key;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{
|
||||
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
@@ -78,8 +78,9 @@ static GRP_MAIL: &[u8] =
|
||||
hello\n";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_is_shown() {
|
||||
async fn test_adhoc_group_show_chats_only() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("0")).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
@@ -94,12 +95,66 @@ async fn test_adhoc_group_is_shown() {
|
||||
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_unknown() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(
|
||||
Config::ShowEmails,
|
||||
Some(&ShowEmails::AcceptedContacts.to_string()),
|
||||
)
|
||||
.await?;
|
||||
tcm.send_recv(alice, bob, "hi").await;
|
||||
receive_imf(
|
||||
bob,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org, claire@example.com\n\
|
||||
Message-ID: <3333@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_known() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
|
||||
Contact::create(&t, "Bob", "bob@example.com").await.unwrap();
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
|
||||
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
|
||||
// (and existent chat is required)
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
|
||||
|
||||
// accept Bob by accepting a delta-message from Bob
|
||||
receive_imf(&t, MSGRMSG, false).await.unwrap();
|
||||
@@ -135,6 +190,7 @@ async fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_all() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 2);
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=all will show up in a single chat
|
||||
@@ -760,6 +816,10 @@ async fn test_concat_multiple_ndns() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
let received_msg = receive_imf(context, imf_raw, false)
|
||||
.await
|
||||
.expect("receive_imf failure")
|
||||
|
||||
@@ -250,6 +250,16 @@ impl SchedulerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_all_relay_storage(&self) -> Result<()> {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.clear_all_relay_storage();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("IO is not started");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
@@ -348,6 +358,7 @@ async fn inbox_loop(
|
||||
let ImapConnectionHandlers {
|
||||
mut connection,
|
||||
stop_token,
|
||||
clear_storage_request_receiver,
|
||||
} = inbox_handlers;
|
||||
|
||||
let transport_id = connection.transport_id();
|
||||
@@ -386,7 +397,14 @@ async fn inbox_loop(
|
||||
}
|
||||
};
|
||||
|
||||
match inbox_fetch_idle(&ctx, &mut connection, session).await {
|
||||
match inbox_fetch_idle(
|
||||
&ctx,
|
||||
&mut connection,
|
||||
session,
|
||||
&clear_storage_request_receiver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed inbox fetch_idle: {err:#}."
|
||||
@@ -407,11 +425,29 @@ async fn inbox_loop(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
|
||||
async fn inbox_fetch_idle(
|
||||
ctx: &Context,
|
||||
imap: &mut Imap,
|
||||
mut session: Session,
|
||||
clear_storage_request_receiver: &Receiver<()>,
|
||||
) -> Result<Session> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
// Clear IMAP storage on request.
|
||||
//
|
||||
// Only doing this for chatmail relays to avoid
|
||||
// accidentally deleting all emails in a shared mailbox.
|
||||
let should_clear_imap_storage =
|
||||
clear_storage_request_receiver.try_recv().is_ok() && session.is_chatmail();
|
||||
if should_clear_imap_storage {
|
||||
info!(ctx, "Transport {transport_id}: Clearing IMAP storage.");
|
||||
session.delete_all_messages(ctx, &imap.folder).await?;
|
||||
}
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
if ctx.quota_needs_update(session.transport_id(), 60).await
|
||||
//
|
||||
// Always update if we just cleared IMAP storage.
|
||||
if (ctx.quota_needs_update(session.transport_id(), 60).await || should_clear_imap_storage)
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session, &imap.folder).await
|
||||
{
|
||||
warn!(
|
||||
@@ -595,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.clone()),
|
||||
Some(err) => connection.connectivity.set_err(&ctx, err),
|
||||
}
|
||||
|
||||
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that
|
||||
@@ -737,6 +773,12 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_all_relay_storage(&self) {
|
||||
for b in &self.inboxes {
|
||||
b.conn_state.clear_relay_storage();
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_smtp(&self) {
|
||||
self.smtp.interrupt();
|
||||
}
|
||||
@@ -870,6 +912,13 @@ struct SmtpConnectionHandlers {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ImapConnectionState {
|
||||
state: ConnectionState,
|
||||
|
||||
/// Channel to request clearing the folder.
|
||||
///
|
||||
/// IMAP loop receiving this should clear the folder
|
||||
/// on the next iteration if IMAP server is a chatmail relay
|
||||
/// and otherwise ignore the request.
|
||||
clear_storage_request_sender: Sender<()>,
|
||||
}
|
||||
|
||||
impl ImapConnectionState {
|
||||
@@ -881,11 +930,13 @@ impl ImapConnectionState {
|
||||
) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
let stop_token = CancellationToken::new();
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
let (clear_storage_request_sender, clear_storage_request_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = ImapConnectionHandlers {
|
||||
connection: Imap::new(context, transport_id, login_param, idle_interrupt_receiver)
|
||||
.await?,
|
||||
stop_token: stop_token.clone(),
|
||||
clear_storage_request_receiver,
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
@@ -894,7 +945,10 @@ impl ImapConnectionState {
|
||||
connectivity: handlers.connection.connectivity.clone(),
|
||||
};
|
||||
|
||||
let conn = ImapConnectionState { state };
|
||||
let conn = ImapConnectionState {
|
||||
state,
|
||||
clear_storage_request_sender,
|
||||
};
|
||||
|
||||
Ok((conn, handlers))
|
||||
}
|
||||
@@ -908,10 +962,19 @@ impl ImapConnectionState {
|
||||
fn stop(&self) {
|
||||
self.state.stop();
|
||||
}
|
||||
|
||||
/// Requests clearing relay storage and interrupts the inbox.
|
||||
fn clear_relay_storage(&self) {
|
||||
self.clear_storage_request_sender.try_send(()).ok();
|
||||
self.state.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ImapConnectionHandlers {
|
||||
connection: Imap,
|
||||
stop_token: CancellationToken,
|
||||
|
||||
/// Channel receiver to get requests to clear IMAP storage.
|
||||
pub(crate) clear_storage_request_receiver: Receiver<()>,
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ impl ConnectivityStore {
|
||||
context.emit_event(EventType::ConnectivityChanged);
|
||||
}
|
||||
|
||||
pub(crate) fn set_err(&self, context: &Context, e: String) {
|
||||
self.set(context, DetailedConnectivity::Error(e));
|
||||
pub(crate) fn set_err(&self, context: &Context, e: impl ToString) {
|
||||
self.set(context, DetailedConnectivity::Error(e.to_string()));
|
||||
}
|
||||
pub(crate) fn set_connecting(&self, context: &Context) {
|
||||
self.set(context, DetailedConnectivity::Connecting);
|
||||
|
||||
@@ -142,7 +142,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
let auth = create_id();
|
||||
token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
|
||||
|
||||
let fingerprint = self_fingerprint(context).await?;
|
||||
let fingerprint = get_self_fingerprint(context).await?.hex();
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
|
||||
|
||||
103
src/smtp.rs
103
src/smtp.rs
@@ -13,7 +13,7 @@ use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::warn;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
@@ -590,77 +590,44 @@ async fn send_mdn_rfc724_mid(
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
add_self_recipients(context, &mut recipients, encrypted).await?;
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
use crate::log::LogExt;
|
||||
let recipients: Vec<_> = recipients
|
||||
.into_iter()
|
||||
.filter_map(|addr| {
|
||||
async_smtp::EmailAddress::new(addr.clone())
|
||||
.with_context(|| format!("Invalid recipient: {addr}"))
|
||||
.log_err(context)
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let recipients: Vec<_> = recipients
|
||||
.into_iter()
|
||||
.filter_map(|addr| {
|
||||
async_smtp::EmailAddress::new(addr.clone())
|
||||
.with_context(|| format!("Invalid recipient: {addr}"))
|
||||
.log_err(context)
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(true)
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
}
|
||||
SendResult::Retry => {
|
||||
info!(
|
||||
context,
|
||||
"Temporary SMTP failure while sending an MDN for {rfc724_mid}."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
SendResult::Failure(err) => Err(err),
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
SendResult::Retry => {
|
||||
info!(
|
||||
context,
|
||||
"Temporary SMTP failure while sending an MDN for {rfc724_mid}."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
SendResult::Failure(err) => Err(err),
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
let _ = smtp;
|
||||
context
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
t.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
(rfc724_mid, recipients.join(" "), body, u32::MAX),
|
||||
)?;
|
||||
let mut stmt = t.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn queue_mdn(context: &Context) -> Result<()> {
|
||||
let queued = send_mdn(context, &mut Smtp::new()).await?;
|
||||
assert!(queued);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to send a single MDN. Returns true if more MDNs should be sent.
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
@@ -902,7 +902,7 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
|
||||
// Delete POI locations
|
||||
// which don't have corresponding message.
|
||||
location::delete_orphaned_poi(context)
|
||||
delete_orphaned_poi_locations(context)
|
||||
.await
|
||||
.context("Failed to delete orphaned POI locations")
|
||||
.log_err(context)
|
||||
|
||||
@@ -12,6 +12,7 @@ use rusqlite::OptionalExtension;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::key::DcKey;
|
||||
use crate::log::warn;
|
||||
@@ -974,7 +975,8 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
|
||||
// keep this default and use DC_SHOW_EMAILS_NO
|
||||
// only for new installations
|
||||
if exists_before_update {
|
||||
sql.set_raw_config_int("show_emails", 2).await?;
|
||||
sql.set_raw_config_int("show_emails", ShowEmails::All as i32)
|
||||
.await?;
|
||||
}
|
||||
sql.set_db_version(50).await?;
|
||||
}
|
||||
@@ -1455,7 +1457,8 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
}
|
||||
if dbversion < 98 {
|
||||
if exists_before_update && sql.get_raw_config_int("show_emails").await?.is_none() {
|
||||
sql.set_raw_config_int("show_emails", 0).await?;
|
||||
sql.set_raw_config_int("show_emails", ShowEmails::Off as i32)
|
||||
.await?;
|
||||
}
|
||||
sql.set_db_version(98).await?;
|
||||
}
|
||||
@@ -1916,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_legacy(context).await?;
|
||||
let entered_param = EnteredLoginParam::load(context).await?;
|
||||
let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
|
||||
|
||||
sql.execute_migration_transaction(
|
||||
|
||||
@@ -153,6 +153,15 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Forwarded"))]
|
||||
Forwarded = 97,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
|
||||
You may not be able to receive message when the storage is 100%% used.\n\n\
|
||||
👉 Please check if you can delete old data in the provider's webinterface \
|
||||
and consider to enable \"Settings / Delete Old Messages\". \
|
||||
You can check your current storage usage anytime at \"Settings / Connectivity\"."
|
||||
))]
|
||||
QuotaExceedingMsgBody = 98,
|
||||
|
||||
#[strum(props(fallback = "Multi Device Synchronization"))]
|
||||
SyncMsgSubject = 101,
|
||||
|
||||
@@ -1091,6 +1100,13 @@ pub(crate) fn forwarded(context: &Context) -> String {
|
||||
translated(context, StockMessage::Forwarded)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
|
||||
pub(crate) fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
|
||||
translated(context, StockMessage::QuotaExceedingMsgBody)
|
||||
.replace1(&format!("{highest_usage}"))
|
||||
.replace("%%", "%")
|
||||
}
|
||||
|
||||
/// Stock string: `Incoming Messages`.
|
||||
pub(crate) fn incoming_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::IncomingMessages)
|
||||
|
||||
@@ -102,6 +102,16 @@ async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = quota_exceeding(&t, 81);
|
||||
assert!(str.contains("81% "));
|
||||
assert!(str.contains("100% "));
|
||||
assert!(!str.contains("%%"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
@@ -275,17 +275,16 @@ impl TestContextManager {
|
||||
|
||||
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
|
||||
|
||||
for _ in 0..2 {
|
||||
loop {
|
||||
let mut something_sent = false;
|
||||
let rev_order = false;
|
||||
if let Some(sent) = joiner.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
|
||||
if let Some(sent) = joiner.pop_sent_msg_opt(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_ex(rev_order, Duration::ZERO).await {
|
||||
if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
joiner.recv_msg_opt(&sent).await;
|
||||
something_sent = true;
|
||||
}
|
||||
@@ -624,38 +623,25 @@ impl TestContext {
|
||||
}
|
||||
|
||||
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
|
||||
let rev_order = !self
|
||||
.get_config_bool(Config::PopSentMsgFromHead)
|
||||
.await
|
||||
.unwrap();
|
||||
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(&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))
|
||||
})
|
||||
.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))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("query_row_optional failed");
|
||||
if let Some(row) = row {
|
||||
@@ -796,7 +782,8 @@ ORDER BY id"
|
||||
let chat_msgs = chat::get_chat_msgs(self, received.chat_id).await.unwrap();
|
||||
assert!(
|
||||
chat_msgs.contains(&ChatItem::Message { msg_id: msg.id }),
|
||||
"received message is not shown in chat, maybe it's hidden"
|
||||
"received message is not shown in chat, maybe it's hidden (you may have \
|
||||
to call set_config(Config::ShowEmails, Some(\"2\")).await)"
|
||||
);
|
||||
|
||||
msg
|
||||
@@ -836,24 +823,17 @@ ORDER BY id"
|
||||
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 msg_id = self.get_last_msg_id_in(chat_id).await;
|
||||
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
|
||||
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()
|
||||
}
|
||||
|
||||
/// Gets the most recent message over all chats.
|
||||
@@ -1107,6 +1087,7 @@ ORDER BY id"
|
||||
self,
|
||||
chat_id,
|
||||
MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: false,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -5,9 +5,13 @@ use crate::download::DownloadState;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
// The code for replacing partial download stubs is already removed, so check that nothing happens
|
||||
// if after that a full message is passed to receive_imf. Users should ask the sender to send the
|
||||
// message again.
|
||||
// The code for downloading stub messages stays
|
||||
// during the transition perios to pre-messages
|
||||
// so people can still download their files shortly after they updated.
|
||||
// After there are a few release with pre-message rolled out,
|
||||
// we will remove the ability to download stub messages and replace the following test
|
||||
// so it checks that it doesn't crash or that the messages are replaced by sth.
|
||||
// like "download failed/expired, please ask sender to send it again"
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_download_stub_message() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -49,9 +53,9 @@ async fn test_download_stub_message() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert!(msg.get_text().contains("[97.66 KiB message]"));
|
||||
assert_eq!(msg.get_text(), "100k text...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
//! Tests about receiving Pre-Messages and Post-Message
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat;
|
||||
use crate::chat::send_msg;
|
||||
use crate::config::Config;
|
||||
use crate::contact;
|
||||
use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PostMsgMetadata};
|
||||
use crate::message::{self, Message, MessageState, Viewtype, delete_msgs, markseen_msgs};
|
||||
use crate::message::{Message, MessageState, Viewtype, delete_msgs, markseen_msgs};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::param::Param;
|
||||
use crate::reaction::{get_msg_reactions, send_reaction};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::smtp;
|
||||
use crate::summary::assert_summary_texts;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tests::pre_messages::util::{
|
||||
big_webxdc_app, send_large_file_message, send_large_image_message, send_large_webxdc_message,
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
/// Test that mimeparser can correctly detect and parse pre-messages and Post-Messages
|
||||
@@ -264,64 +261,6 @@ async fn test_lost_pre_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pre_msg_mdn_before_sending_full() -> Result<()> {
|
||||
pre_msg_mdn_before_sending_full("").await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pre_msg_mdn_before_sending_full_with_text() -> Result<()> {
|
||||
pre_msg_mdn_before_sending_full("text").await
|
||||
}
|
||||
|
||||
async fn pre_msg_mdn_before_sending_full(text: &str) -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config_bool(Config::PopSentMsgFromHead, true)
|
||||
.await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice.create_group_with_members("", &[bob]).await;
|
||||
|
||||
let file_bytes = include_bytes!("../../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(alice, "a.jpg", file_bytes, None)?;
|
||||
msg.set_text(text.to_string());
|
||||
let pre_msg = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
let alice_msg_id = msg.id;
|
||||
|
||||
let msg = bob.recv_msg(&pre_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.id.get_state(bob).await?, MessageState::InFresh);
|
||||
assert_eq!(msg.text, text);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
msg.chat_id.accept(bob).await?;
|
||||
markseen_msgs(bob, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(bob).await?, MessageState::InSeen);
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
|
||||
(msg.from_id,)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
smtp::queue_mdn(bob).await?;
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
alice_msg_id.get_state(alice).await?,
|
||||
MessageState::OutPending
|
||||
);
|
||||
|
||||
let _full_msg = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
alice_msg_id.get_state(alice).await?,
|
||||
MessageState::OutMdnRcvd
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_post_msg_bad_sender() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -607,7 +546,7 @@ async fn test_webxdc_updates_in_post_message_after_pre_message() -> Result<()> {
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#)
|
||||
.await?;
|
||||
|
||||
chat::send_msg(alice, alice_chat_id, &mut alice_instance).await?;
|
||||
send_msg(alice, alice_chat_id, &mut alice_instance).await?;
|
||||
let post_message = alice.pop_sent_msg().await;
|
||||
let pre_message = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -648,7 +587,7 @@ async fn test_webxdc_updates_in_post_message_without_pre_message() -> Result<()>
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#)
|
||||
.await?;
|
||||
|
||||
chat::send_msg(alice, alice_chat_id, &mut alice_instance).await?;
|
||||
send_msg(alice, alice_chat_id, &mut alice_instance).await?;
|
||||
let post_message = alice.pop_sent_msg().await;
|
||||
let pre_message = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -686,7 +625,7 @@ async fn test_markseen_pre_msg() -> Result<()> {
|
||||
alice.create_chat(bob).await; // Make sure the chat is accepted.
|
||||
|
||||
tcm.section("Bob sends a large message to Alice");
|
||||
let (pre_message, post_message, bob_msg_id) =
|
||||
let (pre_message, post_message, _bob_msg_id) =
|
||||
send_large_file_message(bob, bob_chat_id, Viewtype::File, &vec![0u8; 1_000_000]).await?;
|
||||
|
||||
tcm.section("Alice receives a pre-message message from Bob");
|
||||
@@ -701,23 +640,10 @@ async fn test_markseen_pre_msg() -> Result<()> {
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
|
||||
(msg.from_id,)
|
||||
)
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
smtp::queue_mdn(alice).await?;
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
let bob_msg = Message::load_from_db(bob, bob_msg_id).await?;
|
||||
assert_eq!(bob_msg.get_state(), MessageState::OutMdnRcvd);
|
||||
let read_receipts = message::get_msg_read_receipts(bob, bob_msg_id).await?;
|
||||
assert_eq!(read_receipts.len(), 1);
|
||||
let (contact_id, ts) = read_receipts[0];
|
||||
assert_eq!(contact_id, bob.add_or_lookup_contact_id(alice).await);
|
||||
assert!(ts + 3600 - 60 < time());
|
||||
|
||||
tcm.section("Alice downloads message");
|
||||
alice.recv_msg_trash(&post_message).await;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::chat::{self, Chat, add_contact_to_chat, remove_contact_from_chat, sen
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{DcKey, load_self_public_key};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -152,7 +152,11 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
|
||||
bob.sql
|
||||
.execute(
|
||||
"DELETE FROM public_keys WHERE fingerprint=?",
|
||||
(&self_fingerprint(alice).await.unwrap(),),
|
||||
(&load_self_public_key(alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint()
|
||||
.hex(),),
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
|
||||
@@ -44,7 +44,9 @@ async fn test_parse_receive_headers_integration() {
|
||||
Message-ID: 2dfdbde7@example.org
|
||||
|
||||
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000
|
||||
|
||||
DKIM Results: Passed=true";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml");
|
||||
@@ -54,7 +56,9 @@ Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
|
||||
|
||||
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
|
||||
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
|
||||
DKIM Results: Passed=true";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -791,7 +791,18 @@ pub(crate) async fn sync_transports(
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let configured_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
for RemovedTransportData { addr, timestamp } in removed_transports {
|
||||
if *addr == configured_addr {
|
||||
continue;
|
||||
}
|
||||
modified |= transaction.execute(
|
||||
"DELETE FROM transports
|
||||
WHERE addr=? AND add_timestamp<=?",
|
||||
|
||||
@@ -118,6 +118,8 @@ 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?;
|
||||
|
||||
@@ -205,6 +207,8 @@ 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?;
|
||||
|
||||
@@ -422,6 +426,13 @@ async fn check_addrs(
|
||||
a.get_published_self_addrs().await.unwrap(),
|
||||
published_self_addrs.clone(),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_secondary_self_addrs().await.unwrap(),
|
||||
concat(&[
|
||||
addresses.secondary_published,
|
||||
addresses.secondary_unpublished,
|
||||
]),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_published_secondary_self_addrs().await.unwrap(),
|
||||
concat(&[addresses.secondary_published]),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas106.aol.mail.ne1.yahoo.com;
|
||||
dkim=pass header.i=@buzon.uy header.s=2019;
|
||||
spf=pass smtp.mailfrom=buzon.uy;
|
||||
dmarc=pass(p=REJECT) header.from=buzon.uy;
|
||||
From: <alice@buzon.uy>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
|
||||
dkim=unknown;
|
||||
spf=none smtp.mailfrom=delta.blinzeln.de;
|
||||
dmarc=unknown header.from=delta.blinzeln.de;
|
||||
From: <alice@delta.blinzeln.de>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas210.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@disroot.org header.s=mail;
|
||||
spf=pass smtp.mailfrom=disroot.org;
|
||||
dmarc=pass(p=QUARANTINE) header.from=disroot.org;
|
||||
From: <alice@disroot.org>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,7 @@
|
||||
Authentication-Results: atlas105.aol.mail.ne1.yahoo.com;
|
||||
dkim=pass header.i=@fastmail.com header.s=fm2;
|
||||
dkim=pass header.i=@messagingengine.com header.s=fm2;
|
||||
spf=pass smtp.mailfrom=fastmail.com;
|
||||
dmarc=pass(p=NONE,sp=NONE) header.from=fastmail.com;
|
||||
From: <alice@fastmail.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas-baseline-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112;
|
||||
spf=pass smtp.mailfrom=gmail.com;
|
||||
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=gmail.com;
|
||||
From: <alice@gmail.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,8 @@
|
||||
Authentication-Results: atlas112.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@hotmail.com header.s=selector1;
|
||||
spf=pass smtp.mailfrom=hotmail.com;
|
||||
dmarc=pass(p=NONE) header.from=hotmail.com;
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@hotmail.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas101.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@icloud.com header.s=1a1hai;
|
||||
spf=pass smtp.mailfrom=icloud.com;
|
||||
dmarc=pass(p=QUARANTINE,sp=QUARANTINE) header.from=icloud.com;
|
||||
From: <alice@icloud.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@ik.me header.s=20200325;
|
||||
spf=pass smtp.mailfrom=ik.me;
|
||||
dmarc=pass(p=REJECT) header.from=ik.me;
|
||||
From: <alice@ik.me>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas104.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@mail.ru header.s=mail4;
|
||||
spf=pass smtp.mailfrom=mail.ru;
|
||||
dmarc=pass(p=REJECT) header.from=mail.ru;
|
||||
From: <alice@mail.ru>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas211.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@mailo.com header.s=mailo;
|
||||
spf=pass smtp.mailfrom=mailo.com;
|
||||
dmarc=pass(p=NONE) header.from=mailo.com;
|
||||
From: <alice@mailo.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,8 @@
|
||||
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@outlook.com header.s=selector1;
|
||||
spf=pass smtp.mailfrom=outlook.com;
|
||||
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=outlook.com;
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@outlook.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@posteo.de header.s=2017;
|
||||
spf=pass smtp.mailfrom=posteo.de;
|
||||
dmarc=pass(p=NONE) header.from=posteo.de;
|
||||
From: <alice@posteo.de>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas114.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@yandex.ru header.s=mail;
|
||||
spf=pass smtp.mailfrom=yandex.ru;
|
||||
dmarc=pass(p=NONE) header.from=yandex.ru;
|
||||
From: <alice@yandex.ru>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
|
||||
dkim=unknown;
|
||||
spf=none smtp.mailfrom=delta.blinzeln.de;
|
||||
dmarc=unknown header.from=delta.blinzeln.de;
|
||||
From: forged-authres-added@example.com
|
||||
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=aol.com header.i=@aol.com header.b="sjmqxpKe";
|
||||
dkim-atps=neutral
|
||||
From: <alice@aol.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,3 @@
|
||||
From: <alice@delta.blinzeln.de>
|
||||
To: <alice@buzon.uy>
|
||||
Authentication-Results: secure-mailgate.com; auth=pass smtp.auth=91.203.111.88@webbox222.server-home.org
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="L9SmOHOj";
|
||||
dkim-atps=neutral
|
||||
From: <alice@disroot.org>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=fastmail.com header.i=@fastmail.com header.b="kLB05is1";
|
||||
dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="B8mfR89g";
|
||||
dkim-atps=neutral
|
||||
From: <alice@fastmail.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="Ngf1X5eN";
|
||||
dkim-atps=neutral
|
||||
From: <alice@gmail.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,7 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=hotmail.com header.i=@hotmail.com header.b="dEHn9Szj";
|
||||
dkim-atps=neutral
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@hotmail.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=icloud.com header.i=@icloud.com header.b="rAXD4xVN";
|
||||
dkim-atps=neutral
|
||||
From: <alice@icloud.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; secure) header.d=ik.me header.i=@ik.me header.b="EWWQpVZX";
|
||||
dkim-atps=neutral
|
||||
From: <alice@ik.me>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; secure) header.d=mail.de header.i=@mail.de header.b="18cRkjHf";
|
||||
dkim-atps=neutral
|
||||
From: <alice@mail.de>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=mail.ru header.i=@mail.ru header.b="uXBGAnnn";
|
||||
dkim-atps=neutral
|
||||
From: <alice@mail.ru>
|
||||
To: <alice@buzon.uy>
|
||||
Authentication-Results: smtpng1.m.smailru.net; auth=pass smtp.auth=alice@mail.ru smtp.mailfrom=alice@mail.ru
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=mailo.com header.i=@mailo.com header.b="awx9eOw9";
|
||||
dkim-atps=neutral
|
||||
From: <alice@mailo.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,7 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=outlook.com header.i=@outlook.com header.b="Uq5LH/n/";
|
||||
dkim-atps=neutral
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@outlook.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; secure) header.d=posteo.de header.i=@posteo.de header.b="AyOucyBM";
|
||||
dkim-atps=neutral
|
||||
From: <alice@posteo.de>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; secure) header.d=riseup.net header.i=@riseup.net header.b="eQhRD1BM";
|
||||
dkim-atps=neutral
|
||||
From: <alice@riseup.net>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=yahoo.com header.i=@yahoo.com header.b="a1T2PpDI";
|
||||
dkim-atps=neutral
|
||||
From: <alice@yahoo.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.b="oGBtMMzR";
|
||||
dkim-atps=neutral
|
||||
Authentication-Results: iva4-143b1447cf50.qloud-c.yandex.net; dkim=pass header.i=@yandex.ru
|
||||
From: <alice@yandex.ru>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,3 @@
|
||||
From: forged-authres-added@example.com
|
||||
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
|
||||
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@aol.com>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@buzon.uy>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@disroot.org>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@fastmail.com>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@gmail.com>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,4 @@
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@hotmail.com>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@icloud.com>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user