Compare commits

..

46 Commits

Author SHA1 Message Date
link2xt
c9c05a701e deny.toml update 2026-04-27 22:43:39 +02:00
link2xt
6876a2fc50 iroh::NodeAddr renamed to iroh::EndpointAddr 2026-04-27 22:38:46 +02:00
link2xt
5b6d48ac43 Update API usage for iroh 0.98 2026-04-27 22:38:46 +02:00
link2xt
e3bb0febb7 Enable tls-ring feature on iroh 2026-04-27 22:38:46 +02:00
link2xt
667d895a1a Cargo.lock
Had to precisely update pkcs8 to 0.11.0-rc.11,
with 0.11.0-rc.12 ed25519 crate does not compile.
2026-04-27 22:38:46 +02:00
link2xt
768e7de1d5 Increase MSRV to 1.89
This is required by iroh 0.98.1
2026-04-27 22:38:46 +02:00
link2xt
cb0870b7d2 build: update iroh to 0.98 2026-04-27 22:38:46 +02:00
link2xt
91a1e6b752 build: update iroh to 0.94
https://www.iroh.computer/blog/iroh-0-94-0-the-endpoint-takeover
2026-04-27 22:38:46 +02:00
link2xt
1092b3bd1a build: update iroh to 0.92.0 2026-04-27 22:38:46 +02:00
link2xt
459aa66ed0 build(deltachat-rpc-server): enable tokio/signal feature
It is required for tokio::signal::ctrl_c
2026-04-27 22:38:46 +02:00
link2xt
563bd9f24e Add "ring" feature to tokio-rustls
Seems it was enabled by iroh and this is not the case with iroh 0.98.
Better enable the feature explicitly since we directly use it.
2026-04-27 21:18:59 +02:00
Hocuri
b806efa096 refactor: Make Fingerprint not implement Display (#8177)
Currently, the Fingerprint type implements Display, but this doesn't get
you the canonical fingerprint representation, but something
human-readable. This is confusing, and back when I first used
`Fingerprint`, I immediately wrote a bug because of this. So, instead,
make a function `human_readable()` on Fingerprint.

This comes from the discussion at
https://github.com/chatmail/core/pull/8174#discussion_r3143130722.
2026-04-27 11:22:21 +02:00
Hocuri
0580056b62 refactor: Use regular functions rather than FromStr impls (#8178)
Implementing `FromStr` and then calling `parse()` creates an
indirection, which is hard to follow for people who are not familiar
with Rust. @r10s recently voiced this problem when we were
pair-programming, and I agree.

We can decide what exactly to call the new function.

I didn't remove all `FromStr` implementations yet; `FromStr for
Fingerprint`, `FromStr for Params`, `FromStr for MozConfigTag` and
`FromStr for EphemeralTimer` are still left.
2026-04-27 11:09:01 +02:00
link2xt
63f96d9138 perf: set location for all accounts in parallel 2026-04-27 03:12:26 +00:00
link2xt
1204a94252 perf: stop sending locations concurrently 2026-04-27 03:12:26 +00:00
link2xt
73e8bee120 api!(location): avoid repeating module name in function names
E.g. rename location::is_sending_locations_to_chat to location::is_sending_to_chat.
Then import only the location module and use it with location:: prefix,
similarly to how e.g. std::fs or std::tokio::fs avoids using "fs" or "file"
in function names.
2026-04-27 03:12:26 +00:00
link2xt
8c927c7f86 api: add JSON-RPC APIs for location streaming
New API stop_sending_locations() only available
in JSON-RPC stops location streaming
in all accounts and chats.
2026-04-27 03:12:26 +00:00
link2xt
7f9c184659 refactor: split is_sending_locations_to_chat() into two functions 2026-04-27 03:12:26 +00:00
iequidoo
287d730556 chore: Apply rustmft after the previous commit 2026-04-26 12:04:18 -03:00
iequidoo
82bb77b056 feat: Drop support for replacing partial download stubs 2026-04-26 12:04:18 -03:00
Hocuri
aa1f129a48 refactor: Use self_fingerprint() where it makes sense (#8174) 2026-04-26 09:36:08 +02:00
Hocuri
0ad58f7a59 test: Remove unused test data related to Authentication-Result parsing (#8175)
Follow-up to https://github.com/chatmail/core/pull/8172
2026-04-26 09:35:45 +02:00
iequidoo
6d61f7e071 fix: Don't receive message if a deletion request was received before (#8143)
There's no check for `from_id` so another group member can delete the
message, but this can only happen in case of message reordering and the
problem already exists for usual messages, so we may ignore it and
overall a group represents a scope of trust.

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-04-25 21:19:03 -03:00
Hocuri
fa68c1f0e4 refactor: Remove mostly-unused function get_secondary_self_addrs() (#8173) 2026-04-25 23:37:36 +02:00
Hocuri
5f1d54100f refactor: Remove unused Authentication-Results parsing (#8172) 2026-04-25 23:33:15 +02:00
Hocuri
25cd7b65fd api!: Remove unused info_only option when loading a chatlist (#8171)
Remove unused info_only option from `get_chat_msgs_ex()`.

This option was meant to show an "audit log" of a group, i.e. only the
info messages. This feature was removed again from Desktop, but the
option still lingered around in Core.

This also adds a doc comment to the JsonRPC functions, because I
wanted to note somewhere that the parameter is deprecated, and I needed
some place to put this note.
2026-04-25 23:30:53 +02:00
link2xt
63596a4940 feat: remove show_emails config 2026-04-25 20:24:57 +00:00
link2xt
8bc84e13de test: use Displayname instead of ShowEmails for config cache test
ShowEmails is gonig to be removed.
2026-04-25 20:24:57 +00:00
B. Petersen
ba8c39ff5b Revert "api: add clear_all_relay_storage API"
This reverts commit 10ab556d65.
2026-04-25 22:15:19 +02:00
B. Petersen
7de58f5329 feat: adapt quota warning to automatic cleanup 2026-04-25 21:50:13 +02:00
Hocuri
1fd4a19e56 feat: Don't show non-delivery-notfications in broadcast channels (#8159)
Looking forward, it is unclear how exactly we want to handle errors, but
in the meantime, this is a simple thing we can do to improve the
situation.

If you have a broadcast channel, chances are that some of your
recipients have some problems receiving your messages. You are probably
not too interested in that.

In reality, some channel operators were quite confused by these errors,
and we told them to just ignore them, but at this point we may as well
just hide them.

Theoretically we can start removing recipients that never get our
messages at some point.
2026-04-25 17:19:47 +00:00
link2xt
1ab6645bbc api!: remove dc_delete_all_locations 2026-04-24 20:31:09 +00:00
link2xt
c17d067a1a refactor: remove unnecessary async block in dc_set_location 2026-04-24 20:29:49 +00:00
link2xt
3aeb2d44b7 feat: remove non-sticker heuristics and force_sticker()
This causes problems for Delta Chat Desktop at
<https://github.com/deltachat/deltachat-desktop/pull/6278>
even though the logic was originally introduced for iOS.
If the problem remains on iOS,
heuristics can be added into iOS UI.
2026-04-24 18:18:51 +00:00
dependabot[bot]
d069e75cd8 chore(cargo): bump openssl from 0.10.72 to 0.10.78
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.72 to 0.10.78.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.72...openssl-v0.10.78)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.78
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 05:08:01 -03:00
link2xt
ad5e904d1c chore(cargo): update rustls-webpki to 0.103.13
Also ignore RUSTSEC-2026-0104 because iroh 0.35.0
pulls in rustls-webpki that cannot be updated.
2026-04-22 18:10:40 +00:00
iequidoo
38affa2c62 fix: Don't resort re-sent message to the bottom (#8145)
We shouldn't just revert ad7f873c68 because then we won't be able to
normally transfer a backup from a device having clock a bit in the future.

INDEXED BY msgs_index7 is to prevent SQLite from downgrading to using msgs_index2 which has less
ordering.
2026-04-22 13:54:56 -03:00
link2xt
6dfc6f8780 chore: update provider database 2026-04-22 12:27:28 +00:00
dependabot[bot]
8cca0cf75d chore(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](ed0c53931b...cef221092e)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-22 10:46:18 +00:00
dependabot[bot]
b81f50be8f chore(deps): bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.2 to 0.5.3.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](71321a20a9...b1d7e1fb5d)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-22 10:45:09 +00:00
Hocuri
970222f376 feat: Resend the last 10 messages to new broadcast member (#8151)
Last 10 messages in a broadcast channel are resent. They are sent and encrypted only to the new member, not to other subscribers.

Close #7678

Based on https://github.com/chatmail/core/pull/7854, with the following
changes:

- Refactor and simplify code, don't reuse the existing `get_chat_msgs()`
function
- Document that Param::Arg4 is also used for resent messages
cc818d9099
- It's unclear how exactly to resend webxdc status updates. After
discussing with @r10s, don't resend webxdc's at all for now.
38d57ebb30
- Don't set fake `msg_id` in resent messages
e7d0687d90
Setting the msg_id to `u32::MAX` is hacky, and may just as well break
    things as it may fix things, because some code may use the msg.id to
    load information from the database, like `get_iroh_topic_for_msg()`.
    From reading the code, I couldn't find any problem with leaving the
    correct `msg_id`, and if there is one, then we should add a function
parameter `is_resending` that is checked in the corresponding places.

Easiest to review file-by-file rather than individual commits, probably.
I'll squash-merge this.

---------

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2026-04-21 22:34:53 +02:00
link2xt
83e31a5f17 fix: add error cause to connectivity view for IMAP errors
For SMTP errors we already format `last_send_error` with {:#},
but for IMAP errors we have formatted the errors with .to_string().
This resulted in errors such as
"Error: IMAP failed to connect to example.org:443:tls"
instead of
"Error: IMAP failed to connect to example.org:443:tls: Connection failure: Network is unreachable (os error 101)."
in the connectivity view HTML.
2026-04-21 19:08:33 +00:00
iequidoo
31fabb24df feat: Don't send Chat-Group-Name* headers for InBroadcast-s
Broadcast subscribers can't change the chat name, so sending the "Chat-Group-Name{,-Timestamp}"
headers looks unnecessary. That could be useful for other subscriber's devices, but having only the
chat name isn't enough anyway, at least knowing the secret is necessary which is sent by the
broadcast owner.
2026-04-21 08:42:10 -03:00
Hocuri
66df0d2a3c api: Deprecate old server config keys that were replaced by add_or_update_transport() 2026-04-20 15:45:17 +02:00
Hocuri
5a6b1c62dd refactor: Rename EnteredLoginParam::load() and save() to load_legacy() and save_legacy() 2026-04-20 15:45:17 +02:00
Hocuri
18d878378f api!: Remove unused config smtp_certificate_checks 2026-04-20 15:45:17 +02:00
385 changed files with 2447 additions and 5750 deletions

View File

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

View File

@@ -382,7 +382,7 @@ jobs:
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server

View File

@@ -47,4 +47,4 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b

View File

@@ -23,4 +23,4 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3

2565
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ name = "deltachat"
version = "2.50.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
rust-version = "1.89"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -66,8 +66,8 @@ humansize = "2"
hyper = "1"
hyper-util = "0.1.16"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
iroh-gossip = { version = "0.98", default-features = false, features = ["net"] }
iroh = { version = "0.98", default-features = false, features = ["tls-ring"] }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.4", default-features = false }
@@ -101,7 +101,7 @@ tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-rustls = { version = "0.26.2", default-features = false, features = ["ring"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6", default-features = false }
tokio-util = { workspace = true }

View File

@@ -390,27 +390,9 @@ char* dc_get_blobdir (const dc_context_t* context);
/**
* Configure the context. The configuration is handled by key=value pairs as:
*
* - `addr` = Email address to use for configuration.
* If dc_configure() fails this is not the email address actually in use.
* Use `configured_addr` to find out the email address actually in use.
* - `configured_addr` = Email address actually in use.
* - `configured_addr` = Email address in use.
* Unless for testing, do not set this value using dc_set_config().
* Instead, set `addr` and call dc_configure().
* - `mail_server` = IMAP-server, guessed if left out
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
* - `mail_port` = IMAP-port, guessed if left out
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `send_server` = SMTP-server, guessed if left out
* - `send_user` = SMTP-user, guessed if left out
* - `send_pw` = SMTP-password, guessed if left out
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
* - `selfavatar` = File containing avatar. Will immediately be copied to the
@@ -426,12 +408,6 @@ 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.
@@ -440,8 +416,7 @@ 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
* e-mails matching the `show_emails` settings above, the UI should clearly point that out.
* "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
* good outgoing images/videos/voice quality at reasonable sizes (default)
@@ -513,6 +488,27 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1 = Contacts (default, does not include contact requests),
* 2 = Nobody (calls never result in a notification).
*
* Also, there are configs that are only needed
* if you want to use the deprecated dc_configure() API, such as:
*
* - `addr` = Email address to use for configuration.
* If dc_configure() fails this is not the email address actually in use.
* Use `configured_addr` to find out the email address actually in use.
* - `mail_server` = IMAP-server, guessed if left out
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
* - `mail_port` = IMAP-port, guessed if left out
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `send_server` = SMTP-server, guessed if left out
* - `send_user` = SMTP-user, guessed if left out
* - `send_pw` = SMTP-password, guessed if left out
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `imap_certificate_checks` = how to check IMAP and SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* If you want to retrieve a value, use dc_get_config().
*
* @memberof dc_context_t
@@ -538,9 +534,6 @@ 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.
@@ -699,6 +692,12 @@ int dc_get_push_state (dc_context_t* context);
/**
* Configure a context.
*
* This way of configuring a context is deprecated,
* and does not allow to configure multiple transports.
* If you can, use the JSON-RPC API (../deltachat-jsonrpc/src/api.rs)
* `add_or_update_transport()`/`addOrUpdateTransport()` instead.
*
* During configuration IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* If the context is already configured,
@@ -1388,7 +1387,6 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
#define DC_GCM_ADDDAYMARKER 0x01
#define DC_GCM_INFO_ONLY 0x02
/**
@@ -1409,7 +1407,6 @@ 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.
*/
@@ -1473,7 +1470,6 @@ 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);
@@ -2819,19 +2815,6 @@ 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
/**
@@ -5804,7 +5787,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* These constants configure TLS certificate checks for IMAP and SMTP connections.
*
* These constants are set via dc_set_config()
* using keys "imap_certificate_checks" and "smtp_certificate_checks".
* using key "imap_certificate_checks".
*
* @addtogroup DC_CERTCK
* @{
@@ -6417,8 +6400,7 @@ 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,
* e.g. after calling dc_delete_all_locations(), this parameter is set to 0.
* If the locations of several contacts have been changed, this parameter is set to 0.
* @param data2 0
*/
#define DC_EVENT_LOCATION_CHANGED 2035
@@ -6689,14 +6671,6 @@ 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")
*/
@@ -7031,11 +7005,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in message summary text for notifications and chatlist.
#define DC_STR_FORWARDED 97
/// "Quota exceeding, already %1$s%% used."
///
/// Used as device message text.
///
/// `%1$s` will be replaced by the percentage used
/// @deprecated 2026-04-25
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
/// "Multi Device Synchronization"

View File

@@ -60,7 +60,6 @@ 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
@@ -1338,17 +1337,13 @@ 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 {
info_only,
add_daymarker,
},
MessageListOptions { add_daymarker },
)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
@@ -2546,7 +2541,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
}
let ctx = &*context;
block_on(location::send_locations_to_chat(
block_on(location::send_to_chat(
ctx,
ChatId::new(chat_id),
seconds as i64,
@@ -2566,14 +2561,14 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
return 0;
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
if chat_id == 0 {
block_on(location::is_sending(ctx))
.unwrap_or_log_default(ctx, "Failed is_sending_locations()") as libc::c_int
} else {
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
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
}
}
#[no_mangle]
@@ -2589,12 +2584,9 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
block_on(location::set(ctx, latitude, longitude, accuracy))
.log_err(ctx)
.unwrap_or_default() as libc::c_int
}
#[no_mangle]
@@ -2629,23 +2621,6 @@ 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() {

View File

@@ -318,15 +318,6 @@ 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);
@@ -1375,8 +1366,22 @@ impl CommandApi {
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
}
/// Returns all messages of a particular chat.
/// Get all message IDs belonging to a 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.
@@ -1384,17 +1389,14 @@ 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 {
info_only,
add_daymarker,
},
MessageListOptions { add_daymarker },
)
.await?;
Ok(msg
@@ -1426,21 +1428,24 @@ 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 {
info_only,
add_daymarker,
},
MessageListOptions { add_daymarker },
)
.await?;
Ok(msg
@@ -2115,6 +2120,21 @@ 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,
@@ -2137,6 +2157,39 @@ 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
// ---------------------------------------------
@@ -2379,9 +2432,6 @@ 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())
}

View File

@@ -287,8 +287,6 @@ 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.

View File

@@ -238,7 +238,7 @@ impl From<Qr> for QrObject {
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
@@ -257,7 +257,7 @@ impl From<Qr> for QrObject {
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::AskVerifyGroup {
grpname,
grpid,
@@ -278,7 +278,7 @@ impl From<Qr> for QrObject {
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::AskJoinBroadcast {
name,
grpid,
@@ -321,7 +321,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
@@ -338,7 +338,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
@@ -357,7 +357,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::WithdrawJoinBroadcast {
name,
grpid,
@@ -374,7 +374,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
@@ -391,7 +391,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
@@ -410,7 +410,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::ReviveJoinBroadcast {
name,
grpid,

View File

@@ -85,7 +85,7 @@ mod tests {
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;

View File

@@ -345,7 +345,6 @@ 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\
@@ -574,7 +573,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
}
if location::is_sending_locations_to_chat(&context, None).await? {
if location::is_sending(&context).await? {
println!("Location streaming enabled.");
}
println!("{cnt} chats");
@@ -623,7 +622,6 @@ 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,
},
)
@@ -782,11 +780,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"Location streaming: {}",
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
)
.await?,
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
);
}
"getlocations" => {
@@ -826,12 +820,7 @@ 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_locations_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
seconds,
)
.await?;
location::send_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(),
@@ -853,9 +842,6 @@ 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.");

View File

@@ -176,7 +176,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 40] = [
const CHAT_COMMANDS: [&str; 39] = [
"listchats",
"listarchived",
"start-realtime",
@@ -194,7 +194,6 @@ const CHAT_COMMANDS: [&str; 40] = [
"chatinfo",
"sendlocations",
"setlocation",
"dellocations",
"getlocations",
"send",
"send-sync",

View File

@@ -495,3 +495,7 @@ 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)

View File

@@ -206,9 +206,9 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
def get_messages(self, 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, info_only, add_daymarker)
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
@@ -277,6 +277,16 @@ 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,

View File

@@ -59,3 +59,11 @@ 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()

View File

@@ -0,0 +1,32 @@
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()

View File

@@ -9,8 +9,6 @@ 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
@@ -28,22 +26,6 @@ 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."""
@@ -168,6 +150,9 @@ def test_transport_synchronization(acfactory, log) -> None:
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport3["addr"])
# One event for updated `add_timestamp` of the new primary transport,
# one event for the `configured_addr` update.
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1_clone.list_transports()
assert ac1_clone.get_config("configured_addr") == addr3

View File

@@ -18,7 +18,7 @@ futures-lite = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["io-std"] }
tokio = { workspace = true, features = ["io-std", "signal"] }
tokio-util = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }

View File

@@ -7,27 +7,11 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained instant
"RUSTSEC-2024-0384",
# Archived repository
"RUSTSEC-2023-0089",
# Unmaintained paste
"RUSTSEC-2024-0436",
# Unmaintained rustls-pemfile
# It is a transitive dependency of iroh 0.35.0,
# this should be fixed by upgrading to iroh 1.0 once it is released.
"RUSTSEC-2025-0134",
# rustls-webpki v0.102.8
# We cannot upgrade to >=0.103.10 because
# it is a transitive dependency of iroh 0.35.0
# which depends on ^0.102.
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
"RUSTSEC-2026-0049",
"RUSTSEC-2026-0098",
"RUSTSEC-2026-0099"
]
[bans]
@@ -37,33 +21,51 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "bitflags", version = "1.3.2" },
{ name = "constant_time_eq", version = "0.3.1" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "block-buffer", version = "0.10.4" },
{ name = "chacha20", version = "0.9.1" },
{ name = "const-oid", version = "0.9.6" },
{ name = "convert_case", version = "0.5.0" },
{ name = "core-foundation", version = "0.9.4" },
{ name = "cpufeatures", version = "0.2.17" },
{ name = "crypto-common", version = "0.1.6" },
{ name = "curve25519-dalek", version = "4.1.3" },
{ name = "der", version = "0.7.9" },
{ name = "digest", version = "0.10.7" },
{ name = "ed25519-dalek", version = "2.1.1" },
{ name = "ed25519", version = "2.2.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "fiat-crypto", version = "0.2.9" },
{ name = "foldhash", version = "0.1.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "getrandom", version = "0.3.3" },
{ name = "hashbrown", version = "0.15.4" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "lru", version = "0.12.5" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "netlink-packet-route", version = "0.29.0" },
{ name = "nom", version = "7.1.3" },
{ name = "openssl-probe", version = "0.1.6" },
{ name = "pem-rfc7468", version = "0.7.0" },
{ name = "pkcs8", version = "0.10.2" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand_core", version = "0.9.3" },
{ name = "rand", version = "0.8.5" },
{ name = "rand", version = "0.9.4" },
{ name = "r-efi", version = "5.2.0" },
{ name = "rustix", version = "0.38.44" },
{ name = "rustls-webpki", version = "0.102.8" },
{ name = "security-framework", version = "2.11.1" },
{ name = "serdect", version = "0.2.0" },
{ name = "sha2", version = "0.10.9"},
{ name = "signature", version = "2.2.0"},
{ name = "socket2", version = "0.5.9" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "spki", version = "0.7.3"},
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "vergen-lib", version = "0.1.6" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "webpki-roots", version = "0.26.8" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
{ name = "windows_aarch64_msvc" },
@@ -80,6 +82,7 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "wit-bindgen", version = "0.51.0" },
]
@@ -91,6 +94,7 @@ allow = [
"BSD-3-Clause",
"BSL-1.0", # Boost Software License 1.0
"CC0-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MIT",
"MPL-2.0",

View File

@@ -433,7 +433,6 @@ class ACFactory:
if self.pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
assert "addr" in configdict and "mail_pw" in configdict
return configdict
@@ -505,7 +504,6 @@ class ACFactory:
"addr": cloned_from.get_config("addr"),
"mail_pw": cloned_from.get_config("mail_pw"),
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
}
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
REV=ad097ee40579c884e7757de2d3bb0a51f481a32a
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

View File

@@ -8,6 +8,7 @@ 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;
@@ -22,6 +23,7 @@ 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;
@@ -536,6 +538,38 @@ 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.

View File

@@ -4,9 +4,8 @@
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use anyhow::{Context as _, Error, Result, bail};
use anyhow::{Context as _, Result, bail};
use crate::key::{DcKey, SignedPublicKey};
@@ -28,10 +27,8 @@ impl fmt::Display for EncryptPreference {
}
}
impl FromStr for EncryptPreference {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
impl EncryptPreference {
fn new(s: &str) -> Result<Self> {
match s {
"mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference),
@@ -85,10 +82,8 @@ impl fmt::Display for Aheader {
}
}
impl FromStr for Aheader {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
impl Aheader {
pub(crate) fn from_str(s: &str) -> Result<Self> {
let mut attributes: BTreeMap<String, String> = s
.split(';')
.filter_map(|a| {
@@ -116,7 +111,7 @@ impl FromStr for Aheader {
let prefer_encrypt = attributes
.remove("prefer-encrypt")
.and_then(|raw| raw.parse().ok())
.and_then(|raw| EncryptPreference::new(&raw).ok())
.unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
@@ -144,8 +139,9 @@ mod tests {
#[test]
fn test_from_str() -> Result<()> {
let h: Aheader =
format!("addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}").parse()?;
let h = Aheader::from_str(&format!(
"addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}"
))?;
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
@@ -157,7 +153,7 @@ mod tests {
#[test]
fn test_from_str_reset() -> Result<()> {
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
let h: Aheader = raw.parse()?;
let h = Aheader::from_str(&raw)?;
assert_eq!(h.addr, "reset@example.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
@@ -167,7 +163,7 @@ mod tests {
#[test]
fn test_from_str_non_critical() -> Result<()> {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={RAWKEY}");
let h: Aheader = raw.parse()?;
let h = Aheader::from_str(&raw)?;
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
@@ -177,7 +173,7 @@ mod tests {
#[test]
fn test_from_str_superflous_critical() {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={RAWKEY}");
assert!(raw.parse::<Aheader>().is_err());
assert!(Aheader::from_str(&raw).is_err());
}
#[test]

View File

@@ -1,561 +0,0 @@
//! 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(())
}
}

View File

@@ -284,10 +284,6 @@ 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,

View File

@@ -445,7 +445,6 @@ 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",
@@ -453,6 +452,7 @@ 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,8 +734,6 @@ 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(())
}

View File

@@ -6,7 +6,6 @@ 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};
@@ -23,8 +22,9 @@ use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
TIMESTAMP_SENT_TOLERANCE,
};
use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
@@ -34,7 +34,7 @@ use crate::download::{
};
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::key::{Fingerprint, self_fingerprint};
use crate::location;
use crate::log::{LogExt, warn};
use crate::logged_debug_assert;
@@ -1210,7 +1210,8 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
);
let fingerprint = contact
.fingerprint()
.context("Contact does not have a fingerprint in encrypted chat")?;
.context("Contact does not have a fingerprint in encrypted chat")?
.human_readable();
if let Some(public_key) = contact.public_key(context).await? {
if let Some(relay_addrs) = addresses_from_public_key(&public_key) {
let relays = relay_addrs.join(",");
@@ -2466,10 +2467,7 @@ 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
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
{
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
// formats as GIF or VOICE.
//
@@ -2477,12 +2475,7 @@ 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 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 {
if better_type == Viewtype::Image {
maybe_image = true;
} else if better_type != Viewtype::Webxdc
|| context
@@ -2502,10 +2495,7 @@ 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
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
{
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
let new_name = blob
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
.await?;
@@ -2946,17 +2936,19 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
"
UPDATE msgs SET
timestamp=(
SELECT MAX(timestamp) FROM msgs WHERE
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
-- From `InFresh` to `OutMdnRcvd` inclusive except `OutDraft`.
state IN(10,13,16,18,20,24,26,28) AND
hidden IN(0,1) AND
chat_id=?
chat_id=? AND
id<=?
),
pre_rfc724_mid=?, subject=?, param=?
WHERE id=?
",
(
msg.chat_id,
msg.id,
&msg.pre_rfc724_mid,
&msg.subject,
msg.param.to_string(),
@@ -3105,9 +3097,6 @@ 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,
}
@@ -3118,56 +3107,27 @@ 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.
/// Returns messages belonging to the chat according to the given options,
/// sorted by oldest message first.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_chat_msgs_ex(
context: &Context,
chat_id: ChatId,
options: MessageListOptions,
) -> Result<Vec<ChatItem>> {
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(&params) {
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 MessageListOptions { add_daymarker } = options;
let process_row = |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
@@ -3202,39 +3162,18 @@ pub async fn get_chat_msgs_ex(
Ok(ret)
};
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
let items = 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)
}
@@ -4009,9 +3948,47 @@ pub(crate) async fn add_contact_to_chat_ex(
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
if chat.typ == Chattype::OutBroadcast {
resend_last_msgs(context, chat.id, &contact)
.await
.log_err(context)
.ok();
}
Ok(true)
}
async fn resend_last_msgs(context: &Context, chat_id: ChatId, to_contact: &Contact) -> Result<()> {
let msgs: Vec<MsgId> = context
.sql
.query_map_vec(
"
SELECT id
FROM msgs
WHERE chat_id=?
AND hidden=0
AND NOT ( -- Exclude info and system messages
param GLOB '*\nS=*' OR param GLOB 'S=*'
OR from_id=?
OR to_id=?
)
AND type!=?
ORDER BY timestamp DESC, id DESC LIMIT ?",
(
chat_id,
ContactId::INFO,
ContactId::INFO,
Viewtype::Webxdc,
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER,
),
|row: &rusqlite::Row| Ok(row.get::<_, MsgId>(0)?),
)
.await?
.into_iter()
.rev()
.collect();
resend_msgs_ex(context, &msgs, to_contact.fingerprint()).await
}
/// Returns true if an avatar should be attached in the given chat.
///
/// This function does not check if the avatar is set.
@@ -4675,10 +4652,26 @@ pub(crate) async fn save_copy_in_self_talk(
Ok(msg.rfc724_mid)
}
/// Resends given messages with the same Message-ID.
/// Resends given messages to members of the corresponding chats.
///
/// This is primarily intended to make existing webxdcs available to new chat members.
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
resend_msgs_ex(context, msg_ids, None).await
}
/// Resends given messages to a contact with fingerprint `to_fingerprint` or, if it's `None`, to
/// members of the corresponding chats.
///
/// NB: Actually `to_fingerprint` is only passed for `OutBroadcast` chats when a new member is
/// added. Regarding webxdcs: It is not trivial to resend only the own status updates,
/// and it is not trivial to resend them only to the newly-joined member,
/// so that for now, [`resend_last_msgs`] does not automatically resend webxdcs at all.
pub(crate) async fn resend_msgs_ex(
context: &Context,
msg_ids: &[MsgId],
to_fingerprint: Option<Fingerprint>,
) -> Result<()> {
let to_fingerprint = to_fingerprint.map(|f| f.hex());
let mut msgs: Vec<Message> = Vec::new();
for msg_id in msg_ids {
let msg = Message::load_from_db(context, *msg_id).await?;
@@ -4697,10 +4690,17 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
| MessageState::OutFailed
| MessageState::OutDelivered
| MessageState::OutMdnRcvd => {
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
// Broadcast owners shouldn't see spinners on messages being auto-re-sent to new
// subscribers (otherwise big channel owners will see spinners most of the time).
if to_fingerprint.is_none() {
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
}
}
msg_state => bail!("Unexpected message state {msg_state}"),
}
if let Some(to_fingerprint) = &to_fingerprint {
msg.param.set(Param::Arg4, to_fingerprint.clone());
}
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
continue;
}
@@ -4712,7 +4712,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
chat_id: msg.chat_id,
msg_id: msg.id,
});
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
// The event only matters if the message is last in the chat.
// But it's probably too expensive check, and UIs anyways need to debounce.
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if msg.viewtype == Viewtype::Webxdc {
@@ -4905,8 +4906,6 @@ 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
@@ -4922,9 +4921,6 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
(),
)
.await?;
context
.set_config_internal(Config::QuotaExceeding, None)
.await?;
Ok(())
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use super::*;
use crate::Event;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER};
use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef;
use crate::imex::{ImexMode, has_backup, imex};
@@ -2032,12 +2032,6 @@ 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);
@@ -2075,13 +2069,7 @@ async fn test_chat_get_color_encrypted() -> Result<()> {
Ok(())
}
async fn test_sticker(
filename: &str,
bytes: &[u8],
res_viewtype: Viewtype,
w: i32,
h: i32,
) -> Result<()> {
async fn test_sticker(filename: &str, bytes: &[u8], 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;
@@ -2097,7 +2085,7 @@ async fn test_sticker(
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, bob_chat.id);
assert_eq!(msg.get_viewtype(), res_viewtype);
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
assert_eq!(msg.get_filename().unwrap(), filename);
assert_eq!(msg.get_width(), w);
assert_eq!(msg.get_height(), h);
@@ -2111,7 +2099,6 @@ async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
include_bytes!("../../test-data/image/logo.png"),
Viewtype::Sticker,
135,
135,
)
@@ -2123,7 +2110,6 @@ async fn test_sticker_jpeg() -> Result<()> {
test_sticker(
"sticker.jpg",
include_bytes!("../../test-data/image/avatar1000x1000.jpg"),
Viewtype::Image,
1000,
1000,
)
@@ -2131,10 +2117,33 @@ async fn test_sticker_jpeg() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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;
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;
let file = alice.get_blobdir().join("sticker.jpg");
tokio::fs::write(
@@ -2144,53 +2153,38 @@ async fn test_sticker_jpeg_force() {
.await
.unwrap();
// Images without force_sticker should be turned into [Viewtype::Image]
// Send a sticker.
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 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 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::Sticker);
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
// even on drafted messages
// Send a sticker reusing the file.
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 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)
.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
@@ -2692,6 +2686,49 @@ async fn test_resend_own_message() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_doesnt_resort_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp = create_group(alice, "").await?;
let sent1 = alice.send_text(alice_grp, "hi").await;
let sent1_ts = Message::load_from_db(alice, sent1.sender_msg_id)
.await?
.timestamp_sort;
SystemTime::shift(Duration::from_secs(60));
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
let sent2 = alice
.send_text(
alice_grp,
"Let's test resending, there are very few tests on it",
)
.await;
let resent_msg_id = sent1.sender_msg_id;
resend_msgs(alice, &[resent_msg_id]).await?;
assert_eq!(
resent_msg_id.get_state(alice).await?,
MessageState::OutPending
);
alice.pop_sent_msg().await;
assert_eq!(
resent_msg_id.get_state(alice).await?,
MessageState::OutDelivered
);
assert_eq!(
Message::load_from_db(alice, sent1.sender_msg_id)
.await?
.timestamp_sort,
sent1_ts
);
assert_eq!(
alice.get_last_msg_id_in(alice_grp).await,
sent2.sender_msg_id
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_foreign_message_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -2805,6 +2842,15 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
"alice@example.org charlie@example.net"
);
// Check additionally that subscribers don't send "Chat-Group-Name*" headers.
let parsed = alice.parse_msg(&request_with_auth).await;
assert!(parsed.get_header(HeaderDef::ChatGroupName).is_none());
assert!(
parsed
.get_header(HeaderDef::ChatGroupNameTimestamp)
.is_none()
);
alice.recv_msg_trash(&request_with_auth).await;
}
@@ -2947,6 +2993,56 @@ async fn test_broadcast_change_name() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_resend_to_new_member() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_bc_id = create_broadcast(alice, "Channel".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let mut alice_msg_ids = Vec::new();
for i in 0..(N_MSGS_TO_NEW_BROADCAST_MEMBER + 1) {
alice_msg_ids.push(
alice
.send_text(alice_bc_id, &i.to_string())
.await
.sender_msg_id,
);
}
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
for msg_id in alice_msg_ids {
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
}
for i in 0..N_MSGS_TO_NEW_BROADCAST_MEMBER {
let rev_order = false;
let resent_msg = alice
.pop_sent_msg_ex(rev_order, Duration::ZERO)
.await
.unwrap();
let fiona_msg = fiona.recv_msg(&resent_msg).await;
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
assert_eq!(fiona_msg.text, (i + 1).to_string());
assert!(resent_msg.recipients.contains("fiona@example.net"));
assert!(!resent_msg.recipients.contains("bob@"));
// The message is undecryptable for Bob, he mustn't be able to know yet that somebody joined
// the broadcast even if he is a postman in this land. E.g. Fiona may leave after fetching
// the news, Bob won't know about that.
assert!(
MimeMessage::from_bytes(bob, resent_msg.payload().as_bytes())
.await?
.decryption_error
.is_some()
);
bob.recv_msg_trash(&resent_msg).await;
}
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
Ok(())
}
/// - Alice has multiple devices
/// - Alice creates a broadcast and sends a message into it
/// - Alice's second device sees the broadcast
@@ -5720,7 +5816,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 the end
// Bob receives both messages and has nothing at 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);
@@ -5728,6 +5824,11 @@ 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;

View File

@@ -42,50 +42,85 @@ use crate::{constants, stats};
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
/// Deprecated(2026-04).
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
/// or add_transport{from_qr}()/list_transports() instead.
///
/// Email address, used in the `From:` field.
Addr,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server hostname.
MailServer,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server username.
MailUser,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server password.
MailPw,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server port.
MailPort,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server security (e.g. TLS, STARTTLS).
MailSecurity,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// How to check TLS certificates.
///
/// "IMAP" in the name is for compatibility,
/// this actually applies to both IMAP and SMTP connections.
ImapCertificateChecks,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server hostname.
SendServer,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server username.
SendUser,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server password.
SendPw,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server port.
SendPort,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server security (e.g. TLS, STARTTLS).
SendSecurity,
/// Deprecated option for backwards compatibility.
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
SmtpCertificateChecks,
/// Whether to use OAuth 2.
///
/// Historically contained other bitflags, which are now deprecated.
@@ -155,10 +190,6 @@ 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,
@@ -185,32 +216,47 @@ pub enum Config {
/// The primary email address.
ConfiguredAddr,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// List of configured IMAP servers as a JSON array.
ConfiguredImapServers,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured IMAP server hostname.
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailServer,
/// Configured IMAP server port.
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// This is replaced by `configured_imap_servers` for new configurations.
/// Configured IMAP server port.
ConfiguredMailPort,
/// Configured IMAP server security (e.g. TLS, STARTTLS).
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// This is replaced by `configured_imap_servers` for new configurations.
/// Configured IMAP server security (e.g. TLS, STARTTLS).
ConfiguredMailSecurity,
/// Configured IMAP server username.
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// This is set if user has configured username manually.
/// Configured IMAP server username.
ConfiguredMailUser,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured IMAP server password.
ConfiguredMailPw,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured TLS certificate checks.
/// This option is saved on successful configuration
/// and should not be modified manually.
@@ -219,37 +265,53 @@ pub enum Config {
/// but has "IMAP" in the name for backwards compatibility.
ConfiguredImapCertificateChecks,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// List of configured SMTP servers as a JSON array.
ConfiguredSmtpServers,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server hostname.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendServer,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server port.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendPort,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server security (e.g. TLS, STARTTLS).
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendSecurity,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server username.
///
/// This is set if user has configured username manually.
ConfiguredSendUser,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server password.
ConfiguredSendPw,
/// Deprecated, stored for backwards compatibility.
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// ConfiguredImapCertificateChecks is actually used.
ConfiguredSmtpCertificateChecks,
/// Whether OAuth 2 is used with configured provider.
ConfiguredServerFlags,
@@ -262,6 +324,9 @@ pub enum Config {
/// ID of the configured provider from the provider database.
ConfiguredProvider,
/// Deprecated(2026-04).
/// Use [`Context::is_configured()`] instead.
///
/// True if account is configured.
Configured,
@@ -302,11 +367,6 @@ 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,
@@ -347,12 +407,6 @@ 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,
@@ -450,11 +504,7 @@ impl Config {
pub(crate) fn is_synced(&self) -> bool {
matches!(
self,
Self::Displayname
| Self::MdnsEnabled
| Self::ShowEmails
| Self::Selfavatar
| Self::Selfstatus,
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
)
}
@@ -895,15 +945,10 @@ impl Context {
/// Returns `false` if no addresses are configured.
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
Ok(self
.get_config(Config::ConfiguredAddr)
.get_all_self_addrs()
.await?
.iter()
.any(|a| addr_cmp(addr, a))
|| self
.get_secondary_self_addrs()
.await?
.iter()
.any(|a| addr_cmp(addr, a)))
.any(|a| addr_cmp(addr, a)))
}
/// Sets `primary_new` as the new primary self address and saves the old
@@ -950,14 +995,6 @@ 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>> {

View File

@@ -196,13 +196,6 @@ 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?;

View File

@@ -76,7 +76,7 @@ impl Context {
/// Deprecated since 2025-02; use `add_transport_from_qr()`
/// or `add_or_update_transport()` instead.
pub async fn configure(&self) -> Result<()> {
let mut param = EnteredLoginParam::load(self).await?;
let mut param = EnteredLoginParam::load_legacy(self).await?;
self.add_transport_inner(&mut param).await
}
@@ -150,7 +150,7 @@ impl Context {
progress!(self, 0, Some(error_msg.clone()));
bail!(error_msg);
} else {
param.save(self).await?;
param.save_legacy(self).await?;
progress!(self, 1000);
}

View File

@@ -36,17 +36,6 @@ 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,
)]
@@ -244,6 +233,9 @@ Here is what to do:
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
/// How many recent messages should be re-sent to a new broadcast member.
pub(crate) const N_MSGS_TO_NEW_BROADCAST_MEMBER: usize = 10;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -259,18 +251,6 @@ 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

View File

@@ -1396,7 +1396,7 @@ WHERE addr=?
let Some(fingerprint_other) = contact.fingerprint() else {
return Ok(stock_str::encr_none(context));
};
let fingerprint_other = fingerprint_other.to_string();
let fingerprint_other = fingerprint_other.human_readable();
let stock_message = if contact.public_key(context).await?.is_some() {
stock_str::messages_are_e2ee(context)
@@ -1410,7 +1410,7 @@ WHERE addr=?
let fingerprint_self = load_self_public_key(context)
.await?
.dc_fingerprint()
.to_string();
.human_readable();
if addr < contact.addr {
cat_fingerprint(
&mut ret,

View File

@@ -568,15 +568,6 @@ 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) {
@@ -852,7 +843,7 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let all_self_addrs = self.get_all_self_addrs().await?.join(", ");
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
.await?
.into_iter()
@@ -954,11 +945,7 @@ impl Context {
}
}
res.insert("secondary_addrs", secondary_addrs);
res.insert(
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert("all_self_addrs", all_self_addrs);
res.insert(
"who_can_call_me",
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
@@ -1004,18 +991,6 @@ 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)

View File

@@ -284,7 +284,6 @@ async fn test_get_info_completeness() {
"send_security",
"server_flags",
"skip_start_messages",
"smtp_certificate_checks",
"proxy_url", // May contain passwords, don't leak it to the logs.
"socks5_enabled", // SOCKS5 options are deprecated.
"socks5_host",
@@ -603,10 +602,7 @@ 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::ShowEmails).await?,
Some("2".to_string())
);
assert_eq!(alice.get_config(Config::Displayname).await?, None);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
@@ -614,24 +610,21 @@ async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('displayname', 'Alice 2')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
assert_eq!(alice.get_config(Config::Displayname).await?, None);
// 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::ShowEmails).await?,
Some("0".to_string())
alice.get_config(Config::Displayname).await?,
Some("Alice 2".to_string())
);
Ok(())

View File

@@ -234,8 +234,7 @@ 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,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
/// If the locations of several contacts have been changed, this parameter is set to `None`.
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().

View File

@@ -287,7 +287,6 @@ 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};
@@ -555,13 +554,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; 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;
@@ -579,10 +572,6 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; 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);

View File

@@ -489,7 +489,7 @@ impl Imap {
let session = match self.connect(context, configuring).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err);
self.connectivity.set_err(context, format!("{err:#}"));
return Err(err);
}
};
@@ -945,29 +945,6 @@ 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(
@@ -2024,7 +2001,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)
}

View File

@@ -69,7 +69,7 @@ pub struct BackupProvider {
_endpoint: Endpoint,
/// iroh address.
node_addr: iroh::NodeAddr,
node_addr: iroh::EndpointAddr,
/// Authentication token that should be submitted
/// to retrieve the backup.
@@ -95,13 +95,12 @@ impl BackupProvider {
/// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
pub async fn prepare(context: &Context) -> Result<Self> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.alpns(vec![BACKUP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
.await?;
let node_addr = endpoint.node_addr().await?;
let node_addr = endpoint.addr();
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
@@ -168,7 +167,7 @@ impl BackupProvider {
async fn handle_connection(
context: Context,
conn: iroh::endpoint::Connecting,
conn: iroh::endpoint::Accepting,
auth_token: String,
dbfile: Arc<TempPathGuard>,
) -> Result<()> {
@@ -299,13 +298,12 @@ impl Future for BackupProvider {
pub async fn get_backup2(
context: &Context,
node_addr: iroh::NodeAddr,
node_addr: iroh::EndpointAddr,
auth_token: String,
) -> Result<()> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(relay_mode)
.bind()
.await?;
@@ -353,7 +351,7 @@ pub async fn get_backup2(
/// This is a long running operation which will return only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
/// does avoid having [`iroh::EndpointAddr`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
match qr {

View File

@@ -1,7 +1,7 @@
//! Cryptographic key module.
use std::collections::BTreeMap;
use std::fmt;
use std::fmt::{self, Write as _};
use std::io::Cursor;
use anyhow::{Context as _, Result, bail, ensure};
@@ -583,6 +583,21 @@ impl Fingerprint {
pub fn hex(&self) -> String {
hex::encode_upper(&self.0)
}
/// Make a human-readable fingerprint.
pub fn human_readable(&self) -> String {
let mut f = String::new();
// Split key into chunks of 4 with space and newline at 20 chars
for (i, c) in self.hex().chars().enumerate() {
if i > 0 && i % 20 == 0 {
writeln!(&mut f).ok();
} else if i > 0 && i % 4 == 0 {
write!(&mut f, " ").ok();
}
write!(&mut f, "{c}").ok();
}
f
}
}
impl From<pgp::types::Fingerprint> for Fingerprint {
@@ -599,22 +614,6 @@ impl fmt::Debug for Fingerprint {
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Split key into chunks of 4 with space and newline at 20 chars
for (i, c) in self.hex().chars().enumerate() {
if i > 0 && i % 20 == 0 {
writeln!(f)?;
} else if i > 0 && i % 4 == 0 {
write!(f, " ")?;
}
write!(f, "{c}")?;
}
Ok(())
}
}
/// Parse a human-readable or otherwise formatted fingerprint.
impl std::str::FromStr for Fingerprint {
type Err = anyhow::Error;
@@ -890,7 +889,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
]);
assert_eq!(
fp.to_string(),
fp.human_readable(),
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
);
}

View File

@@ -101,7 +101,6 @@ mod update_helper;
pub mod webxdc;
#[macro_use]
mod dehtml;
mod authres;
pub mod color;
pub mod html;
pub mod net;

View File

@@ -264,15 +264,11 @@ impl Kml {
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
#[expect(clippy::arithmetic_side_effects)]
pub async fn send_locations_to_chat(
context: &Context,
chat_id: ChatId,
seconds: i64,
) -> Result<()> {
pub async fn send_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_locations_to_chat(context, Some(chat_id)).await?;
let is_sending_locations_before = is_sending_to_chat(context, chat_id).await?;
context
.sql
.execute(
@@ -305,35 +301,49 @@ pub async fn send_locations_to_chat(
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)
/// 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(())
}
/// Sets current location of the user device.
@@ -459,13 +469,6 @@ 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.
@@ -495,7 +498,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_location(context: &Context, location_id: u32) -> Result<()> {
pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()> {
context
.sql
.execute(
@@ -507,7 +510,7 @@ pub(crate) async fn delete_poi_location(context: &Context, location_id: u32) ->
}
/// Deletes POI locations that don't have corresponding message anymore.
pub(crate) async fn delete_orphaned_poi_locations(context: &Context) -> Result<()> {
pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> {
context.sql.execute("
DELETE FROM locations
WHERE independent=1 AND id NOT IN
@@ -716,9 +719,9 @@ pub(crate) async fn save(
pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
loop {
let next_event = match maybe_send_locations(context).await {
let next_event = match maybe_send(context).await {
Err(err) => {
warn!(context, "maybe_send_locations failed: {:#}", err);
warn!(context, "location::maybe_send failed: {:#}", err);
Some(60) // Retry one minute later.
}
Ok(next_event) => next_event,
@@ -756,7 +759,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_locations(context: &Context) -> Result<Option<u64>> {
async fn maybe_send(context: &Context) -> Result<Option<u64>> {
let mut next_event: Option<u64> = None;
let now = time();
@@ -1051,7 +1054,7 @@ Content-Disposition: attachment; filename="location.kml"
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
send_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.");
@@ -1103,7 +1106,7 @@ Content-Disposition: attachment; filename="location.kml"
// Alice enables location streaming.
// Bob receives a message saying that Alice enabled location streaming.
send_locations_to_chat(alice, alice_chat.id, 60).await?;
send_to_chat(alice, alice_chat.id, 60).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Alice gets new location from GPS.
@@ -1113,7 +1116,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_locations(alice).await?;
maybe_send(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);

View File

@@ -138,10 +138,11 @@ pub struct EnteredLoginParam {
}
impl EnteredLoginParam {
/// Loads entered account settings.
/// Loads entered account settings
/// that were set by the deprecated `configured_*` configs.
///
/// This is a legacy API for loading from separate config parameters.
pub(crate) async fn load(context: &Context) -> Result<Self> {
/// This is only needed by tests and clients using the old CFFI API.
pub(crate) async fn load_legacy(context: &Context) -> Result<Self> {
let addr = context
.get_config(Config::Addr)
.await?
@@ -178,7 +179,7 @@ impl EnteredLoginParam {
// The setting is named `imap_certificate_checks`
// for backwards compatibility,
// but now it is a global setting applied to all protocols,
// while `smtp_certificate_checks` is ignored.
// while `smtp_certificate_checks` has been removed.
let certificate_checks = if let Some(certificate_checks) = context
.get_config_parsed::<i32>(Config::ImapCertificateChecks)
.await?
@@ -241,7 +242,10 @@ impl EnteredLoginParam {
/// Saves entered account settings,
/// so that they can be prefilled if the user wants to configure the server again.
pub(crate) async fn save(&self, context: &Context) -> Result<()> {
///
/// This is needed in case a UI is not yet updated, and still uses `get_config("mail_pw")` etc.
/// in order to prefill the entered account settings.
pub(crate) async fn save_legacy(&self, context: &Context) -> Result<()> {
context.set_config(Config::Addr, Some(&self.addr)).await?;
context
@@ -364,7 +368,7 @@ mod tests {
.await?;
t.set_config(Config::MailPw, Some("foobarbaz")).await?;
let param = EnteredLoginParam::load(t).await?;
let param = EnteredLoginParam::load_legacy(t).await?;
assert_eq!(param.addr, "alice@example.org");
assert_eq!(
param.certificate_checks,
@@ -373,13 +377,13 @@ mod tests {
t.set_config(Config::ImapCertificateChecks, Some("1"))
.await?;
let param = EnteredLoginParam::load(t).await?;
let param = EnteredLoginParam::load_legacy(t).await?;
assert_eq!(param.certificate_checks, EnteredCertificateChecks::Strict);
// Fail to load invalid settings, but do not panic.
t.set_config(Config::ImapCertificateChecks, Some("999"))
.await?;
assert!(EnteredLoginParam::load(t).await.is_err());
assert!(EnteredLoginParam::load_legacy(t).await.is_err());
Ok(())
}
@@ -407,7 +411,7 @@ mod tests {
certificate_checks: Default::default(),
oauth2: false,
};
param.save(&t).await?;
param.save_legacy(&t).await?;
assert_eq!(
t.get_config(Config::Addr).await?.unwrap(),
"alice@example.org"
@@ -416,7 +420,7 @@ mod tests {
assert_eq!(t.get_config(Config::SendPw).await?, None);
assert_eq!(t.get_config_int(Config::SendPort).await?, 2947);
assert_eq!(EnteredLoginParam::load(&t).await?, param);
assert_eq!(EnteredLoginParam::load_legacy(&t).await?, param);
Ok(())
}

View File

@@ -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::delete_poi_location;
use crate::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
WHERE m.id=? AND chat_id!=3 -- DC_CHAT_ID_TRASH
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 [`send_locations_to_chat()`].
/// this is done by [`location::set()`] and [`location::send_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
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
/// [`location::send_to_chat()`]: crate::location::send_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,12 +795,6 @@ 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
@@ -1655,7 +1649,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 {
delete_poi_location(context, msg.location_id).await?;
location::delete_poi(context, msg.location_id).await?;
}
let on_server = true;
msg.id
@@ -2123,7 +2117,6 @@ 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(
@@ -2322,8 +2315,6 @@ 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.

View File

@@ -194,6 +194,7 @@ fn new_address_with_name(name: &str, address: String) -> Address<'static> {
}
impl MimeFactory {
/// Returns `MimeFactory` for rendering `msg`.
#[expect(clippy::arithmetic_side_effects)]
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
let now = time();
@@ -1394,10 +1395,7 @@ impl MimeFactory {
}
}
if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
{
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
headers.push((
"Chat-Group-Name",
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
@@ -1408,7 +1406,11 @@ impl MimeFactory {
mail_builder::headers::text::Text::new(ts.to_string()).into(),
));
}
}
if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
{
match command {
SystemMessage::MemberRemovedFromGroup => {
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
@@ -1623,7 +1625,7 @@ impl MimeFactory {
// We should not send `null` as relay URL
// as this is the only way to reach the node.
debug_assert!(node_addr.relay_url().is_some());
debug_assert_eq!(node_addr.relay_urls().count(), 1);
headers.push((
HeaderDef::IrohNodeAddr.into(),
mail_builder::headers::text::Text::new(serde_json::to_string(&node_addr)?)
@@ -1827,7 +1829,7 @@ impl MimeFactory {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
if location::is_sending_to_chat(context, msg.chat_id).await?
&& let Some(part) = self.get_location_kml_part(context).await?
{
parts.push(part);
@@ -2225,18 +2227,18 @@ fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool {
/// rather than all recipients.
/// This function returns the fingerprint of the recipient the message should be sent to.
fn must_have_only_one_recipient<'a>(msg: &'a Message, chat: &Chat) -> Option<Result<&'a str>> {
if chat.typ == Chattype::OutBroadcast
&& matches!(
msg.param.get_cmd(),
SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
)
{
let Some(fp) = msg.param.get(Param::Arg4) else {
return Some(Err(format_err!("Missing removed/added member")));
};
return Some(Ok(fp));
if chat.typ != Chattype::OutBroadcast {
None
} else if let Some(fp) = msg.param.get(Param::Arg4) {
Some(Ok(fp))
} else if matches!(
msg.param.get_cmd(),
SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
) {
Some(Err(format_err!("Missing removed/added member")))
} else {
None
}
None
}
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {

View File

@@ -506,11 +506,6 @@ 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();

View File

@@ -14,9 +14,8 @@ 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::ChatId;
use crate::chat::{Chat, ChatId};
use crate::config::Config;
use crate::constants;
use crate::contact::{ContactId, import_public_key};
@@ -275,7 +274,7 @@ impl MimeMessage {
let timestamp_rcvd = smeared_time(context);
let mut timestamp_sent =
Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
let mut hop_info = parse_receive_headers(&mail.get_headers());
let hop_info = parse_receive_headers(&mail.get_headers());
let mut headers = Default::default();
let mut headers_removed = HashSet::<String>::new();
@@ -366,11 +365,7 @@ 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?;
@@ -2132,7 +2127,7 @@ async fn parse_gossip_headers(
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
for value in &gossip_headers {
let header = match value.parse::<Aheader>() {
let header = match Aheader::from_str(value) {
Ok(header) => header,
Err(err) => {
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
@@ -2582,6 +2577,10 @@ 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()

View File

@@ -136,6 +136,10 @@ pub enum Param {
/// For "MemberAddedToGroup" and "MemberRemovedFromGroup",
/// this is the fingerprint added to / removed from the group.
///
/// For messages resent when adding a new member to a broadcast channel,
/// this is the fingerprint of the added member;
/// the message must only be sent to this one member then.
///
/// For call messages, this is the end timsetamp.
Arg4 = b'H',
@@ -243,9 +247,6 @@ 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',

View File

@@ -19,18 +19,22 @@
//! This message contains the users relay-server and public key.
//! Direct IP address is not included as this information can be persisted by email providers.
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
//! 5. Upon receiving an announcement message, other peers store the sender's [NodeAddr] in the database
//! 5. Upon receiving an announcement message, other peers store the sender's [EndpointAddr] in the database
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
//! and `joinRealtimeChannel().send()` just like the other peers.
use anyhow::{Context as _, Result, anyhow, bail};
use data_encoding::BASE32_NOPAD;
use futures_lite::StreamExt;
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
use iroh::address_lookup::MemoryLookup;
use iroh::{
Endpoint, EndpointAddr, EndpointId, PublicKey, RelayMode, RelayUrl, SecretKey, TransportAddr,
};
use iroh_gossip::api::{Event as GossipEvent, GossipReceiver, GossipSender, JoinOptions};
use iroh_gossip::net::{GOSSIP_ALPN, Gossip};
use iroh_gossip::proto::TopicId;
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap};
use std::collections::HashMap;
use std::env;
use tokio::sync::{RwLock, oneshot};
use tokio::task::JoinHandle;
@@ -54,6 +58,9 @@ pub struct Iroh {
/// Iroh router needed for Iroh peer channels.
pub(crate) router: iroh::protocol::Router,
/// Address lookup, called "Discovery service" before Iroh 0.96.0.
pub(crate) address_lookup: MemoryLookup,
/// [Gossip] needed for Iroh peer channels.
pub(crate) gossip: Gossip,
@@ -105,7 +112,7 @@ impl Iroh {
}
let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
let node_ids = peers.iter().map(|p| p.id).collect::<Vec<_>>();
info!(
ctx,
@@ -115,7 +122,7 @@ impl Iroh {
// Inform iroh of potentially new node addresses
for node_addr in &peers {
if !node_addr.is_empty() {
self.router.endpoint().add_node_addr(node_addr.clone())?;
self.address_lookup.add_endpoint_info(node_addr.clone());
}
}
@@ -124,6 +131,7 @@ impl Iroh {
let (gossip_sender, gossip_receiver) = self
.gossip
.subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
.await?
.split();
let ctx = ctx.clone();
@@ -139,10 +147,10 @@ impl Iroh {
}
/// Add gossip peer to realtime channel if it is already active.
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: EndpointAddr) -> Result<()> {
if self.iroh_channels.read().await.get(&topic).is_some() {
self.router.endpoint().add_node_addr(peer.clone())?;
self.gossip.subscribe(topic, vec![peer.node_id])?;
self.address_lookup.add_endpoint_info(peer.clone());
self.gossip.subscribe(topic, vec![peer.id]).await?;
}
Ok(())
}
@@ -184,16 +192,20 @@ impl Iroh {
*entry
}
/// Get the iroh [NodeAddr] without direct IP addresses.
/// Get the iroh [EndpointAddr] without direct IP addresses.
///
/// The address is guaranteed to have home relay URL set
/// as it is the only way to reach the node
/// without global discovery mechanisms.
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
let mut addr = self.router.endpoint().node_addr().await?;
addr.direct_addresses = BTreeSet::new();
debug_assert!(addr.relay_url().is_some());
Ok(addr)
pub(crate) async fn get_node_addr(&self) -> Result<EndpointAddr> {
// Wait until home relay connection is established.
self.router.endpoint().online().await;
let mut endpoint_addr = self.router.endpoint().addr();
endpoint_addr
.addrs
.retain(|addr| matches!(addr, TransportAddr::Relay(_)));
debug_assert_eq!(endpoint_addr.addrs.len(), 1);
Ok(endpoint_addr)
}
/// Leave the realtime channel for a given topic.
@@ -219,11 +231,11 @@ pub(crate) struct ChannelState {
/// The subscribe loop handle.
subscribe_loop: JoinHandle<()>,
sender: iroh_gossip::net::GossipSender,
sender: GossipSender,
}
impl ChannelState {
fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
fn new(subscribe_loop: JoinHandle<()>, sender: GossipSender) -> Self {
Self {
subscribe_loop,
sender,
@@ -235,7 +247,7 @@ impl Context {
/// Create iroh endpoint and gossip.
async fn init_peer_channels(&self) -> Result<Iroh> {
info!(self, "Initializing peer channels.");
let secret_key = SecretKey::generate(rand_old::rngs::OsRng);
let secret_key = SecretKey::generate();
let public_key = secret_key.public();
let relay_mode = if let Some(relay_url) = self
@@ -252,8 +264,9 @@ impl Context {
RelayMode::Default
};
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
let address_lookup = MemoryLookup::new();
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.address_lookup(address_lookup.clone())
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
@@ -267,8 +280,7 @@ impl Context {
let gossip = Gossip::builder()
.max_message_size(128 * 1024)
.spawn(endpoint.clone())
.await?;
.spawn(endpoint.clone());
let router = iroh::protocol::Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
@@ -276,6 +288,7 @@ impl Context {
Ok(Iroh {
router,
address_lookup,
gossip,
sequence_numbers: Mutex::new(HashMap::new()),
iroh_channels: RwLock::new(HashMap::new()),
@@ -322,11 +335,15 @@ impl Context {
}
}
pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
pub(crate) async fn maybe_add_gossip_peer(
&self,
topic: TopicId,
peer: EndpointAddr,
) -> Result<()> {
if let Some(iroh) = &*self.iroh.read().await {
info!(
self,
"Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
"Adding (maybe existing) peer with id {} to {topic}.", peer.id
);
iroh.maybe_add_gossip_peer(topic, peer).await?;
}
@@ -334,12 +351,12 @@ impl Context {
}
}
/// Cache a peers [NodeId] for one topic.
/// Cache a peers [EndpointId] for one topic.
pub(crate) async fn iroh_add_peer_for_topic(
ctx: &Context,
msg_id: MsgId,
topic: TopicId,
peer: NodeId,
peer: EndpointId,
relay_server: Option<&str>,
) -> Result<()> {
ctx.sql
@@ -365,11 +382,11 @@ pub async fn add_gossip_peer_from_header(
}
let node_addr =
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
serde_json::from_str::<EndpointAddr>(node_addr).context("Failed to parse node address")?;
info!(
context,
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.id
);
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
@@ -384,8 +401,8 @@ pub async fn add_gossip_peer_from_header(
return Ok(());
};
let node_id = node_addr.node_id;
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
let node_id = node_addr.id;
let relay_server = node_addr.relay_urls().map(|relay| relay.as_str()).next();
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
context.maybe_add_gossip_peer(topic, node_addr).await?;
@@ -403,8 +420,8 @@ pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: Topic
Ok(())
}
/// Get a list of [NodeAddr]s for one webxdc.
async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeAddr>> {
/// Get a list of [EndpointAddr]s for one webxdc.
async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<EndpointAddr>> {
ctx.sql
.query_map(
"SELECT public_key, relay_server FROM iroh_gossip_peers WHERE msg_id = ? AND public_key != ?",
@@ -417,11 +434,11 @@ async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeA
|g| {
g.map(|data| {
let (key, server) = data?;
let server = server.map(|data| Ok::<_, url::ParseError>(RelayUrl::from(Url::parse(&data)?))).transpose()?;
let id = NodeId::from_bytes(&key.try_into()
let server: Option<TransportAddr> = server.map(|data| Ok::<_, url::ParseError>(TransportAddr::Relay(RelayUrl::from(Url::parse(&data)?)))).transpose()?;
let id = EndpointId::from_bytes(&key.try_into()
.map_err(|_| anyhow!("Can't convert sql data to [u8; 32]"))?)?;
Ok::<_, anyhow::Error>(NodeAddr::from_parts(
id, server, vec![]
Ok::<_, anyhow::Error>(EndpointAddr::from_parts(
id, server
))
})
.collect::<std::result::Result<Vec<_>, _>>()
@@ -536,45 +553,39 @@ pub(crate) fn iroh_topic_from_str(topic: &str) -> Result<TopicId> {
#[expect(clippy::arithmetic_side_effects)]
async fn subscribe_loop(
context: &Context,
mut stream: iroh_gossip::net::GossipReceiver,
mut stream: GossipReceiver,
topic: TopicId,
msg_id: MsgId,
join_tx: oneshot::Sender<()>,
) -> Result<()> {
let mut join_tx = Some(join_tx);
stream.joined().await?;
// Try to notify that at least one peer joined,
// but ignore the error if receiver is dropped and nobody listens.
join_tx.send(()).ok();
for node in stream.neighbors() {
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
while let Some(event) = stream.try_next().await? {
match event {
Event::Gossip(event) => match event {
GossipEvent::Joined(nodes) => {
if let Some(join_tx) = join_tx.take() {
// Try to notify that at least one peer joined,
// but ignore the error if receiver is dropped and nobody listens.
join_tx.send(()).ok();
}
for node in nodes {
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
}
GossipEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
GossipEvent::NeighborDown(_node) => {}
GossipEvent::Received(message) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: message
.content
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
},
Event::Lagged => {
GossipEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
GossipEvent::NeighborDown(_node) => {}
GossipEvent::Received(message) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: message
.content
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
GossipEvent::Lagged => {
warn!(context, "Gossip lost some messages");
}
};
@@ -639,7 +650,7 @@ mod tests {
.await
.unwrap()
.into_iter()
.map(|addr| addr.node_id)
.map(|addr| addr.id)
.collect::<Vec<_>>();
assert_eq!(
@@ -652,7 +663,7 @@ mod tests {
.get_node_addr()
.await
.unwrap()
.node_id
.id
]
);
@@ -715,7 +726,7 @@ mod tests {
.await
.unwrap()
.into_iter()
.map(|addr| addr.node_id)
.map(|addr| addr.id)
.collect::<Vec<_>>();
assert_eq!(
@@ -727,7 +738,7 @@ mod tests {
.get_node_addr()
.await
.unwrap()
.node_id
.id
]
);
@@ -805,7 +816,7 @@ mod tests {
.await
.unwrap()
.into_iter()
.map(|addr| addr.node_id)
.map(|addr| addr.id)
.collect::<Vec<_>>();
assert_eq!(
@@ -818,7 +829,7 @@ mod tests {
.get_node_addr()
.await
.unwrap()
.node_id
.id
]
);

View File

@@ -234,34 +234,6 @@ static P_BLUEWIN_CH: Provider = Provider {
oauth2_authorizer: None,
};
// buzon.uy.md: buzon.uy
static P_BUZON_UY: Provider = Provider {
id: "buzon.uy",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/buzon-uy",
server: &[
Server {
protocol: Imap,
socket: Starttls,
hostname: "mail.buzon.uy",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "mail.buzon.uy",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
};
// chello.at.md: chello.at
static P_CHELLO_AT: Provider = Provider {
id: "chello.at",
@@ -303,48 +275,6 @@ static P_COMCAST: Provider = Provider {
oauth2_authorizer: None,
};
// daleth.cafe.md: daleth.cafe
static P_DALETH_CAFE: Provider = Provider {
id: "daleth.cafe",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/daleth-cafe",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "daleth.cafe",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "daleth.cafe",
port: 465,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Starttls,
hostname: "daleth.cafe",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "daleth.cafe",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
};
// dismail.de.md: dismail.de
static P_DISMAIL_DE: Provider = Provider {
id: "dismail.de",
@@ -496,22 +426,6 @@ static P_FIREMAIL_DE: Provider = Provider {
oauth2_authorizer: None,
};
// five.chat.md: five.chat
static P_FIVE_CHAT: Provider = Provider {
id: "five.chat",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/five-chat",
server: &[],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::BccSelf,
value: "1",
}]),
oauth2_authorizer: None,
};
// freenet.de.md: freenet.de
static P_FREENET_DE: Provider = Provider {
id: "freenet.de",
@@ -629,16 +543,10 @@ static P_HERMES_RADIO: Provider = Provider {
strict_tls: false,
..ProviderOptions::new()
},
config_defaults: Some(&[
ConfigDefault {
key: Config::MdnsEnabled,
value: "0",
},
ConfigDefault {
key: Config::ShowEmails,
value: "2",
},
]),
config_defaults: Some(&[ConfigDefault {
key: Config::MdnsEnabled,
value: "0",
}]),
oauth2_authorizer: None,
};
@@ -919,90 +827,6 @@ static P_MAILO_COM: Provider = Provider {
oauth2_authorizer: None,
};
// mehl.cloud.md: mehl.cloud
static P_MEHL_CLOUD: Provider = Provider {
id: "mehl.cloud",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mehl-cloud",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "mehl.cloud",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mehl.cloud",
port: 443,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
hostname: "mehl.cloud",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mehl.cloud",
port: 465,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Starttls,
hostname: "mehl.cloud",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "mehl.cloud",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
};
// mehl.store.md: mehl.store, ende.in.net, l2i.top, szh.homes, sls.post.in, ente.quest, ente.cfd, nein.jetzt
static P_MEHL_STORE: Provider = Provider {
id: "mehl.store",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "This account provides 3GB storage for eMails and the possibility to access a NEXTCLOUD-instance by using the email-credits!",
overview_page: "https://providers.delta.chat/mehl-store",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "mail.ende.in.net",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "mail.ende.in.net",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
};
// migadu.md: migadu.com
static P_MIGADU: Provider = Provider {
id: "migadu",
@@ -1250,8 +1074,8 @@ static P_OUVATON_COOP: Provider = Provider {
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
static P_POSTEO: Provider = Provider {
id: "posteo",
status: Status::Ok,
before_login_hint: "",
status: Status::Preparation,
before_login_hint: "You must create an app-specific password before you can log in.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/posteo",
server: &[
@@ -1562,51 +1386,6 @@ static P_T_ONLINE: Provider = Provider {
oauth2_authorizer: None,
};
// testrun.md: testrun.org
static P_TESTRUN: Provider = Provider {
id: "testrun",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/testrun",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "testrun.org",
port: 465,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Starttls,
hostname: "testrun.org",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "testrun.org",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::BccSelf,
value: "1",
}]),
oauth2_authorizer: None,
};
// tiscali.it.md: tiscali.it
static P_TISCALI_IT: Provider = Provider {
id: "tiscali.it",
@@ -2004,7 +1783,7 @@ static P_ZOHO: Provider = Provider {
oauth2_authorizer: None,
};
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 521] = [
("163.com", &P_163),
("aktivix.org", &P_AKTIVIX_ORG),
("aliyun.com", &P_ALIYUN),
@@ -2014,11 +1793,9 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
("delta.blinzeln.de", &P_BLINDZELN_ORG),
("delta.blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("chello.at", &P_CHELLO_AT),
("xfinity.com", &P_COMCAST),
("comcast.net", &P_COMCAST),
("daleth.cafe", &P_DALETH_CAFE),
("dismail.de", &P_DISMAIL_DE),
("disroot.org", &P_DISROOT),
("e.email", &P_E_EMAIL),
@@ -2145,7 +1922,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
("your-mail.com", &P_FASTMAIL),
("firemail.at", &P_FIREMAIL_DE),
("firemail.de", &P_FIREMAIL_DE),
("five.chat", &P_FIVE_CHAT),
("freenet.de", &P_FREENET_DE),
("gmail.com", &P_GMAIL),
("googlemail.com", &P_GMAIL),
@@ -2368,15 +2144,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
("mailbox.org", &P_MAILBOX_ORG),
("secure.mailbox.org", &P_MAILBOX_ORG),
("mailo.com", &P_MAILO_COM),
("mehl.cloud", &P_MEHL_CLOUD),
("mehl.store", &P_MEHL_STORE),
("ende.in.net", &P_MEHL_STORE),
("l2i.top", &P_MEHL_STORE),
("szh.homes", &P_MEHL_STORE),
("sls.post.in", &P_MEHL_STORE),
("ente.quest", &P_MEHL_STORE),
("ente.cfd", &P_MEHL_STORE),
("nein.jetzt", &P_MEHL_STORE),
("migadu.com", &P_MIGADU),
("nauta.cu", &P_NAUTA_CU),
("naver.com", &P_NAVER),
@@ -2469,7 +2236,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 534] = [
("systemli.org", &P_SYSTEMLI_ORG),
("t-online.de", &P_T_ONLINE),
("magenta.de", &P_T_ONLINE),
("testrun.org", &P_TESTRUN),
("tiscali.it", &P_TISCALI_IT),
("tutanota.com", &P_TUTANOTA),
("tutanota.de", &P_TUTANOTA),
@@ -2552,10 +2318,8 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
("autistici.org", &P_AUTISTICI_ORG),
("blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("chello.at", &P_CHELLO_AT),
("comcast", &P_COMCAST),
("daleth.cafe", &P_DALETH_CAFE),
("dismail.de", &P_DISMAIL_DE),
("disroot", &P_DISROOT),
("e.email", &P_E_EMAIL),
@@ -2563,7 +2327,6 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
("example.com", &P_EXAMPLE_COM),
("fastmail", &P_FASTMAIL),
("firemail.de", &P_FIREMAIL_DE),
("five.chat", &P_FIVE_CHAT),
("freenet.de", &P_FREENET_DE),
("gmail", &P_GMAIL),
("gmx.net", &P_GMX_NET),
@@ -2581,8 +2344,6 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
("mail2tor", &P_MAIL2TOR),
("mailbox.org", &P_MAILBOX_ORG),
("mailo.com", &P_MAILO_COM),
("mehl.cloud", &P_MEHL_CLOUD),
("mehl.store", &P_MEHL_STORE),
("migadu", &P_MIGADU),
("nauta.cu", &P_NAUTA_CU),
("naver", &P_NAVER),
@@ -2602,7 +2363,6 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
("t-online", &P_T_ONLINE),
("testrun", &P_TESTRUN),
("tiscali.it", &P_TISCALI_IT),
("tutanota", &P_TUTANOTA),
("ukr.net", &P_UKR_NET),
@@ -2622,4 +2382,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
});
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 1, 28).unwrap());
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 4, 21).unwrap());

View File

@@ -146,7 +146,7 @@ pub enum Qr {
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Iroh node address.
node_addr: iroh::NodeAddr,
node_addr: iroh::EndpointAddr,
/// Authentication token.
auth_token: String,
@@ -645,7 +645,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
}
} else {
Ok(Qr::FprWithoutAddr {
fingerprint: fingerprint.to_string(),
fingerprint: fingerprint.human_readable(),
})
}
}
@@ -781,7 +781,7 @@ fn decode_backup2(qr: &str) -> Result<Qr> {
.split_once('&')
.context("Backup QR code has no separator")?;
let auth_token = auth_token.to_string();
let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
let node_addr = serde_json::from_str::<iroh::EndpointAddr>(node_addr)
.context("Invalid node addr in backup QR code")?;
Ok(Qr::Backup2 {

View File

@@ -388,7 +388,7 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
bob,
&format!(
"OPENPGP4FPR:{}#a=alice@example.org",
alice_contact.fingerprint().unwrap()
alice_contact.fingerprint().unwrap().hex()
),
)
.await?;
@@ -709,7 +709,7 @@ async fn test_decode_dclogin_advanced_options() -> Result<()> {
assert_eq!(param.smtp.security, Socket::Plain);
// `sc` option is actually ignored and `ic` is used instead
// because `smtp_certificate_checks` is deprecated.
// because `smtp_certificate_checks` has been removed.
assert_eq!(param.certificate_checks, EnteredCertificateChecks::Strict);
Ok(())
@@ -955,25 +955,3 @@ async fn test_decode_socks5() -> Result<()> {
Ok(())
}
/// Ensure that `DCBACKUP2` QR code does not fail to deserialize
/// because iroh changes the format of `NodeAddr`
/// as happened between iroh 0.29 and iroh 0.30 before.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_backup() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?;
assert!(matches!(qr, Qr::Backup2 { .. }));
let qr = check_qr(&ctx, r#"DCBACKUP2:AIvFjRFBt_aMiisSZ8P33JqY&{"node_id":"buzkyd4x76w66qtanjk5fm6ikeuo4quletajowsl3a3p7l6j23pa","info":{"relay_url":null,"direct_addresses":["192.168.1.5:12345"]}}"#).await?;
assert!(matches!(qr, Qr::Backup2 { .. }));
let qr = check_qr(&ctx, r#"DCBACKUP9:from-the-future"#).await?;
assert!(matches!(qr, Qr::BackupTooNew { .. }));
let qr = check_qr(&ctx, r#"DCBACKUP99:far-from-the-future"#).await?;
assert!(matches!(qr, Qr::BackupTooNew { .. }));
Ok(())
}

View File

@@ -6,33 +6,17 @@ 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};
/// warn about a nearly full mailbox after this usage percentage is reached.
/// quota icon is "yellow".
/// quota icon in connectivity is "yellow".
pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
/// warning again after this usage percentage is reached,
/// quota icon is "red".
/// quota icon in connectivity 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 {
@@ -70,37 +54,6 @@ 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.
@@ -134,32 +87,6 @@ impl Context {
Err(anyhow!(stock_str::not_supported_by_provider(self)))
};
if let Ok(quota) = &quota {
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 {
@@ -179,29 +106,10 @@ 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!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE);
assert!(0 < QUOTA_WARN_THRESHOLD_PERCENTAGE);
assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
Ok(())

View File

@@ -36,10 +36,7 @@ pub struct Reaction {
reaction: String,
}
// We implement From<&str> instead of std::str::FromStr, because
// FromStr requires error type and reaction parsing never returns an
// error.
impl From<&str> for Reaction {
impl Reaction {
/// Convert a `&str` into a `Reaction`.
/// Everything after the first whitespace is ignored.
///
@@ -51,7 +48,7 @@ impl From<&str> for Reaction {
/// reactions is not different from other kinds of spam attacks
/// such as sending large numbers of large messages, and should be
/// dealt with the same way, e.g. by blocking the user.
fn from(reaction: &str) -> Self {
pub fn new(reaction: &str) -> Self {
let reaction: &str = reaction
.split_ascii_whitespace()
.next()
@@ -61,9 +58,7 @@ impl From<&str> for Reaction {
reaction: reaction.to_string(),
}
}
}
impl Reaction {
/// Returns true if reaction contains no emoji.
pub fn is_empty(&self) -> bool {
self.reaction.is_empty()
@@ -212,7 +207,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
let msg = Message::load_from_db(context, msg_id).await?;
let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into();
let reaction = Reaction::new(reaction);
let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
@@ -282,7 +277,7 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
|row| {
let contact_id: ContactId = row.get(0)?;
let reaction: String = row.get(1)?;
Ok((contact_id, Reaction::from(reaction.as_str())))
Ok((contact_id, Reaction::new(reaction.as_str())))
},
)
.await?;
@@ -361,32 +356,32 @@ mod tests {
#[test]
fn test_parse_reaction() {
// Check that basic set of emojis from RFC 9078 is supported.
assert_eq!(Reaction::from("👍").as_str(), "👍");
assert_eq!(Reaction::from("👎").as_str(), "👎");
assert_eq!(Reaction::from("😀").as_str(), "😀");
assert_eq!(Reaction::from("").as_str(), "");
assert_eq!(Reaction::from("😢").as_str(), "😢");
assert_eq!(Reaction::new("👍").as_str(), "👍");
assert_eq!(Reaction::new("👎").as_str(), "👎");
assert_eq!(Reaction::new("😀").as_str(), "😀");
assert_eq!(Reaction::new("").as_str(), "");
assert_eq!(Reaction::new("😢").as_str(), "😢");
// Empty string can be used to remove all reactions.
assert!(Reaction::from("").is_empty());
assert!(Reaction::new("").is_empty());
// Short strings can be used as emojis, could be used to add
// support for custom emojis via emoji shortcodes.
assert_eq!(Reaction::from(":deltacat:").as_str(), ":deltacat:");
assert_eq!(Reaction::new(":deltacat:").as_str(), ":deltacat:");
// Check that long strings are not valid emojis.
assert!(
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
Reaction::new(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
);
// Multiple reactions separated by spaces or tabs are not supported.
assert_eq!(Reaction::from("👍 ❤").as_str(), "👍");
assert_eq!(Reaction::from("👍\t").as_str(), "👍");
assert_eq!(Reaction::new("👍 ❤").as_str(), "👍");
assert_eq!(Reaction::new("👍\t").as_str(), "👍");
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), "👍");
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), "👍");
assert_eq!(Reaction::new("👍\t:foo: ❤").as_str(), "👍");
assert_eq!(Reaction::new("👍\t:foo: ❤").as_str(), "👍");
assert_eq!(Reaction::from("👍 👍").as_str(), "👍");
assert_eq!(Reaction::new("👍 👍").as_str(), "👍");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -582,7 +577,7 @@ Content-Disposition: reaction\n\
assert_eq!(chat_id, expected_chat_id);
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
assert_eq!(reaction, Reaction::from(expected_reaction));
assert_eq!(reaction, Reaction::new(expected_reaction));
}
_ => panic!("Unexpected event {event:?}."),
}

View File

@@ -3,6 +3,7 @@
use std::cmp;
use std::collections::{BTreeMap, BTreeSet};
use std::iter;
use std::str::FromStr as _;
use std::sync::LazyLock;
use anyhow::{Context as _, Result, ensure};
@@ -11,14 +12,13 @@ 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, ShowEmails};
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX};
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;
@@ -525,52 +525,15 @@ pub(crate) async fn receive_imf_inner(
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
);
// 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);
// These checks must be done before processing of SecureJoin and other special messages.
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.
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.");
} 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."
);
if mime_parser.incoming {
return Ok(None);
}
@@ -589,7 +552,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);
@@ -639,8 +602,7 @@ pub(crate) async fn receive_imf_inner(
mime_parser.get_header(HeaderDef::References),
mime_parser.get_header(HeaderDef::InReplyTo),
)
.await?
.filter(|p| Some(p.id) != replace_msg_id);
.await?;
let mut chat_assignment =
decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
@@ -720,20 +682,8 @@ 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)
};
@@ -768,7 +718,6 @@ pub(crate) async fn receive_imf_inner(
rfc724_mid_orig,
from_id,
seen,
replace_msg_id,
prevent_rename,
chat_id,
chat_id_blocked,
@@ -819,12 +768,50 @@ pub(crate) async fn receive_imf_inner(
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
context
.execute_sync_items(
sync_items,
mime_parser.timestamp_sent,
&mime_parser.from.addr,
)
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
.await;
// Receiving encrypted message from self updates primary transport.
let from_addr = &mime_parser.from.addr;
let transport_changed = context
.sql
.transaction(|transaction| {
let transport_exists = transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(from_addr,),
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)?;
let transport_changed = if transport_exists {
transaction.execute(
"
UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
",
(from_addr,),
)? > 0
} else {
warn!(
context,
"Received sync message from unknown address {from_addr:?}."
);
false
};
Ok(transport_changed)
})
.await?;
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);
}
} else {
warn!(context, "Sync items are not encrypted.");
}
@@ -1025,11 +1012,6 @@ 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
@@ -1216,20 +1198,6 @@ 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()
@@ -1770,7 +1738,6 @@ 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,
@@ -1862,7 +1829,7 @@ async fn add_parts(
// Extract ephemeral timer from the message
let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
{
match value.parse::<EphemeralTimer>() {
match EphemeralTimer::from_str(value) {
Ok(timer) => timer,
Err(err) => {
warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
@@ -2140,7 +2107,7 @@ async fn add_parts(
chat_id,
from_id,
sort_timestamp,
Reaction::from(reaction_str.as_str()),
Reaction::new(reaction_str.as_str()),
is_incoming_fresh,
)
.await?;
@@ -2151,22 +2118,6 @@ 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 {
@@ -2209,10 +2160,9 @@ 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,
@@ -2222,34 +2172,29 @@ INSERT INTO msgs
ephemeral_timestamp, download_state, hop_info
)
VALUES (
?,
?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?, 1,
?, ?, ?, ?,
?, ?, ?, ?
)
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 {
)",
)?;
let params = params![
if let PreMessageMode::Pre {
post_msg_rfc724_mid,
..
} = &mime_parser.pre_message
{
post_msg_rfc724_mid
} else { rfc724_mid_orig },
if let PreMessageMode::Pre {..} = &mime_parser.pre_message {
} else {
rfc724_mid_orig
} else { "" },
},
if let PreMessageMode::Pre { .. } = &mime_parser.pre_message {
rfc724_mid_orig
} else {
""
},
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
if trash { ContactId::UNDEFINED } else { to_id },
@@ -2258,13 +2203,27 @@ RETURNING id
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()
@@ -2281,33 +2240,28 @@ RETURNING id
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 },
],
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
}
)?;
];
let row_id = MsgId::new(stmt.insert(params)?.try_into()?);
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);
}
@@ -2332,14 +2286,6 @@ RETURNING id
.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,
@@ -2443,6 +2389,7 @@ 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 {
@@ -2457,6 +2404,8 @@ 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?;

View File

@@ -16,7 +16,7 @@ use crate::imex::{ImexMode, imex};
use crate::key;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
};
use crate::tools::{SystemTime, time};
@@ -78,9 +78,8 @@ static GRP_MAIL: &[u8] =
hello\n";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_adhoc_group_show_chats_only() {
async fn test_adhoc_group_is_shown() {
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);
@@ -95,66 +94,12 @@ async fn test_adhoc_group_show_chats_only() {
receive_imf(&t, GRP_MAIL, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
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);
assert_eq!(chats.len(), 2);
}
#[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();
@@ -190,7 +135,6 @@ 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
@@ -816,10 +760,6 @@ 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")

View File

@@ -250,16 +250,6 @@ 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 {
@@ -358,7 +348,6 @@ async fn inbox_loop(
let ImapConnectionHandlers {
mut connection,
stop_token,
clear_storage_request_receiver,
} = inbox_handlers;
let transport_id = connection.transport_id();
@@ -397,14 +386,7 @@ async fn inbox_loop(
}
};
match inbox_fetch_idle(
&ctx,
&mut connection,
session,
&clear_storage_request_receiver,
)
.await
{
match inbox_fetch_idle(&ctx, &mut connection, session).await {
Err(err) => warn!(
ctx,
"Transport {transport_id}: Failed inbox fetch_idle: {err:#}."
@@ -425,29 +407,11 @@ async fn inbox_loop(
.await;
}
async fn inbox_fetch_idle(
ctx: &Context,
imap: &mut Imap,
mut session: Session,
clear_storage_request_receiver: &Receiver<()>,
) -> Result<Session> {
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> 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.
//
// Always update if we just cleared IMAP storage.
if (ctx.quota_needs_update(session.transport_id(), 60).await || should_clear_imap_storage)
if ctx.quota_needs_update(session.transport_id(), 60).await
&& let Err(err) = ctx.update_recent_quota(&mut session, &imap.folder).await
{
warn!(
@@ -631,7 +595,7 @@ async fn smtp_loop(
info!(ctx, "SMTP fake idle started.");
match &connection.last_send_error {
None => connection.connectivity.set_idle(&ctx),
Some(err) => connection.connectivity.set_err(&ctx, err),
Some(err) => connection.connectivity.set_err(&ctx, err.clone()),
}
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that
@@ -773,12 +737,6 @@ 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();
}
@@ -912,13 +870,6 @@ 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 {
@@ -930,13 +881,11 @@ 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 {
@@ -945,10 +894,7 @@ impl ImapConnectionState {
connectivity: handlers.connection.connectivity.clone(),
};
let conn = ImapConnectionState {
state,
clear_storage_request_sender,
};
let conn = ImapConnectionState { state };
Ok((conn, handlers))
}
@@ -962,19 +908,10 @@ 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<()>,
}

View File

@@ -157,8 +157,8 @@ impl ConnectivityStore {
context.emit_event(EventType::ConnectivityChanged);
}
pub(crate) fn set_err(&self, context: &Context, e: impl ToString) {
self.set(context, DetailedConnectivity::Error(e.to_string()));
pub(crate) fn set_err(&self, context: &Context, e: String) {
self.set(context, DetailedConnectivity::Error(e));
}
pub(crate) fn set_connecting(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connecting);

View File

@@ -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 = get_self_fingerprint(context).await?.hex();
let fingerprint = self_fingerprint(context).await?;
let self_addr = context.get_primary_self_addr().await?;
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
@@ -861,7 +861,8 @@ fn encrypted_and_signed(
} else {
warn!(
context,
"Message does not match expected fingerprint {expected_fingerprint}.",
"Message does not match expected fingerprint {}.",
expected_fingerprint.human_readable()
);
false
}

View File

@@ -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::delete_orphaned_poi_locations;
use crate::location;
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.
delete_orphaned_poi_locations(context)
location::delete_orphaned_poi(context)
.await
.context("Failed to delete orphaned POI locations")
.log_err(context)

View File

@@ -12,7 +12,6 @@ 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;
@@ -975,8 +974,7 @@ 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", ShowEmails::All as i32)
.await?;
sql.set_raw_config_int("show_emails", 2).await?;
}
sql.set_db_version(50).await?;
}
@@ -1457,8 +1455,7 @@ 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", ShowEmails::Off as i32)
.await?;
sql.set_raw_config_int("show_emails", 0).await?;
}
sql.set_db_version(98).await?;
}
@@ -1919,7 +1916,7 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
inc_and_check(&mut migration_version, 131)?;
if dbversion < migration_version {
let entered_param = EnteredLoginParam::load(context).await?;
let entered_param = EnteredLoginParam::load_legacy(context).await?;
let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
sql.execute_migration_transaction(

View File

@@ -153,15 +153,6 @@ 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,
@@ -1100,13 +1091,6 @@ 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)

View File

@@ -102,16 +102,6 @@ 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;

View File

@@ -307,12 +307,7 @@ impl Context {
/// If an error is returned, the caller shall not try over because some sync items could be
/// already executed. Sync items are considered independent and executed in the given order but
/// regardless of whether executing of the previous items succeeded.
pub(crate) async fn execute_sync_items(
&self,
items: &SyncItems,
timestamp_sent: i64,
from: &str,
) {
pub(crate) async fn execute_sync_items(&self, items: &SyncItems, timestamp_sent: i64) {
info!(self, "executing {} sync item(s)", items.items.len());
for item in &items.items {
// Limit the timestamp to ensure it is not in the future.
@@ -332,7 +327,7 @@ impl Context {
SyncData::Transports {
transports,
removed_transports,
} => sync_transports(self, from, transports, removed_transports).await,
} => sync_transports(self, transports, removed_transports).await,
},
SyncDataOrUnknown::Unknown(data) => {
warn!(self, "Ignored unknown sync item: {data}.");
@@ -641,12 +636,7 @@ mod tests {
.to_string(),
)
?;
t.execute_sync_items(
&sync_items,
timestamp_sent,
&t.get_config(Config::Addr).await?.unwrap(),
)
.await;
t.execute_sync_items(&sync_items, timestamp_sent).await;
assert!(
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)

View File

@@ -275,16 +275,17 @@ impl TestContextManager {
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
loop {
for _ in 0..2 {
let mut something_sent = false;
if let Some(sent) = joiner.pop_sent_msg_opt(Duration::ZERO).await {
let rev_order = false;
if let Some(sent) = joiner.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
for inviter in inviters {
inviter.recv_msg_opt(&sent).await;
}
something_sent = true;
}
for inviter in inviters {
if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
if let Some(sent) = inviter.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
joiner.recv_msg_opt(&sent).await;
something_sent = true;
}
@@ -623,25 +624,35 @@ impl TestContext {
}
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
let rev_order = true;
self.pop_sent_msg_ex(rev_order, timeout).await
}
pub async fn pop_sent_msg_ex(
&self,
rev_order: bool,
timeout: Duration,
) -> Option<SentMessage<'_>> {
let start = Instant::now();
let mut query = "
SELECT id, msg_id, mime, recipients
FROM smtp
ORDER BY id"
.to_string();
if rev_order {
query += " DESC";
}
let (rowid, msg_id, payload, recipients) = loop {
let row = self
.ctx
.sql
.query_row_optional(
r#"
SELECT id, msg_id, mime, recipients
FROM smtp
ORDER BY id DESC"#,
(),
|row| {
let rowid: i64 = row.get(0)?;
let msg_id: MsgId = row.get(1)?;
let mime: String = row.get(2)?;
let recipients: String = row.get(3)?;
Ok((rowid, msg_id, mime, recipients))
},
)
.query_row_optional(&query, (), |row| {
let rowid: i64 = row.get(0)?;
let msg_id: MsgId = row.get(1)?;
let mime: String = row.get(2)?;
let recipients: String = row.get(3)?;
Ok((rowid, msg_id, mime, recipients))
})
.await
.expect("query_row_optional failed");
if let Some(row) = row {
@@ -782,8 +793,7 @@ impl TestContext {
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 (you may have \
to call set_config(Config::ShowEmails, Some(\"2\")).await)"
"received message is not shown in chat, maybe it's hidden"
);
msg
@@ -823,17 +833,24 @@ impl TestContext {
assert_eq!(received.chat_id, DC_CHAT_ID_TRASH);
}
/// Gets the most recent message ID of a chat.
///
/// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg_id_in(&self, chat_id: ChatId) -> MsgId {
let msgs = chat::get_chat_msgs(&self.ctx, chat_id).await.unwrap();
if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
*msg_id
} else {
panic!("Wrong item type");
}
}
/// Gets the most recent message of a chat.
///
/// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
let msgs = chat::get_chat_msgs(&self.ctx, chat_id).await.unwrap();
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
let msg_id = self.get_last_msg_id_in(chat_id).await;
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
}
/// Gets the most recent message over all chats.
@@ -1087,7 +1104,6 @@ impl TestContext {
self,
chat_id,
MessageListOptions {
info_only: false,
add_daymarker: false,
},
)

View File

@@ -5,13 +5,9 @@ use crate::download::DownloadState;
use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::TestContext;
// 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"
// 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.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_download_stub_message() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -53,9 +49,9 @@ async fn test_download_stub_message() -> Result<()> {
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Done);
assert_eq!(msg.download_state(), DownloadState::Available);
assert_eq!(msg.get_subject(), "foo");
assert_eq!(msg.get_text(), "100k text...");
assert!(msg.get_text().contains("[97.66 KiB message]"));
Ok(())
}

View File

@@ -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::{DcKey, load_self_public_key};
use crate::key::self_fingerprint;
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -152,11 +152,7 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
bob.sql
.execute(
"DELETE FROM public_keys WHERE fingerprint=?",
(&load_self_public_key(alice)
.await
.unwrap()
.dc_fingerprint()
.hex(),),
(&self_fingerprint(alice).await.unwrap(),),
)
.await?;
let chat = Chat::load_from_db(bob, chat_id).await?;

View File

@@ -44,9 +44,7 @@ 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
DKIM Results: Passed=true";
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
check_parse_receive_headers_integration(raw, expected).await;
let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml");
@@ -56,9 +54,7 @@ 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
DKIM Results: Passed=true";
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
check_parse_receive_headers_integration(raw, expected).await;
}

View File

@@ -21,7 +21,6 @@ use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::events::EventType;
use crate::log::warn;
use crate::login_param::EnteredLoginParam;
use crate::net::load_connection_timestamp;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
@@ -775,7 +774,6 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
/// Process received data for transport synchronization.
pub(crate) async fn sync_transports(
context: &Context,
from_addr: &str,
transports: &[TransportData],
removed_transports: &[RemovedTransportData],
) -> Result<()> {
@@ -790,7 +788,7 @@ pub(crate) async fn sync_transports(
modified |= save_transport(context, entered, configured, *timestamp, *is_published).await?;
}
let primary_changed = context
context
.sql
.transaction(|transaction| {
for RemovedTransportData { addr, timestamp } in removed_transports {
@@ -808,43 +806,13 @@ pub(crate) async fn sync_transports(
(addr, timestamp),
)?;
}
let transport_exists = transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(from_addr,),
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)?;
let primary_changed = if transport_exists {
transaction.execute(
"
UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
",
(from_addr,),
)? > 0
} else {
warn!(
context,
"Received sync message from unknown address {from_addr:?}."
);
false
};
Ok(primary_changed)
Ok(())
})
.await?;
if modified {
tokio::task::spawn(restart_io_if_running_boxed(context.clone()));
}
if primary_changed {
info!(context, "Primary transport changed to {from_addr:?}.");
context.sql.uncache_raw_config("configured_addr").await;
}
if modified || primary_changed {
context.self_public_key.lock().await.take();
tokio::task::spawn(restart_io_if_running_boxed(context.clone()));
context.emit_event(EventType::TransportsModified);
}
Ok(())

View File

@@ -118,8 +118,6 @@ async fn test_posteo_alias() -> Result<()> {
t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredServerFlags, Some("0"))
.await?;
@@ -207,8 +205,6 @@ async fn test_empty_server_list_legacy() -> Result<()> {
.await?; // Strict
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredServerFlags, Some("0"))
.await?;
@@ -426,13 +422,6 @@ 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]),

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,7 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,8 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,8 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,3 +0,0 @@
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

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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>

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,7 +0,0 @@
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>

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,6 +0,0 @@
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

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,7 +0,0 @@
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>

View File

@@ -1,5 +0,0 @@
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>

View File

@@ -1,5 +0,0 @@
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>

Some files were not shown because too many files have changed in this diff Show More