Compare commits

...

67 Commits

Author SHA1 Message Date
B. Petersen
78d304443a chore(release): prepare for 1.134.0 2024-01-31 14:52:40 +00:00
link2xt
d6c24eb9f6 Make Accounts::background_fetch() not return Result 2024-01-31 14:04:03 +01:00
link2xt
f7fd1ef2bf Emit DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE even on timeout
Otherwise if there is a timeout,
UI will wait for DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE forever.
2024-01-31 14:04:03 +01:00
link2xt
af7bf5bd2b s/forgeting/forgetting/ 2024-01-31 14:04:03 +01:00
link2xt
ea666f1098 Hide background_fetch_without_timeout from public API 2024-01-31 14:04:03 +01:00
Simon Laux
5bb80f94c7 Apply suggestions from code review
Co-authored-by: bjoern <r10s@b44t.com>
2024-01-31 14:04:03 +01:00
link2xt
2f29c56a36 fix: do not log error if watched folder is not configured
This may happen if Sent folder does not exist
but configuration option to watch it is enabled.
2024-01-31 14:04:03 +01:00
Simon Laux
de86b8a96e rename event also in core 2024-01-31 14:04:03 +01:00
Simon Laux
060c9c8aa1 cargo fmt 2024-01-31 14:04:03 +01:00
Simon Laux
727428a965 rename event and mention event in method documentation 2024-01-31 14:04:03 +01:00
Simon Laux
df455bbcf5 BackgroundFetchCompletedForAllAccounts event 2024-01-31 14:04:03 +01:00
Simon Laux
946eea4c9e add rate limit for quota check in background fetch (12h for now) 2024-01-31 14:04:03 +01:00
Simon Laux
5cbc87369e rename cffi function 2024-01-31 14:04:03 +01:00
Simon Laux
5cdd5e0564 Apply suggestions from code review
Co-authored-by: link2xt <link2xt@testrun.org>
2024-01-31 14:04:03 +01:00
Simon Laux
f493d6bb40 don't hold write lock in cffi (this blocked events) 2024-01-31 14:04:03 +01:00
Simon Laux
8e073b9c3e log time that the function took 2024-01-31 14:04:03 +01:00
Simon Laux
ea2a692d18 fix iOS build issue 2024-01-31 14:04:03 +01:00
Simon Laux
1b7c5be9c5 Test server has no sentbox folder 2024-01-31 14:04:03 +01:00
Simon Laux
f7903df805 api: cffi: add dc_accounts_background_fetch_with_timeout 2024-01-31 14:04:03 +01:00
Simon Laux
d2c61dc90e api: jsonrpc: add background_fetch_for_all_accounts 2024-01-31 14:04:03 +01:00
Simon Laux
7b68098785 feat: add background fetch method 2024-01-31 14:04:03 +01:00
Simon Laux
48f2ea717e refactor: move convert folder meaning logic in own method
also unify the error handling for the cases where it can go wrong.
2024-01-31 14:04:03 +01:00
link2xt
cb3f03fd39 feat: add support for IMAP METADATA 2024-01-31 04:16:04 +00:00
iequidoo
06f1fe18d6 fix: Delete resent messages on receiver side (#5155)
If a Delta Chat message has the Message-ID already existing in the db, but a greater "Date", it's a
resent message that can be deleted. Messages having the same "Date" mustn't be deleted because they
can be already seen messages moved back to INBOX. Also don't delete messages having lesser "Date" to
avoid deleting both messages in a multi-device setting.
2024-01-30 22:01:45 -03:00
iequidoo
1dbf924c6a feat: chat::resend_msgs: Guarantee strictly increasing time in the Date header
Use `create_smeared_timestamp()` for this. This allows to dedup messages on the receiver -- if it
sees the same Message-ID, but a different timestamp, then it's a resent message that can be deleted.
2024-01-30 22:01:45 -03:00
link2xt
3f6814f421 chore: remove unnecessary exception from deny.toml 2024-01-30 20:27:07 +00:00
link2xt
782828ac4f chore(deps): update imap-proto 2024-01-30 20:12:07 +00:00
iequidoo
bd3759d55e test: test_import_export_online_all: Send the message to the existing address to avoid errors (#5220) 2024-01-29 20:34:03 -03:00
iequidoo
672993e69e feat: qr::check_qr(): Accept i.delta.chat invite links (#5217)
Accepts invite links with `https://i.delta.chat[/]#FINGERPRINT&a=ADDR[&OPTIONAL_PARAMS]`
scheme. Only `i.delta.chat` domain is supported now not to intersect with the common HTTPS scheme.
2024-01-28 20:33:56 -03:00
iequidoo
987bdaf237 ci: Add/remove necessary newlines to fix Python lint 2024-01-26 14:46:46 -03:00
iequidoo
7cf382a3b8 fix: Treat only "Auto-Submitted: auto-generated" messages as bot-sent (#5213)
"Auto-Submitted: auto-replied" messages mustn't be considered as sent by either bots or non-bots,
e.g. MDNs have this header value and it's the same for bots and non-bots.
2024-01-26 13:03:19 -03:00
Simon Laux
19dce9ddfa api!: jsonrpc: device message api now requires Option<MessageData> instead of String for the message (#5211)
api: jsonrpc: device message api now setting empty device messages to
block adding a message for a specific label

required for https://github.com/deltachat/deltachat-desktop/pull/3639

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-01-25 11:26:11 +01:00
Simon Laux
0afc0dd65a fix: add tolerance for macOS and iOS changing # to %23
fixes #1969

Bug description:
macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too, see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
2024-01-24 23:58:49 +01:00
link2xt
73d612a07d feat: do not drop unknown report attachments
In particular TLSRPT reports
contain files that may be interesting for admins.
Currently Delta Chat drops the attachment
so message appears as a text message without actual payload.
2024-01-24 16:13:20 +00:00
link2xt
3b1529ef81 chore(release): prepare for 1.133.2 2024-01-24 01:57:45 +00:00
link2xt
15187c0adb fix: downgrade OpenSSL from 3.2.0 to 3.1.4 2024-01-24 01:48:23 +00:00
Sebastian Klähn
c5f31c3d03 fix: No new chats for MDNs with alias (#5196) (#5199)
close #5196
2024-01-22 16:51:37 +01:00
link2xt
2c17e78347 chore(release): prepare for 1.133.1 2024-01-21 04:18:13 +00:00
Sebastian Klähn
4ee646ce0b feat(api): Add is_bot to cffi and jsonrpc (#5197)
@adbenitez wants this feature on Deltalab to display a bot tag. 
Other UIs might also want to adopt this feature :)

---------
Co-authored-by: link2xt <link2xt@testrun.org>
2024-01-20 15:00:10 +00:00
B. Petersen
1f7b4a74fa add missing 'unencrypted message' defines
in #5161, it was forgotten to adapt deltachat.h;
moreover, this PR tweaks some other minor things
2024-01-20 15:00:23 +01:00
Sebastian Klähn
4bc90701cc feat: Add system message when provider does not allow unencrypted messages (#5161) (#5195)
close #5161

![Screenshot from 2024-01-19
19-56-09](https://github.com/deltachat/deltachat-core-rust/assets/39526136/27ecdd9b-1739-410b-bb26-80d5bdbbc39a)

---------

Co-authored-by: bjoern <r10s@b44t.com>
2024-01-20 11:47:23 +00:00
dependabot[bot]
490deb9347 chore(deps): bump h2 from 0.3.17 to 0.3.24 in /fuzz
Bumps [h2](https://github.com/hyperium/h2) from 0.3.17 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.17...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 23:58:52 +00:00
Sebastian Klähn
28d9484a13 fix(node): run tests with native ESM modules instead of esm (#5194)
close #5156

---------

Co-authored-by: Septias <scoreplayer2000@gmail.comclear>
2024-01-19 18:09:19 +01:00
link2xt
e67e684ee0 test: wait for joiner success in test_verified_group_[member_added]_recovery
If we wait for inviter success,
vg-member-added message may be still in flight
and reach ac2 after device resetup.

Making ac2 wait for joining the group ensures that old
device receives vg-member-added message
and new device will not receive it and fail to decrypt.

Other instances of wait_for_securejoin_inviter_success()
in the same tests are also replaced for reliability.
2024-01-18 17:00:21 +00:00
link2xt
6cfe3e6a97 chore(deps): update h2 0.4.0 -> 0.4.2 2024-01-18 13:40:38 +00:00
Sebastian Klähn
99ac524905 chore(deps): update h2 from 0.3.22 -> 0.3.24 2024-01-18 14:21:58 +01:00
link2xt
2faf7fdb78 fix: BCC-to-self even if server deletion is set to "at once" 2024-01-18 10:20:01 +00:00
link2xt
6a8ea8a083 fix: set message download state to Failure on IMAP errors
Previously the message was removed from `download` table,
but message bubble was stuck in InProgress state.

Now download state is updated by the caller,
so it cannot be accidentally skipped.
2024-01-18 10:18:57 +00:00
link2xt
e0e56cd831 chore: update quoted_printable to 0.5
And update mailparse to 0.14.1 so there is no duplicate dependency.
2024-01-18 09:35:05 +00:00
missytake
bbc6febb72 test: no timeout in SetupPlugin 2024-01-17 14:20:29 +01:00
missytake
7f7f42d721 test: Ensure that member is added before yielding chat 2024-01-17 14:20:29 +01:00
iequidoo
589236c27b fix: chat::send_msg: Remove encryption-related params from already sent message
This allows to send existing messages (incoming and outgoing) taken from encrypted chats, to
unencrypted ones. `Param::ForcePlaintext` is removed as well -- if a message can be sent encrypted
this time, nothing bad with this.
2024-01-17 14:20:29 +01:00
iequidoo
c16c5e0802 test: Bring test_forward_encrypted_to_unencrypted into line with current API
Currently `Chat.send_msg()` modifies the source message and returns another message object
equivalent to the source one. That's how it works in the core and thus in Python bindings too.
2024-01-17 14:20:29 +01:00
missytake
36cab40ac1 test: add get_protected_chat to testplugin.py 2024-01-17 14:20:29 +01:00
missytake
4186d78305 test: add python test for message forwarding from encrypted to unencrypted chat 2024-01-17 14:20:29 +01:00
iequidoo
06cccb77f8 feat: Use Quoted-Printable for the text part (#3986)
This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable encoding and
thus breaking messages and signatures. It's unlikely that the reader uses a MUA not supporting
Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for encrypted messages.
2024-01-16 23:46:24 -03:00
link2xt
1895f4c556 chore(release): prepare for 1.133.0 2024-01-15 22:55:26 +00:00
link2xt
849a873e61 feat: only try to configure non-strict TLS checks if explicitly set
Trying non-strict TLS checks is not necessary
for most servers with proper TLS setup,
but doubles the time needed to fail configuration
when the server is not responding, e.g.
when all connection attempts time out.

There is also a risk of accidentally
configuring non-strict TLS checks in a rare case
that strict TLS check configuration spuriously failed,
e.g. on a bad network.

If the server has a known broken TLS setup,
it can still be added to the provider database
or configured with non-strict TLS check manually.
User can also configure another email provider,
such as chatmail servers, instead of using the server
with invalid TLS hostname.

This change does not affect exising setups.
2024-01-15 22:54:31 +00:00
link2xt
b5c0372c99 docs: restore "Constants" page in Doxygen >=1.9.8
deltachat.h uses `@defgroup` commands to create topics
for groups of constants. Prior to Doxygen 1.9.8
defining a group created a "module"
and all constants were visible from the modules.html page.
In Doxygen 1.9.8 "modules" were renamed into "topics"
as C++20 modules have taken their place,
so Delta Chat documentation does not have modules
in Doxygen sense anymore.

The change is to replace "modules.html" with "topics.html"
in the DoxygenLayout.xml.

See <https://www.doxygen.nl/manual/grouping.html> for
Doxygen documentation about groups and their relation to topics.
2024-01-14 12:17:54 +00:00
link2xt
1ba9b69849 chore: npm run build:core:constants 2024-01-13 22:51:24 +00:00
holger krekel
6345a4f5b3 fix link for securejoin 2024-01-13 12:50:16 +01:00
Sebastian Klähn
382fc75b1e Add more docs (#5174)
Add some docs to smtp functions
2024-01-12 11:14:05 +01:00
Sebastian Klähn
92fc9ea971 feat: Encrypt MDNs #5168 (#5175)
This PR stops MDNs from being forced to be sent unencrypted. 
If no encryption is possible (by `should_encrypt`), the fix #5152 still
applies.

close #5168
2024-01-12 10:54:54 +01:00
Sebastian Klähn
de7ac2a240 fix: emit events more reliable when starting and stopping io #5097 (#5101)
Send `EventType::ConnectivityChanged` when using the context methods
`start_io` and `stop_io`.

close #5097

---------

Co-authored-by: Septias <scoreplayer2000@gmail.comclear>
2024-01-12 09:45:34 +01:00
link2xt
7b0e5adaee chore(deps): update rustyline from 12 to 13 2024-01-12 02:45:53 +00:00
iequidoo
406b59501b chore: deltachat-jsonrpc/src/api/types/events.rs: Apply rustfmt 2024-01-11 21:53:26 -03:00
iequidoo
d5da2bed75 feat: Add ConfigSynced event
Add an event for a case if a multi-device synced config value changed. Maybe the app needs to
refresh smth on such an event. For uniformity it is emitted on the source device too. The value is
omitted, otherwise it would be logged which might not be good for privacy.
2024-01-11 21:53:26 -03:00
61 changed files with 1436 additions and 493 deletions

View File

@@ -1,5 +1,120 @@
# Changelog
## [1.134.0] - 2024-01-31
### API-Changes
- [**breaking**] JSON-RPC: device message api now requires `Option<MessageData>` instead of `String` for the message ([#5211](https://github.com/deltachat/deltachat-core-rust/pull/5211)).
- CFFI: add `dc_accounts_background_fetch` and event `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE`.
- JSON-RPC: add `accounts_background_fetch`.
### Features / Changes
- `Qr::check_qr()`: Accept i.delta.chat invite links ([#5217](https://github.com/deltachat/deltachat-core-rust/pull/5217)).
- Add support for IMAP METADATA, fetching `/shared/comment` and `/shared/admin` and displaying it in account info.
### Fixes
- Add tolerance for macOS and iOS changing `#` to `%23`.
- Do not drop unknown report attachments, such as TLS reports.
- Treat only "Auto-Submitted: auto-generated" messages as bot-sent ([#5213](https://github.com/deltachat/deltachat-core-rust/pull/5213)).
- `Chat::resend_msgs`: Guarantee strictly increasing time in the `Date` header.
- Delete resent messages on receiver side ([#5155](https://github.com/deltachat/deltachat-core-rust/pull/5155)).
- Fix iOS build issue.
### CI
- Add/remove necessary newlines to fix Python lint.
### Tests
- `test_import_export_online_all`: Send the message to the existing address to avoid errors ([#5220](https://github.com/deltachat/deltachat-core-rust/pull/5220)).
## [1.133.2] - 2024-01-24
### Fixes
- Downgrade OpenSSL from 3.2.0 to 3.1.4 ([#5206](https://github.com/deltachat/deltachat-core-rust/issues/5206))
- No new chats for MDNs with alias ([#5196](https://github.com/deltachat/deltachat-core-rust/issues/5196)) ([#5199](https://github.com/deltachat/deltachat-core-rust/pull/5199)).
## [1.133.1] - 2024-01-21
### API-Changes
- Add `is_bot` to cffi and jsonrpc ([#5197](https://github.com/deltachat/deltachat-core-rust/pull/5197)).
### Features / Changes
- Add system message when provider does not allow unencrypted messages ([#5195](https://github.com/deltachat/deltachat-core-rust/pull/5195)).
### Fixes
- `Chat::send_msg`: Remove encryption-related params from already sent message. This allows to send received encrypted `dc_msg_t` object to unencrypted chat, e.g. in a Python bot.
- Set message download state to Failure on IMAP errors. This avoids partially downloaded messages getting stuck in "Downloading..." state without actually being in a download queue.
- BCC-to-self even if server deletion is set to "at once". This is a workaround for SMTP servers which do not return response in time, BCC-self works as a confirmation that message was sent out successfully and does not need more retries.
- node: Run tests with native ESM modules instead of `esm` ([#5194](https://github.com/deltachat/deltachat-core-rust/pull/5194)).
- Use Quoted-Printable MIME encoding for the text part ([#3986](https://github.com/deltachat/deltachat-core-rust/pull/3986)).
### Tests
- python: Add `get_protected_chat` to testplugin.py.
## [1.133.0] - 2024-01-14
### Features / Changes
- Securejoin protocol implementation refinements
- Track forward and backward verification separately ([#5089](https://github.com/deltachat/deltachat-core-rust/pull/5089)) to avoid inconsistent states.
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
- deltachat-repl: Enable INFO logging by default and add timestamps.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
### Build system
- Use released version of iroh 0.4.2 for "setup second device" feature.
### CI
- Update to Rust 1.75.0.
- Downgrade `chai` from 4.4.0 to 4.3.10.
### Documentation
- Add a link <https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html> to autoconfig RFC draft.
- Update securejoin link in `standards.md` from <https://countermitm.readthedocs.io/> to <https://securejoin.readthedocs.io>.
- Restore "Constants" page in Doxygen >=1.9.8
### Fixes
- imap: Limit the rate of LOGIN attempts rather than connection attempts. This is to avoid having to wait for rate limiter right after switching from a bad or offline network to a working network while still guarding against reconnection loop.
- Do not ignore `peerstate.save_to_db()` errors.
- securejoin: Mark 1:1s as protected regardless of the Config::VerifiedOneOnOneChats.
- Delete received outgoing messages from SMTP queue ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
- imap: Fail fast on `LIST` errors to avoid busy loop when connection is lost.
- Split SMTP jobs already in `chat::create_send_msg_jobs()` ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
- Do not remove contents from unencrypted [Schleuder](https://schleuder.org/) mailing lists messages.
- Reset message error when scheduling resending ([#5119](https://github.com/deltachat/deltachat-core-rust/pull/5119)).
- Emit events more reliably when starting and stopping I/O ([#5101](https://github.com/deltachat/deltachat-core-rust/pull/5101)).
- Fix timestamp of chat protection info message for correct message ordering after restoring a backup ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
### Refactor
- sql: Recreate `config` table with UNIQUE constraint.
- sql: Recreate `keypairs` table to remove unused `addr` and `created` fields and move `is_default` flag to `config` table.
- Send `Secure-Join-Fingerprint` only in `*-request-with-auth`.
### Tests
- Test joining non-protected group.
- Test that read receipts don't degrade encryption.
- Test that changing default private key breaks backward verification.
- Test recovery from lost vc-contact-confirm.
- Use `wait_for_incoming_msg_event()` more.
## [1.132.1] - 2023-12-12
### Features / Changes
@@ -3357,3 +3472,7 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.131.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.8...v1.131.9
[1.132.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.9...v1.132.0
[1.132.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.0...v1.132.1
[1.133.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.1...v1.133.0
[1.133.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.0...v1.133.1
[1.133.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.1...v1.133.2
[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0

93
Cargo.lock generated
View File

@@ -224,9 +224,9 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d69fc1499878158750f644c4eb46aff55bb9d32d77e3dc4aecf8308d5c3ba6"
checksum = "98892ebee4c05fc66757e600a7466f0d9bfcde338f645d64add323789f26cb36"
dependencies = [
"async-channel 2.1.1",
"base64 0.21.5",
@@ -738,13 +738,11 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "clipboard-win"
version = "4.5.0"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc"
dependencies = [
"error-code",
"str-buf",
"winapi",
]
[[package]]
@@ -1091,7 +1089,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.132.1"
version = "1.134.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1110,7 +1108,7 @@ dependencies = [
"encoded-words",
"escaper",
"fast-socks5",
"fd-lock 4.0.2",
"fd-lock",
"format-flowed",
"futures",
"futures-lite",
@@ -1129,6 +1127,7 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"openssl-src",
"parking_lot",
"percent-encoding",
"pgp",
@@ -1138,6 +1137,7 @@ dependencies = [
"proptest",
"qrcodegen",
"quick-xml",
"quoted_printable",
"rand 0.8.5",
"ratelimit",
"regex",
@@ -1169,7 +1169,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.132.1"
version = "1.134.0"
dependencies = [
"anyhow",
"async-channel 2.1.1",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.132.1"
version = "1.134.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1208,7 +1208,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.132.1"
version = "1.134.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1233,7 +1233,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.132.1"
version = "1.134.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1777,13 +1777,9 @@ dependencies = [
[[package]]
name = "error-code"
version = "2.3.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
dependencies = [
"libc",
"str-buf",
]
checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc"
[[package]]
name = "escaper"
@@ -1862,17 +1858,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fd-lock"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "fd-lock"
version = "4.0.2"
@@ -2165,9 +2150,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.22"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
@@ -2184,9 +2169,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a"
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
dependencies = [
"bytes",
"fnv",
@@ -2434,7 +2419,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.22",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"httparse",
@@ -2457,7 +2442,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.0",
"h2 0.4.2",
"http 1.0.0",
"http-body 1.0.0",
"httparse",
@@ -2574,8 +2559,9 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.16.3"
source = "git+https://github.com/djc/tokio-imap.git?rev=01ff256a7e42a9f7d2732706f8b71a16ce93427e#01ff256a7e42a9f7d2732706f8b71a16ce93427e"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e70cd66882c8cb1c9802096ba75212822153c51478dc61621e1a22f6c92361"
dependencies = [
"nom",
]
@@ -2827,9 +2813,9 @@ dependencies = [
[[package]]
name = "mailparse"
version = "0.14.0"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
checksum = "2d096594926cab442e054e047eb8c1402f7d5b2272573b97ba68aa40629f9757"
dependencies = [
"charset",
"data-encoding",
@@ -3005,11 +2991,11 @@ dependencies = [
[[package]]
name = "nix"
version = "0.26.4"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.1",
"cfg-if",
"libc",
]
@@ -3205,9 +3191,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.2.1+3.2.0"
version = "300.1.6+3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3"
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
dependencies = [
"cc",
]
@@ -3761,9 +3747,9 @@ dependencies = [
[[package]]
name = "quoted_printable"
version = "0.4.8"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
[[package]]
name = "radix_trie"
@@ -3975,7 +3961,7 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.22",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
@@ -4225,21 +4211,20 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
version = "12.0.0"
version = "13.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9"
checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"clipboard-win",
"fd-lock 3.0.13",
"fd-lock",
"home",
"libc",
"log",
"memchr",
"nix",
"radix_trie",
"scopeguard",
"unicode-segmentation",
"unicode-width",
"utf8parse",
@@ -4690,12 +4675,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "str-buf"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
[[package]]
name = "strsim"
version = "0.10.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.132.1"
version = "1.134.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
@@ -30,9 +30,6 @@ opt-level = "z"
codegen-units = 1
strip = true
[patch.crates-io]
imap-proto = { git = "https://github.com/djc/tokio-imap.git", rev = "01ff256a7e42a9f7d2732706f8b71a16ce93427e" }
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
format-flowed = { path = "./format-flowed" }
@@ -40,7 +37,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "2.0.0"
async-imap = { version = "0.9.5", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
@@ -76,6 +73,7 @@ pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.31"
quoted_printable = "0.5"
rand = "0.8"
regex = "1.9"
reqwest = { version = "0.11.23", features = ["json"] }
@@ -101,6 +99,14 @@ toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
[dev-dependencies]
ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.132.1"
version = "1.134.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -9,7 +9,7 @@
<tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/>
</tab>
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>

View File

@@ -3151,6 +3151,23 @@ void dc_accounts_maybe_network (dc_accounts_t* accounts);
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
/**
* Perform a background fetch for all accounts in parallel with a timeout.
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
*
* dc_accounts_background_fetch() was created for the iOS Background fetch.
*
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
* even in case of timeout, unless the function fails and returns 0.
* Process all events until you get this one and you can safely return to the background
* without forgetting to create notifications caused by timing race conditions.
*
* @memberof dc_accounts_t
* @param timeout The timeout in seconds
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
*/
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
/**
* Create the event emitter that is used to receive events.
*
@@ -4397,6 +4414,9 @@ int dc_msg_is_info (const dc_msg_t* msg);
* Currently, the following types are defined:
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
@@ -4423,6 +4443,7 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
@@ -5068,6 +5089,15 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
*/
int dc_contact_is_verified (dc_contact_t* contact);
/**
* Returns whether contact is a bot.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0 if the contact is not a bot, 1 otherwise.
*/
int dc_contact_is_bot (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
@@ -6206,6 +6236,18 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_SELFAVATAR_CHANGED 2110
/**
* A multi-device synced config value changed. Maybe the app needs to refresh smth. For uniformity
* this is emitted on the source device too. The value isn't reported, otherwise it would be logged
* which might not be good for privacy. You can get the new value with
* `dc_get_config(context, data2)`.
*
* @param data1 0
* @param data2 (char*) Configuration key.
*/
#define DC_EVENT_CONFIG_SYNCED 2111
/**
* webxdc status update received.
* To get the received status update, use dc_get_webxdc_status_updates() with
@@ -6230,6 +6272,16 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
/**
* Tells that the Background fetch was completed (or timed out).
*
* This event acts as a marker, when you reach this event you can be sure
* that all events emitted during the background fetch were processed.
*
* This event is only emitted by the account manager
*/
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
/**
* @}
@@ -6998,6 +7050,8 @@ void dc_event_unref(dc_event_t* event);
/// "You added member %1$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the added member's name.
#define DC_STR_ADD_MEMBER_BY_YOU 128
/// "Member %1$s added by %2$s."
@@ -7219,6 +7273,21 @@ void dc_event_unref(dc_event_t* event);
/// Used as the first info messages in newly created groups.
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
/// "Member %1$s added."
///
/// Used as info messages.
///
/// `%1$s` will be replaced by the added member's name.
#define DC_STR_MESSAGE_ADD_MEMBER 173
/// "Your email provider %1$s requires end-to-end encryption which is not setup yet."
///
/// Used as info messages when a message cannot be sent because it cannot be encrypted.
///
/// `%1$s` will be replaced by the provider's domain.
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
/**
* @}
*/

View File

@@ -556,8 +556,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::SecurejoinJoinerProgress { .. } => 2061,
EventType::ConnectivityChanged => 2100,
EventType::SelfavatarChanged => 2110,
EventType::ConfigSynced { .. } => 2111,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
EventType::AccountsBackgroundFetchDone => 2200,
}
}
@@ -583,8 +585,10 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::Error(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_) => 0,
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -643,7 +647,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged => 0,
| EventType::SelfavatarChanged
| EventType::AccountsBackgroundFetchDone
| EventType::ConfigSynced { .. } => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
@@ -705,6 +711,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate { .. }
| EventType::WebxdcInstanceDeleted { .. }
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -722,6 +729,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::ConfigSynced { key } => {
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
}
}
}
@@ -4125,6 +4136,15 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_is_bot()");
return 0;
}
(*contact).contact.is_bot() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
if contact.is_null() {
@@ -4882,6 +4902,26 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_background_fetch(
accounts: *mut dc_accounts_t,
timeout_in_seconds: u64,
) -> libc::c_int {
if accounts.is_null() || timeout_in_seconds <= 2 {
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
return 0;
}
let accounts = &*accounts;
block_on(async move {
let accounts = accounts.read().await;
accounts
.background_fetch(Duration::from_secs(timeout_in_seconds))
.await;
});
1
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.132.1"
version = "1.134.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -231,6 +231,20 @@ impl CommandApi {
Ok(())
}
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
Ok(())
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
@@ -896,19 +910,35 @@ impl CommandApi {
.to_u32())
}
// for now only text messages, because we only used text messages in desktop thusfar
/// Add a message to the device-chat.
/// Device-messages usually contain update information
/// and some hints that are added during the program runs, multi-device etc.
/// The device-message may be defined by a label;
/// if a message with the same label was added or skipped before,
/// the message is not added again, even if the message was deleted in between.
/// If needed, the device-chat is created before.
///
/// Sends the `MsgsChanged` event on success.
///
/// Setting msg to None will prevent the device message with this label from being added in the future.
async fn add_device_message(
&self,
account_id: u32,
label: String,
text: String,
) -> Result<u32> {
msg: Option<MessageData>,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
if let Some(msg) = msg {
let mut message = msg.create_message(&ctx).await?;
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
if !message_id.is_unset() {
return Ok(Some(message_id.to_u32()));
}
} else {
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
}
Ok(None)
}
/// Mark all messages in a chat as _noticed_.
@@ -1808,38 +1838,7 @@ impl CommandApi {
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
viewtype.into()
} else if data.file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
message.set_text(data.text.unwrap_or_default());
if data.html.is_some() {
message.set_html(data.html);
}
if data.override_sender_name.is_some() {
message.set_override_sender_name(data.override_sender_name);
}
if let Some(file) = data.file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = data.location {
message.set_location(latitude, longitude);
}
if let Some(id) = data.quoted_message_id {
message
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
let mut message = data.create_message(&ctx).await?;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();

View File

@@ -45,6 +45,9 @@ pub struct ContactObject {
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
/// If the contact is a bot.
is_bot: bool,
}
impl ContactObject {
@@ -80,6 +83,7 @@ impl ContactObject {
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
is_bot: contact.is_bot(),
})
}
}

View File

@@ -28,55 +28,37 @@ pub enum EventType {
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info {
msg: String,
},
Info { msg: String },
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected {
msg: String,
},
SmtpConnected { msg: String },
/// Emitted when IMAP connection is established and login was successful.
ImapConnected {
msg: String,
},
ImapConnected { msg: String },
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent {
msg: String,
},
SmtpMessageSent { msg: String },
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted {
msg: String,
},
ImapMessageDeleted { msg: String },
/// Emitted when an IMAP message has been moved
ImapMessageMoved {
msg: String,
},
ImapMessageMoved { msg: String },
/// Emitted before going into IDLE on the Inbox folder.
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile {
file: String,
},
NewBlobFile { file: String },
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile {
file: String,
},
DeletedBlobFile { file: String },
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning {
msg: String,
},
Warning { msg: String },
/// The library-user should report an error to the end-user.
///
@@ -88,18 +70,14 @@ pub enum EventType {
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error {
msg: String,
},
Error { msg: String },
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// setChatName(), setChatProfileImage(),
/// addContactToChat(), removeContactFromChat(),
/// and messages sending functions.
ErrorSelfNotInGroup {
msg: String,
},
ErrorSelfNotInGroup { msg: String },
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
@@ -110,10 +88,7 @@ pub enum EventType {
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
#[serde(rename_all = "camelCase")]
MsgsChanged {
chat_id: u32,
msg_id: u32,
},
MsgsChanged { chat_id: u32, msg_id: u32 },
/// Reactions for the message changed.
#[serde(rename_all = "camelCase")]
@@ -128,10 +103,7 @@ pub enum EventType {
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
#[serde(rename_all = "camelCase")]
IncomingMsg {
chat_id: u32,
msg_id: u32,
},
IncomingMsg { chat_id: u32, msg_id: u32 },
/// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch,
@@ -139,47 +111,31 @@ pub enum EventType {
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch {
msg_ids: Vec<u32>,
},
IncomingMsgBunch { msg_ids: Vec<u32> },
/// Messages were seen or noticed.
/// chat id is always set.
#[serde(rename_all = "camelCase")]
MsgsNoticed {
chat_id: u32,
},
MsgsNoticed { chat_id: u32 },
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgDelivered {
chat_id: u32,
msg_id: u32,
},
MsgDelivered { chat_id: u32, msg_id: u32 },
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgFailed {
chat_id: u32,
msg_id: u32,
},
MsgFailed { chat_id: u32, msg_id: u32 },
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgRead {
chat_id: u32,
msg_id: u32,
},
MsgRead { chat_id: u32, msg_id: u32 },
/// A single message is deleted.
#[serde(rename_all = "camelCase")]
MsgDeleted {
chat_id: u32,
msg_id: u32,
},
MsgDeleted { chat_id: u32, msg_id: u32 },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
@@ -189,24 +145,17 @@ pub enum EventType {
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[serde(rename_all = "camelCase")]
ChatModified {
chat_id: u32,
},
ChatModified { chat_id: u32 },
/// Chat ephemeral timer changed.
#[serde(rename_all = "camelCase")]
ChatEphemeralTimerModified {
chat_id: u32,
timer: u32,
},
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[serde(rename_all = "camelCase")]
ContactsChanged {
contact_id: Option<u32>,
},
ContactsChanged { contact_id: Option<u32> },
/// Location of one or more contact has changed.
///
@@ -214,9 +163,7 @@ pub enum EventType {
/// If the locations of several contacts have been changed,
/// this parameter is set to `None`.
#[serde(rename_all = "camelCase")]
LocationChanged {
contact_id: Option<u32>,
},
LocationChanged { contact_id: Option<u32> },
/// Inform about the configuration progress started by configure().
ConfigureProgress {
@@ -234,9 +181,7 @@ pub enum EventType {
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexProgress {
progress: usize,
},
ImexProgress { progress: usize },
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
@@ -246,9 +191,7 @@ pub enum EventType {
///
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexFileWritten {
path: String,
},
ImexFileWritten { path: String },
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
@@ -263,10 +206,7 @@ pub enum EventType {
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
#[serde(rename_all = "camelCase")]
SecurejoinInviterProgress {
contact_id: u32,
progress: usize,
},
SecurejoinInviterProgress { contact_id: u32, progress: usize },
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
@@ -277,10 +217,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
#[serde(rename_all = "camelCase")]
SecurejoinJoinerProgress {
contact_id: u32,
progress: usize,
},
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
@@ -288,8 +225,17 @@ pub enum EventType {
/// getConnectivityHtml() for details.
ConnectivityChanged,
/// Deprecated by `ConfigSynced`.
SelfavatarChanged,
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
/// would be logged which might not be good for privacy.
ConfigSynced {
/// Configuration key.
key: String,
},
#[serde(rename_all = "camelCase")]
WebxdcStatusUpdate {
msg_id: u32,
@@ -298,9 +244,14 @@ pub enum EventType {
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted {
msg_id: u32,
},
WebxdcInstanceDeleted { msg_id: u32 },
/// Tells that the Background fetch was completed (or timed out).
/// This event acts as a marker, when you reach this event you can be sure
/// that all events emitted during the background fetch were processed.
///
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
}
impl From<CoreEventType> for EventType {
@@ -396,6 +347,9 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::ConnectivityChanged => ConnectivityChanged,
CoreEventType::SelfavatarChanged => SelfavatarChanged,
CoreEventType::ConfigSynced { key } => ConfigSynced {
key: key.to_string(),
},
CoreEventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
@@ -406,6 +360,7 @@ impl From<CoreEventType> for EventType {
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
}
}
}

View File

@@ -345,6 +345,7 @@ pub enum SystemMessageType {
SecurejoinMessage,
LocationStreamingEnabled,
LocationOnly,
InvalidUnencryptedMail,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
@@ -385,6 +386,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
}
}
}
@@ -546,6 +548,44 @@ pub struct MessageData {
pub quoted_message_id: Option<u32>,
}
impl MessageData {
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
viewtype.into()
} else if self.file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
message.set_text(self.text.unwrap_or_default());
if self.html.is_some() {
message.set_html(self.html);
}
if self.override_sender_name.is_some() {
message.set_override_sender_name(self.override_sender_name);
}
if let Some(file) = self.file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = self.location {
message.set_location(latitude, longitude);
}
if let Some(id) = self.quoted_message_id {
message
.set_quote(
context,
Some(
&Message::load_from_db(context, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
Ok(message)
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageReadReceipt {

View File

@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.132.1"
"version": "1.134.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.132.1"
version = "1.134.0"
license = "MPL-2.0"
edition = "2021"
@@ -12,7 +12,7 @@ dirs = "5"
log = "0.4.20"
pretty_env_logger = "0.5"
rusqlite = "0.30"
rustyline = "12"
rustyline = "13"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]

View File

@@ -299,8 +299,8 @@ impl Highlighter for DcHelper {
self.highlighter.highlight(line, pos)
}
fn highlight_char(&self, line: &str, pos: usize) -> bool {
self.highlighter.highlight_char(line, pos)
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
self.highlighter.highlight_char(line, pos, forced)
}
}

View File

@@ -1,4 +1,5 @@
"""Delta Chat JSON-RPC high-level API"""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat

View File

@@ -1,4 +1,5 @@
"""Event loop implementations offering high level event handling/hooking."""
import logging
from typing import (
TYPE_CHECKING,

View File

@@ -1,4 +1,5 @@
"""High-level classes for event processing and filtering."""
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union

View File

@@ -175,7 +175,7 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -183,7 +183,8 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -191,8 +192,7 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
@@ -239,7 +239,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -247,7 +247,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -255,8 +256,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.132.1"
version = "1.134.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -33,7 +33,6 @@ skip = [
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "fd-lock", version = "3.0.13" },
{ name = "getrandom", version = "<0.2" },
{ name = "h2", version = "0.3.22" },
{ name = "http-body", version = "0.4.5" },
@@ -100,5 +99,4 @@ license-files = [
github = [
"async-email",
"deltachat",
"djc",
]

49
fuzz/Cargo.lock generated
View File

@@ -190,11 +190,11 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.1"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
checksum = "a9d69fc1499878158750f644c4eb46aff55bb9d32d77e3dc4aecf8308d5c3ba6"
dependencies = [
"async-channel 1.8.0",
"async-channel 2.1.1",
"base64 0.21.0",
"bytes",
"chrono",
@@ -922,7 +922,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.131.9"
version = "1.133.0"
dependencies = [
"anyhow",
"async-channel 2.1.1",
@@ -963,6 +963,7 @@ dependencies = [
"pin-project",
"qrcodegen",
"quick-xml",
"quoted_printable 0.5.0",
"rand 0.8.5",
"ratelimit",
"regex",
@@ -1859,9 +1860,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.17"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
@@ -1869,7 +1870,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 1.9.2",
"indexmap",
"slab",
"tokio",
"tokio-util",
@@ -2146,16 +2147,6 @@ dependencies = [
"nom",
]
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.1.0"
@@ -2215,7 +2206,8 @@ checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
[[package]]
name = "iroh"
version = "0.4.2"
source = "git+https://github.com/n0-computer/iroh?branch=maint-0.4#9881b7886235035a1124e4371f7a4cd59379e51b"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85075391dcb8491a4939266334b28601052d418b37d20b33c58ffb5776adc912"
dependencies = [
"abao",
"anyhow",
@@ -2420,7 +2412,7 @@ checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32"
dependencies = [
"charset",
"data-encoding",
"quoted_printable",
"quoted_printable 0.4.6",
]
[[package]]
@@ -2431,7 +2423,7 @@ checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
dependencies = [
"charset",
"data-encoding",
"quoted_printable",
"quoted_printable 0.4.6",
]
[[package]]
@@ -3258,6 +3250,12 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
[[package]]
name = "quoted_printable"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
[[package]]
name = "rand"
version = "0.7.3"
@@ -3411,9 +3409,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "reqwest"
version = "0.11.20"
version = "0.11.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -3436,6 +3434,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
@@ -4156,9 +4155,9 @@ dependencies = [
[[package]]
name = "system-configuration"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@@ -4429,7 +4428,7 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap 2.1.0",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",

View File

@@ -29,9 +29,11 @@ module.exports = {
DC_DOWNLOAD_FAILURE: 20,
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
DC_EVENT_CHAT_MODIFIED: 2020,
DC_EVENT_CONFIGURE_PROGRESS: 2041,
DC_EVENT_CONFIG_SYNCED: 2111,
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
DC_EVENT_CONTACTS_CHANGED: 2030,
DC_EVENT_DELETED_BLOB_FILE: 151,
@@ -79,6 +81,7 @@ module.exports = {
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
DC_INFO_GROUP_IMAGE_CHANGED: 3,
DC_INFO_GROUP_NAME_CHANGED: 2,
DC_INFO_INVALID_UNENCRYPTED_MAIL: 13,
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
DC_INFO_LOCATION_ONLY: 9,
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
@@ -225,11 +228,13 @@ module.exports = {
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
DC_STR_IMAGE: 9,
DC_STR_INCOMING_MESSAGES: 103,
DC_STR_INVALID_UNENCRYPTED_MAIL: 174,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
DC_STR_LOCATION: 66,
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
DC_STR_MESSAGES: 114,
DC_STR_MESSAGE_ADD_MEMBER: 173,
DC_STR_MSGACTIONBYME: 63,
DC_STR_MSGACTIONBYUSER: 62,
DC_STR_MSGADDMEMBER: 17,

View File

@@ -34,6 +34,8 @@ module.exports = {
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE'
}

View File

@@ -29,9 +29,11 @@ export enum C {
DC_DOWNLOAD_FAILURE = 20,
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
DC_EVENT_CHAT_MODIFIED = 2020,
DC_EVENT_CONFIGURE_PROGRESS = 2041,
DC_EVENT_CONFIG_SYNCED = 2111,
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
DC_EVENT_CONTACTS_CHANGED = 2030,
DC_EVENT_DELETED_BLOB_FILE = 151,
@@ -79,6 +81,7 @@ export enum C {
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
DC_INFO_GROUP_IMAGE_CHANGED = 3,
DC_INFO_GROUP_NAME_CHANGED = 2,
DC_INFO_INVALID_UNENCRYPTED_MAIL = 13,
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
DC_INFO_LOCATION_ONLY = 9,
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
@@ -225,11 +228,13 @@ export enum C {
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_INVALID_UNENCRYPTED_MAIL = 174,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MESSAGE_ADD_MEMBER = 173,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
@@ -319,6 +324,8 @@ export const EventId2EventName: { [key: number]: string } = {
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
}

View File

@@ -178,7 +178,7 @@ export class AccountManager extends EventEmitter {
static newTemporary() {
let directory = null
while (true) {
const randomString = Math.random().toString(36).substr(2, 5)
const randomString = Math.random().toString(36).substring(2, 5)
directory = join(tmpdir(), 'deltachat-' + randomString)
if (!existsSync(directory)) break
}

View File

@@ -1,13 +1,17 @@
// @ts-check
import DeltaChat from '../dist'
import { DeltaChat } from '../dist/index.js'
import { deepStrictEqual, strictEqual } from 'assert'
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { EventId2EventName, C } from '../dist/constants'
import { EventId2EventName, C } from '../dist/constants.js'
import { join } from 'path'
import { statSync } from 'fs'
import { Context } from '../dist/context'
import { Context } from '../dist/context.js'
import {fileURLToPath} from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
chai.use(chaiAsPromised)
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.

View File

@@ -10,7 +10,6 @@
"@types/node": "^20.8.10",
"chai": "~4.3.10",
"chai-as-promised": "^7.1.1",
"esm": "^3.2.25",
"mocha": "^8.2.1",
"node-gyp": "^10.0.0",
"prebuildify": "^5.0.1",
@@ -53,8 +52,8 @@
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.132.1"
"version": "1.134.0"
}

View File

@@ -1,6 +1,5 @@
"""Account class implementation."""
import os
from array import array
from contextlib import contextmanager
@@ -478,6 +477,16 @@ class Account:
msg_ids = [msg.id for msg in messages]
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
def resend_messages(self, messages: List[Message]) -> None:
"""Resend list of messages.
:param messages: list of :class:`deltachat.message.Message` object.
:returns: None
"""
msg_ids = [msg.id for msg in messages]
if lib.dc_resend_msgs(self._dc_context, msg_ids, len(msg_ids)) != 1:
raise ValueError(f"could not resend messages {msg_ids}")
def delete_messages(self, messages: List[Message]) -> None:
"""delete messages (local and remote).

View File

@@ -10,6 +10,7 @@ import time
import weakref
import random
from queue import Queue
from threading import Event
from typing import Callable, Dict, List, Optional, Set
import pytest
@@ -590,6 +591,27 @@ class ACFactory:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
class SetupPlugin:
def __init__(self) -> None:
self.member_added = Event()
@account_hookimpl
def ac_member_added(self, chat: deltachat.Chat, contact, actor, message):
self.member_added.set()
setupplugin = SetupPlugin()
ac1.add_account_plugin(setupplugin)
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
setupplugin.member_added.wait()
msg = ac2.wait_next_incoming_message()
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
msg = ac2.wait_next_incoming_message()
assert "Member Me " in msg.text and " added by " in msg.text
return chat
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):

View File

@@ -498,6 +498,26 @@ def test_forward_messages(acfactory, lp):
assert not chat3.get_messages()
def test_forward_encrypted_to_unencrypted(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
chat = acfactory.get_protected_chat(ac1, ac2)
lp.sec("ac1: send encrypted message to ac2")
txt = "This should be encrypted"
chat.send_text(txt)
msg = ac2.wait_next_incoming_message()
assert msg.text == txt
assert msg.is_encrypted()
lp.sec("ac2: forward message to ac3 unencrypted")
unencrypted_chat = ac2.create_chat(ac3)
msg_id = msg.id
msg2 = unencrypted_chat.send_msg(msg)
assert msg2 == msg
assert msg.id != msg_id
assert not msg.is_encrypted()
def test_forward_own_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -523,6 +543,27 @@ def test_forward_own_message(acfactory, lp):
assert msg_in.is_forwarded()
def test_resend_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat1 = ac1.create_chat(ac2)
lp.sec("ac1: send message to ac2")
chat1.send_text("message")
lp.sec("ac2: receive message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message"
chat2 = msg_in.chat
chat2_msg_cnt = len(chat2.get_messages())
lp.sec("ac1: resend message")
ac1.resend_messages([msg_in])
lp.sec("ac2: check that message is deleted")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
assert len(chat2.get_messages()) == chat2_msg_cnt
def test_long_group_name(acfactory, lp):
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
group names after MIME serialization/deserialization".
@@ -1531,10 +1572,11 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
def test_import_export_online_all(acfactory, tmp_path, data, lp):
(ac1,) = acfactory.get_online_accounts(1)
(ac1, some1) = acfactory.get_online_accounts(2)
lp.sec("create some chat content")
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
some1_addr = some1.get_config("addr")
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
chat1.send_text("msg1")
assert len(ac1.get_contacts(query="some1")) == 1
@@ -1551,7 +1593,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
contacts = ac.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3

View File

@@ -1 +1 @@
2023-12-12
2024-01-31

View File

@@ -102,7 +102,7 @@ def main():
found = True
if not found:
raise SystemExit(
f"{changelog_name} contains no entry for version: {newversion}"
f"CHANGELOG.md contains no entry for version: {newversion}"
)
for toml_filename in toml_list:

View File

@@ -5,6 +5,7 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use futures::future::join_all;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -291,6 +292,42 @@ impl Accounts {
}
}
/// Performs a background fetch for all accounts in parallel.
///
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_without_timeout(&self) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
join_all(
self.accounts
.values()
.cloned()
.map(background_fetch_and_log_error),
)
.await;
}
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
/// process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
pub async fn background_fetch(&self, timeout: std::time::Duration) {
if let Err(_err) =
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
{
self.emit_event(EventType::Warning(
"Background fetch timed out.".to_string(),
));
}
self.emit_event(EventType::AccountsBackgroundFetchDone);
}
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
self.events.emit(Event { id: 0, typ: event })

View File

@@ -2657,6 +2657,11 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
return send_msg_inner(context, chat_id, msg).await;
}
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.update_param(context).await?;
}
send_msg_inner(context, chat_id, msg).await
}
@@ -2746,10 +2751,18 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let from = context.get_primary_self_addr().await?;
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately. `from` must be the last addr, see `receive_imf_inner()` why.
// Send BCC to self if it is enabled.
//
// Previous versions of Delta Chat did not send BCC self
// if DeleteServerAfter was set to immediately delete messages
// from the server. This is not the case anymore
// because BCC-self messages are also used to detect
// that message was sent if SMTP server is slow to respond
// and connection is frequently lost
// before receiving status line.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await? != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
@@ -4063,6 +4076,7 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
chat_id: msg.chat_id,
msg_id: msg.id,
});
msg.timestamp_sort = create_smeared_timestamp(context);
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
}
@@ -6489,6 +6503,7 @@ mod tests {
// Bob receives all messages
let bob = TestContext::new_bob().await;
let msg = bob.recv_msg(&sent1).await;
let sent1_ts_sent = msg.timestamp_sent;
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
@@ -6511,6 +6526,7 @@ mod tests {
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
assert_eq!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent);
Ok(())
}

View File

@@ -564,6 +564,7 @@ impl Context {
if sync != Sync {
return Ok(());
}
self.emit_event(EventType::ConfigSynced { key });
let Some(val) = value else {
return Ok(());
};
@@ -866,10 +867,43 @@ mod tests {
// Reset to default. Test that it's not synced because defaults may differ across client
// versions.
alice0.set_config(Config::MdnsEnabled, None).await?;
assert!(alice0.get_config_bool(Config::MdnsEnabled).await?);
assert_eq!(alice0.get_config_bool(Config::MdnsEnabled).await?, true);
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::MdnsEnabled
}
)
})
.await;
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::MdnsEnabled
}
)
})
.await;
sync(&alice0, &alice1).await;
assert!(!alice1.get_config_bool(Config::MdnsEnabled).await?);
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
alice1
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::MdnsEnabled
}
)
})
.await;
let show_emails = alice0.get_config_bool(Config::ShowEmails).await?;
alice0

View File

@@ -137,20 +137,11 @@ impl ServerParams {
}
fn expand_strict_tls(self) -> Vec<ServerParams> {
if self.strict_tls.is_none() {
vec![
Self {
strict_tls: Some(true), // Strict.
..self.clone()
},
Self {
strict_tls: None, // Automatic.
..self
},
]
} else {
vec![self]
}
vec![Self {
// Strict if not set by the user or provider database.
strict_tls: Some(self.strict_tls.unwrap_or(true)),
..self
}]
}
}
@@ -162,31 +153,10 @@ pub(crate) fn expand_param_vector(
domain: &str,
) -> Vec<ServerParams> {
v.into_iter()
.map(|params| {
if params.socket == Socket::Plain {
ServerParams {
// Avoid expanding plaintext configuration into configuration with and without
// `strict_tls` if `strict_tls` is set to `None` as `strict_tls` is not used for
// plaintext connections. Always setting it to "enabled", just in case.
strict_tls: Some(true),
..params
}
} else {
params
}
})
// The order of expansion is important.
//
// Ports are expanded the last, so they are changed the first. Username is only changed if
// default value (address with domain) didn't work for all available hosts and ports.
//
// Strict TLS must be expanded first, so we try all configurations with strict TLS first
// and only then try again without strict TLS. Otherwise we may lock to wrong hostname
// without strict TLS when another hostname with strict TLS is available. For example, if
// both smtp.example.net and mail.example.net are running an SMTP server, but both use a
// certificate that is only valid for mail.example.net, we want to skip smtp.example.net
// and use mail.example.net with strict TLS instead of using smtp.example.net without
// strict TLS.
.flat_map(|params| params.expand_strict_tls().into_iter())
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
@@ -257,22 +227,6 @@ mod tests {
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: None,
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: None
}
],
);
@@ -284,7 +238,7 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: None,
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",

View File

@@ -214,6 +214,9 @@ pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
// `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: i64 = 12 * 60 * 60; // 12 hours
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;

View File

@@ -15,15 +15,16 @@ use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::constants::{DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
use crate::contact::Contact;
use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::SchedulerState;
use crate::scheduler::{convert_folder_meaning, SchedulerState};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -224,6 +225,9 @@ pub struct InnerContext {
/// <https://datatracker.ietf.org/doc/html/rfc2971>
pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,
/// IMAP METADATA.
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
@@ -384,6 +388,7 @@ impl Context {
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
metadata: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
@@ -436,6 +441,55 @@ impl Context {
self.scheduler.maybe_network().await;
}
/// Does a background fetch
/// pauses the scheduler and does one imap fetch, then unpauses and returns
pub async fn background_fetch(&self) -> Result<()> {
if !(self.is_configured().await?) {
return Ok(());
}
let address = self.get_primary_self_addr().await?;
let time_start = std::time::SystemTime::now();
info!(self, "background_fetch started fetching {address}");
let _pause_guard = self.scheduler.pause(self.clone()).await?;
// connection
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
connection.prepare(self).await?;
// fetch imap folders
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?;
connection
.fetch_move_delete(self, &watch_folder, folder_meaning)
.await?;
}
// update quota (to send warning if full) - but only check it once in a while
let quota_needs_update = {
let quota = self.quota.read().await;
quota
.as_ref()
.filter(|quota| quota.modified + DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT > time())
.is_none()
};
if quota_needs_update {
if let Err(err) = self.update_recent_quota(&mut connection).await {
warn!(self, "Failed to update quota: {err:#}.");
}
}
info!(
self,
"background_fetch done for {address} took {:?}",
time_start.elapsed().unwrap_or_default()
);
Ok(())
}
pub(crate) async fn schedule_resync(&self) -> Result<()> {
self.resync_request.store(true, Ordering::Relaxed);
self.scheduler.interrupt_inbox().await;
@@ -669,6 +723,16 @@ impl Context {
res.insert("imap_server_id", format!("{server_id:?}"));
}
if let Some(metadata) = &*self.metadata.read().await {
if let Some(comment) = &metadata.comment {
res.insert("imap_server_comment", format!("{comment:?}"));
}
if let Some(admin) = &metadata.admin {
res.insert("imap_server_admin", format!("{admin:?}"));
}
}
res.insert("secondary_addrs", secondary_addrs);
res.insert(
"fetch_existing_msgs",

View File

@@ -136,39 +136,36 @@ pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Im
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
"SELECT uid, folder, uidvalidity FROM imap WHERE rfc724_mid=? AND target!=''",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
let uidvalidity: u32 = row.get(2)?;
Ok((server_uid, server_folder, uidvalidity))
},
)
.await?;
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
msg.id
.update_download_state(context, DownloadState::Failure)
.await?;
Err(anyhow!("Call download_full() again to try over."))
}
ImapActionResult::Success => {
// update_download_state() not needed as receive_imf() already
// set the state and emitted the event.
Ok(())
}
}
} else {
let Some((server_uid, server_folder, uidvalidity)) = row else {
// No IMAP record found, we don't know the UID and folder.
msg.id
.update_download_state(context, DownloadState::Failure)
.await?;
Err(anyhow!("Call download_full() again to try over."))
return Err(anyhow!("Call download_full() again to try over."));
};
match imap
.fetch_single_msg(
context,
&server_folder,
uidvalidity,
server_uid,
msg.rfc724_mid.clone(),
)
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
Err(anyhow!("Call download_full() again to try over."))
}
ImapActionResult::Success => Ok(()),
}
}
@@ -181,6 +178,7 @@ impl Imap {
&mut self,
context: &Context,
folder: &str,
uidvalidity: u32,
uid: u32,
rfc724_mid: String,
) -> ImapActionResult {
@@ -197,7 +195,15 @@ impl Imap {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (last_uid, _received) = match self
.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, false)
.fetch_many_msgs(
context,
folder,
uidvalidity,
vec![uid],
&uid_message_ids,
false,
false,
)
.await
{
Ok(res) => res,
@@ -257,7 +263,7 @@ mod tests {
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_inner;
use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::TestContext;
#[test]
@@ -338,7 +344,7 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain";
receive_imf_inner(
receive_imf_from_inbox(
&t,
"Mr.12345678901@example.com",
header.as_bytes(),
@@ -354,7 +360,7 @@ mod tests {
.get_text()
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
receive_imf_inner(
receive_imf_from_inbox(
&t,
"Mr.12345678901@example.com",
format!("{header}\n\n100k text...").as_bytes(),
@@ -383,7 +389,7 @@ mod tests {
.await?;
// download message from bob partially, this must not change the ephemeral timer
receive_imf_inner(
receive_imf_from_inbox(
&t,
"first@example.org",
b"From: Bob <bob@example.org>\n\
@@ -426,7 +432,7 @@ mod tests {
let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
// not downloading the status update results in an placeholder
receive_imf_inner(
receive_imf_from_inbox(
&bob,
&sent2_rfc724_mid,
sent2.payload().as_bytes(),
@@ -442,7 +448,7 @@ mod tests {
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
// (usually status updates are too small for not being downloaded directly)
receive_imf_inner(
receive_imf_from_inbox(
&bob,
&sent2_rfc724_mid,
sent2.payload().as_bytes(),
@@ -493,7 +499,7 @@ mod tests {
";
// not downloading the mdn results in an placeholder
receive_imf_inner(
receive_imf_from_inbox(
&bob,
"bar@example.org",
raw,
@@ -509,7 +515,7 @@ mod tests {
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
// (usually mdn are too small for not being downloaded directly)
receive_imf_inner(&bob, "bar@example.org", raw, false, None, false).await?;
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None, false).await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
assert!(Message::load_from_db(&bob, msg.id)
.await?

View File

@@ -52,7 +52,7 @@ impl EncryptHelper {
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, &str)],
peerstates: &[(Option<Peerstate>, String)],
) -> Result<bool> {
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
1
@@ -94,7 +94,7 @@ impl EncryptHelper {
context: &Context,
verified: bool,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, &str)>,
peerstates: Vec<(Option<Peerstate>, String)>,
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
@@ -117,7 +117,7 @@ impl EncryptHelper {
// Encrypt to secondary verified keys
// if we also encrypt to the introducer ("verifier") of the key.
if verified {
for (peerstate, _addr) in peerstates {
for (peerstate, _addr) in &peerstates {
if let Some(peerstate) = peerstate {
if let (Some(key), Some(verifier)) = (
peerstate.secondary_verified_key.as_ref(),
@@ -293,7 +293,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
Ok(())
}
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, &'static str)> {
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, String)> {
let addr = "bob@foo.bar";
let pub_key = bob_keypair().public;
let peerstate = Peerstate {
@@ -315,7 +315,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
backward_verified_key_id: None,
fingerprint_changed: false,
};
vec![(Some(peerstate), addr)]
vec![(Some(peerstate), addr.to_string())]
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -340,7 +340,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar")];
let ps = vec![(None, "bob@foo.bar".to_string())];
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}

View File

@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::chat::ChatId;
use crate::config::Config;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
@@ -261,8 +262,17 @@ pub enum EventType {
ConnectivityChanged,
/// The user's avatar changed.
/// Deprecated by `ConfigSynced`.
SelfavatarChanged,
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
/// would be logged which might not be good for privacy.
ConfigSynced {
/// Configuration key.
key: Config,
},
/// Webxdc status update received.
WebxdcStatusUpdate {
/// Message ID.
@@ -277,4 +287,11 @@ pub enum EventType {
/// ID of the deleted message.
msg_id: MsgId,
},
/// Tells that the Background fetch was completed (or timed out).
/// This event acts as a marker, when you reach this event you can be sure
/// that all events emitted during the background fetch were processed.
///
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
}

View File

@@ -73,6 +73,7 @@ pub enum ImapActionResult {
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
DATE \
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
@@ -117,6 +118,17 @@ struct OAuth2 {
access_token: String,
}
#[derive(Debug)]
pub(crate) struct ServerMetadata {
/// IMAP METADATA `/shared/comment` as defined in
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
pub comment: Option<String>,
/// IMAP METADATA `/shared/admin` as defined in
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2>.
pub admin: Option<String>,
}
impl async_imap::Authenticator for OAuth2 {
type Response = String;
@@ -769,6 +781,7 @@ impl Imap {
let mut uids_fetch = Vec::<(_, bool /* partially? */)>::with_capacity(msgs.len() + 1);
let mut uid_message_ids = BTreeMap::new();
let mut largest_uid_skipped = None;
let delete_target = context.get_delete_msgs_target().await?;
// Store the info about IMAP messages in the database.
for (uid, ref fetch_response) in msgs {
@@ -794,8 +807,24 @@ impl Imap {
// Such move to the same folder results in the messages
// getting a new UID, so the messages will be detected as new
// in the `INBOX.DeltaChat` folder again.
let _target;
let target = if let Some(message_id) = &message_id {
if context
let is_dup = if let Some((_, ts_sent_old)) =
message::rfc724_mid_exists(context, message_id).await?
{
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let ts_sent = headers
.get_header_value(HeaderDef::Date)
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
is_dup_msg(is_chat_msg, ts_sent, ts_sent_old)
} else {
false
};
if is_dup {
info!(context, "Deleting duplicate message {message_id}.");
&delete_target
} else if context
.sql
.exists(
"SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
@@ -807,9 +836,10 @@ impl Imap {
context,
"Not moving the message {} that we have seen before.", &message_id
);
folder.to_string()
folder
} else {
target_folder(context, folder, folder_meaning, &headers).await?
_target = target_folder(context, folder, folder_meaning, &headers).await?;
&_target
}
} else {
// Do not move the messages without Message-ID.
@@ -819,7 +849,7 @@ impl Imap {
context,
"Not moving the message that does not have a Message-ID."
);
folder.to_string()
folder
};
// Generate a fake Message-ID to identify the message in the database
@@ -834,7 +864,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(&message_id, &folder, uid, uid_validity, &target),
(&message_id, &folder, uid, uid_validity, target),
)
.await?;
@@ -887,6 +917,7 @@ impl Imap {
.fetch_many_msgs(
context,
folder,
uid_validity,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
@@ -1431,10 +1462,12 @@ impl Imap {
/// Returns the last UID fetched successfully and the info about each downloaded message.
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
folder: &str,
uidvalidity: u32,
request_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
@@ -1568,6 +1601,9 @@ impl Imap {
);
match receive_imf_inner(
context,
folder,
uidvalidity,
request_uid,
rfc724_mid,
body,
is_seen,
@@ -1611,6 +1647,50 @@ impl Imap {
Ok((last_uid, received_msgs))
}
/// Retrieves server metadata if it is supported.
///
/// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
/// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
/// metadata.
pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
let session = self.session.as_mut().context("no session")?;
if !session.can_metadata() {
return Ok(());
}
let mut lock = context.metadata.write().await;
if (*lock).is_some() {
return Ok(());
}
info!(
context,
"Server supports metadata, retrieving server comment and admin contact."
);
let mut comment = None;
let mut admin = None;
let mailbox = "";
let options = "";
let metadata = session
.get_metadata(mailbox, options, "(/shared/comment /shared/admin)")
.await?;
for m in metadata {
match m.entry.as_ref() {
"/shared/comment" => {
comment = m.value;
}
"/shared/admin" => {
admin = m.value;
}
_ => {}
}
}
*lock = Some(ServerMetadata { comment, admin });
Ok(())
}
}
impl Session {
@@ -2247,6 +2327,15 @@ pub(crate) async fn prefetch_should_download(
Ok(should_download)
}
/// Returns whether a message is a duplicate (resent message).
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
// because they are stored to the db before sending. Also consider as duplicates only messages
// with greater timestamp to avoid deleting both messages in a multi-device setting.
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
}
/// Marks messages in `msgs` table as seen, searching for them by UID.
///
/// Returns updated chat ID if any message was marked as seen.

View File

@@ -21,6 +21,10 @@ pub(crate) struct Capabilities {
/// <https://tools.ietf.org/html/rfc7162>
pub can_condstore: bool,
/// True if the server has METADATA capability as defined in
/// <https://tools.ietf.org/html/rfc5464>
pub can_metadata: bool,
/// Server ID if the server supports ID capability.
pub server_id: Option<HashMap<String, String>>,
}

View File

@@ -59,6 +59,7 @@ async fn determine_capabilities(
can_move: caps.has_str("MOVE"),
can_check_quota: caps.has_str("QUOTA"),
can_condstore: caps.has_str("CONDSTORE"),
can_metadata: caps.has_str("METADATA"),
server_id,
};
Ok(capabilities)

View File

@@ -64,4 +64,8 @@ impl Session {
pub fn can_condstore(&self) -> bool {
self.capabilities.can_condstore
}
pub fn can_metadata(&self) -> bool {
self.capabilities.can_metadata
}
}

View File

@@ -14,8 +14,22 @@ use crate::socks::Socks5Config;
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
/// Same as AcceptInvalidCertificates unless overridden by
/// `strict_tls` setting in provider database.
/// Same as AcceptInvalidCertificates if stored in the database
/// as `configured_{imap,smtp}_certificate_checks`.
///
/// Previous Delta Chat versions stored this in `configured_*`
/// if Automatic configuration
/// was selected, configuration with strict TLS checks failed
/// and configuration without strict TLS checks succeeded.
///
/// Currently Delta Chat stores only
/// `Strict` or `AcceptInvalidCertificates` variants
/// in `configured_*` settings.
///
/// `Automatic` in `{imap,smtp}_certificate_checks`
/// means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// `Automatic` is the same as `Strict`.
Automatic = 0,
Strict = 1,

View File

@@ -757,7 +757,7 @@ impl Message {
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
}
/// Returns true if message is Auto-Submitted.
/// Returns true if message is auto-generated.
pub fn is_bot(&self) -> bool {
self.param.get_bool(Param::Bot).unwrap_or_default()
}
@@ -1130,7 +1130,7 @@ impl Message {
/// `References` header is not taken into account.
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
if let Some((msg_id, _ts_sent)) = rfc724_mid_exists(context, in_reply_to).await? {
let msg = Message::load_from_db(context, msg_id).await?;
return if msg.chat_id.is_trash() {
// If message is already moved to trash chat, pretend it does not exist.
@@ -1816,18 +1816,23 @@ pub async fn estimate_deletion_cnt(
Ok(cnt)
}
/// See [`rfc724_mid_exists_and()`].
pub(crate) async fn rfc724_mid_exists(
context: &Context,
rfc724_mid: &str,
) -> Result<Option<MsgId>> {
) -> Result<Option<(MsgId, i64)>> {
rfc724_mid_exists_and(context, rfc724_mid, "1").await
}
/// Returns [MsgId] and "sent" timestamp of the message with given `rfc724_mid` (Message-ID header)
/// if it exists in the db.
///
/// @param cond SQL subexpression for filtering messages.
pub(crate) async fn rfc724_mid_exists_and(
context: &Context,
rfc724_mid: &str,
cond: &str,
) -> Result<Option<MsgId>> {
) -> Result<Option<(MsgId, i64)>> {
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
if rfc724_mid.is_empty() {
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
@@ -1837,12 +1842,13 @@ pub(crate) async fn rfc724_mid_exists_and(
let res = context
.sql
.query_row_optional(
&("SELECT id FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
&("SELECT id, timestamp_sent FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
(rfc724_mid,),
|row| {
let msg_id: MsgId = row.get(0)?;
let timestamp_sent: i64 = row.get(1)?;
Ok(msg_id)
Ok((msg_id, timestamp_sent))
},
)
.await?;
@@ -1858,7 +1864,7 @@ pub(crate) async fn get_latest_by_rfc724_mids(
mids: &[String],
) -> Result<Option<Message>> {
for id in mids.iter().rev() {
if let Some(msg_id) = rfc724_mid_exists(context, id).await? {
if let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? {
let msg = Message::load_from_db(context, msg_id).await?;
if msg.chat_id != DC_CHAT_ID_TRASH {
return Ok(Some(msg));

View File

@@ -277,7 +277,7 @@ impl<'a> MimeFactory<'a> {
async fn peerstates_for_recipients(
&self,
context: &Context,
) -> Result<Vec<(Option<Peerstate>, &str)>> {
) -> Result<Vec<(Option<Peerstate>, String)>> {
let self_addr = context.get_primary_self_addr().await?;
let mut res = Vec::new();
@@ -286,7 +286,7 @@ impl<'a> MimeFactory<'a> {
.iter()
.filter(|(_, addr)| addr != &self_addr)
{
res.push((Peerstate::from_addr(context, addr).await?, addr.as_str()));
res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
}
Ok(res)
@@ -351,7 +351,7 @@ impl<'a> MimeFactory<'a> {
.unwrap_or_default()
}
}
Loaded::Mdn { .. } => true,
Loaded::Mdn { .. } => false,
}
}
@@ -917,6 +917,16 @@ impl<'a> MimeFactory<'a> {
Ok(Some(part))
}
fn add_message_text(&self, part: PartBuilder, mut text: String) -> PartBuilder {
// This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable
// encoding and thus breaking messages and signatures. It's unlikely that the reader uses a
// MUA not supporting Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for
// encrypted messages.
let part = part.header(("Content-Transfer-Encoding", "quoted-printable"));
text = quoted_printable::encode_to_str(text);
part.body(text)
}
#[allow(clippy::cognitive_complexity)]
async fn render_message(
&mut self,
@@ -1214,13 +1224,11 @@ impl<'a> MimeFactory<'a> {
footer
);
// Message is sent as text/plain, with charset = utf-8
let mut main_part = PartBuilder::new()
.header((
"Content-Type".to_string(),
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text);
let mut main_part = PartBuilder::new().header((
"Content-Type",
"text/plain; charset=utf-8; format=flowed; delsp=no",
));
main_part = self.add_message_text(main_part, message_text);
if is_reaction {
main_part = main_part.header(("Content-Disposition", "reaction"));
@@ -1347,15 +1355,12 @@ impl<'a> MimeFactory<'a> {
};
let p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
let message_text = format!("{}\r\n", format_flowed(&p2));
message = message.child(
PartBuilder::new()
.header((
"Content-Type".to_string(),
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text)
.build(),
);
let text_part = PartBuilder::new().header((
"Content-Type".to_string(),
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
));
let text_part = self.add_message_text(text_part, message_text);
message = message.child(text_part.build());
// second body part: machine-readable, always REQUIRED by RFC 6522
let message_text2 = format!(
@@ -1571,6 +1576,7 @@ mod tests {
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1826,6 +1832,37 @@ mod tests {
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_create_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::SkipAutocrypt, 1);
let chat_alice = alice.create_chat(&bob).await.id;
let sent = alice.send_msg(chat_alice, &mut msg).await;
let rcvd = bob.recv_msg(&sent).await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?;
let rendered_msg = mimefactory.render(&bob).await?;
assert!(!rendered_msg.is_encrypted);
let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?;
let rendered_msg = mimefactory.render(&bob).await?;
// When encrypted, the MDN should be encrypted as well
assert!(rendered_msg.is_encrypted);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_subject_in_group() -> Result<()> {
async fn send_msg_get_subject(
@@ -2166,6 +2203,7 @@ mod tests {
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(inner.match_indices("Subject:").count(), 0);
assert_eq!(inner.match_indices("quoted-printable").count(), 1);
assert_eq!(body.match_indices("this is the text!").count(), 1);
@@ -2186,6 +2224,7 @@ mod tests {
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(inner.match_indices("Subject:").count(), 0);
assert_eq!(inner.match_indices("quoted-printable").count(), 1);
assert_eq!(body.match_indices("this is the text!").count(), 1);
@@ -2242,6 +2281,7 @@ mod tests {
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 0);
assert_eq!(part.match_indices("quoted-printable").count(), 1);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);
@@ -2289,6 +2329,7 @@ mod tests {
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 0);
assert_eq!(part.match_indices("quoted-printable").count(), 1);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);

View File

@@ -117,8 +117,8 @@ pub(crate) struct MimeMessage {
/// Hop info for debugging.
pub(crate) hop_info: String,
/// Whether the contact sending this should be marked as bot.
pub(crate) is_bot: bool,
/// Whether the contact sending this should be marked as bot or non-bot.
pub(crate) is_bot: Option<bool>,
/// When the message was received, in secs since epoch.
pub(crate) timestamp_rcvd: i64,
@@ -176,6 +176,10 @@ pub enum SystemMessage {
/// "%1$s sent a message from another device."
ChatProtectionDisabled = 12,
/// Message can't be sent because of `Invalid unencrypted mail to <>`
/// which is sent by chatmail servers.
InvalidUnencryptedMail = 13,
/// Self-sent-message that contains only json used for multi-device-sync;
/// if possible, we attach that to other messages as for locations.
MultiDeviceSync = 20,
@@ -390,9 +394,6 @@ impl MimeMessage {
}
}
// Auto-submitted is also set by holiday-notices so we also check `chat-version`
let is_bot = headers.contains_key("auto-submitted") && headers.contains_key("chat-version");
let timestamp_rcvd = smeared_time(context);
let timestamp_sent = headers
.get(HeaderDef::Date.get_headername())
@@ -427,7 +428,7 @@ impl MimeMessage {
is_mime_modified: false,
decoded_data: Vec::new(),
hop_info,
is_bot,
is_bot: None,
timestamp_rcvd,
timestamp_sent,
};
@@ -460,6 +461,13 @@ impl MimeMessage {
},
};
if parser.mdn_reports.is_empty() {
// "Auto-Submitted" is also set by holiday-notices so we also check "chat-version".
let is_bot = parser.headers.get("auto-submitted")
== Some(&"auto-generated".to_string())
&& parser.headers.contains_key("chat-version");
parser.is_bot = Some(is_bot);
}
parser.maybe_remove_bad_parts();
parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await;
@@ -698,7 +706,7 @@ impl MimeMessage {
self.do_add_single_part(part);
}
if self.headers.contains_key("auto-submitted") {
if self.is_bot == Some(true) {
for part in &mut self.parts {
part.param.set(Param::Bot, "1");
}
@@ -970,10 +978,13 @@ impl MimeMessage {
}
}
Some(_) => {
if let Some(first) = mail.subparts.first() {
any_part_added = self
.parse_mime_recursive(context, first, is_related)
.await?;
for cur_data in &mail.subparts {
if self
.parse_mime_recursive(context, cur_data, is_related)
.await?
{
any_part_added = true;
}
}
}
}
@@ -2733,6 +2744,7 @@ Chat-Version: 1.0\n\
Message-ID: <bar@example.org>\n\
To: Alice <alice@example.org>\n\
From: Bob <bob@example.org>\n\
Auto-Submitted: auto-replied\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
\n\
@@ -2768,6 +2780,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
assert_eq!(message.parts.len(), 1);
assert_eq!(message.mdn_reports.len(), 1);
assert_eq!(message.is_bot, None);
}
/// Test parsing multiple MDNs combined in a single message.
@@ -3819,4 +3832,20 @@ Content-Disposition: reaction\n\
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_tlsrpt() -> Result<()> {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/tlsrpt.eml");
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(msg.parts.len(), 1);
assert_eq!(msg.parts[0].typ, Viewtype::File);
assert_eq!(msg.parts[0].msg, "Report Domain: nine.testrun.org Submitter: google.com Report-ID: <2024.01.20T00.00.00Z+nine.testrun.org@google.com> This is an aggregate TLS report from google.com");
Ok(())
}
}

View File

@@ -65,7 +65,7 @@ pub enum Param {
/// For Messages: the message is a reaction.
Reaction = b'x',
/// For Messages: a message with Auto-Submitted header ("bot").
/// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
Bot = b'b',
/// For Messages: unset or 0=not forwarded,

View File

@@ -25,6 +25,8 @@ use crate::socks::Socks5Config;
use crate::token;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
@@ -253,6 +255,10 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
decode_openpgp(context, qr)
.await
.context("failed to decode OPENPGP4FPR QR code")?
} else if qr.starts_with(IDELTACHAT_SCHEME) {
decode_ideltachat(context, IDELTACHAT_SCHEME, qr).await?
} else if qr.starts_with(IDELTACHAT_NOSLASH_SCHEME) {
decode_ideltachat(context, IDELTACHAT_NOSLASH_SCHEME, qr).await?
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
decode_account(qr)?
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
@@ -301,11 +307,12 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
let (fingerprint, fragment) = match payload.find('#').map(|offset| {
let (fp, rest) = payload.split_at(offset);
// need to remove the # from the fragment
(fp, &rest[1..])
}) {
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
let (fingerprint, fragment) = match payload
.split_once('#')
.or_else(|| payload.split_once("%23"))
{
Some(pair) => pair,
None => (payload, ""),
};
@@ -453,6 +460,15 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
}
}
/// scheme: `https://i.delta.chat[/]#FINGERPRINT&a=ADDR[&OPTIONAL_PARAMS]`
async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<Qr> {
let qr = qr.replacen(prefix, OPENPGP4FPR_SCHEME, 1);
let qr = qr.replacen('&', "#", 1);
decode_openpgp(context, &qr)
.await
.context("failed to decode {prefix} QR code")
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr
@@ -943,6 +959,40 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_ideltachat_link() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(
&ctx.ctx,
"https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
).await?;
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
let qr = check_qr(
&ctx.ctx,
"https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
).await?;
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
Ok(())
}
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(
&ctx.ctx,
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
).await?;
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_group() -> Result<()> {
let ctx = TestContext::new().await;

View File

@@ -252,7 +252,7 @@ pub(crate) async fn set_msg_reaction(
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
} else {
info!(
@@ -316,7 +316,7 @@ mod tests {
use crate::contact::{Contact, ContactAddress, Origin};
use crate::download::DownloadState;
use crate::message::MessageState;
use crate::receive_imf::{receive_imf, receive_imf_inner};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -568,7 +568,7 @@ Here's my footer -- bob@example.net"
let msg_full = format!("{msg_header}\n\n100k text...");
// Alice downloads message from Bob partially.
let alice_received_message = receive_imf_inner(
let alice_received_message = receive_imf_from_inbox(
&alice,
"first@example.org",
msg_header.as_bytes(),
@@ -599,7 +599,7 @@ Here's my footer -- bob@example.net"
assert_eq!(msg.download_state(), DownloadState::Available);
// Alice downloads full message.
receive_imf_inner(
receive_imf_from_inbox(
&alice,
"first@example.org",
msg_full.as_bytes(),

View File

@@ -87,7 +87,7 @@ pub async fn receive_imf(
.split("\r\n\r\n")
.next()
.context("No empty line in the message")?;
return receive_imf_inner(
return receive_imf_from_inbox(
context,
&rfc724_mid,
head.as_bytes(),
@@ -98,7 +98,32 @@ pub async fn receive_imf(
.await;
}
}
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None, false).await
}
/// Emulates reception of a message from "INBOX".
///
/// Only used for tests and REPL tool, not actual message reception pipeline.
pub(crate) async fn receive_imf_from_inbox(
context: &Context,
rfc724_mid: &str,
imf_raw: &[u8],
seen: bool,
is_partial_download: Option<u32>,
fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> {
receive_imf_inner(
context,
"INBOX",
0,
0,
rfc724_mid,
imf_raw,
seen,
is_partial_download,
fetching_existing_messages,
)
.await
}
/// Inserts a tombstone into `msgs` table
@@ -130,8 +155,12 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId>
/// If `is_partial_download` is set, it contains the full message size in bytes.
/// Do not confuse that with `replace_msg_id` that will be set when the full message is loaded
/// later.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn receive_imf_inner(
context: &Context,
folder: &str,
uidvalidity: u32,
uid: u32,
rfc724_mid: &str,
imf_raw: &[u8],
seen: bool,
@@ -193,48 +222,17 @@ pub(crate) async fn receive_imf_inner(
);
let incoming = !context.is_self_addr(&mime_parser.from.addr).await?;
// For the case if we missed a successful SMTP response. Be optimistic that the message is
// delivered also.
let delivered = !incoming && {
let self_addr = context.get_primary_self_addr().await?;
context
.sql
.execute(
"DELETE FROM smtp \
WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
(rfc724_mid_orig, &self_addr),
)
.await?;
!context
.sql
.exists(
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
(rfc724_mid_orig,),
)
.await?
};
async fn on_msg_in_db(
context: &Context,
msg_id: MsgId,
delivered: bool,
) -> Result<Option<ReceivedMsg>> {
if delivered {
msg_id.set_delivered(context).await?;
}
Ok(None)
}
// check, if the mail is already in our database.
// make sure, this check is done eg. before securejoin-processing.
let (replace_msg_id, replace_chat_id);
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if is_partial_download.is_some() {
// Should never happen, see imap::prefetch_should_download(), but still.
info!(
context,
"Got a partial download and message is already in DB."
);
return on_msg_in_db(context, old_msg_id, delivered).await;
return Ok(None);
}
let msg = Message::load_from_db(context, old_msg_id).await?;
replace_msg_id = Some(old_msg_id);
@@ -249,8 +247,27 @@ pub(crate) async fn receive_imf_inner(
None
};
} else {
replace_msg_id = if rfc724_mid_orig != rfc724_mid {
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
None
} else if let Some((old_msg_id, old_ts_sent)) =
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
{
if imap::is_dup_msg(
mime_parser.has_chat_version(),
mime_parser.timestamp_sent,
old_ts_sent,
) {
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
let target = context.get_delete_msgs_target().await?;
context
.sql
.execute(
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
(target, folder, uidvalidity, uid),
)
.await?;
}
Some(old_msg_id)
} else {
None
};
@@ -261,7 +278,31 @@ pub(crate) async fn receive_imf_inner(
// Need to update chat id in the db.
} else if let Some(msg_id) = replace_msg_id {
info!(context, "Message is already downloaded.");
return on_msg_in_db(context, msg_id, delivered).await;
if incoming {
return Ok(None);
}
// For the case if we missed a successful SMTP response. Be optimistic that the message is
// delivered also.
let self_addr = context.get_primary_self_addr().await?;
context
.sql
.execute(
"DELETE FROM smtp \
WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
(rfc724_mid_orig, &self_addr),
)
.await?;
if !context
.sql
.exists(
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
(rfc724_mid_orig,),
)
.await?
{
msg_id.set_delivered(context).await?;
}
return Ok(None);
};
let prevent_rename =
@@ -539,7 +580,9 @@ pub(crate) async fn receive_imf_inner(
.handle_reports(context, from_id, &mime_parser.parts)
.await;
from_id.mark_bot(context, mime_parser.is_bot).await?;
if let Some(is_bot) = mime_parser.is_bot {
from_id.mark_bot(context, is_bot).await?;
}
Ok(Some(received_msg))
}
@@ -733,7 +776,7 @@ async fn add_parts(
create_blocked_default
};
if chat_id.is_none() {
if chat_id.is_none() && !is_mdn {
// try to create a group
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
@@ -2587,7 +2630,7 @@ async fn get_previous_message(
) -> Result<Option<Message>> {
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
if let Some(rfc724mid) = parse_message_ids(field).last() {
if let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await? {
if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
return Ok(Some(Message::load_from_db(context, msg_id).await?));
}
}

View File

@@ -310,6 +310,56 @@ async fn test_read_receipt_and_unarchive() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_and_alias() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chat = alice.create_chat(&bob).await;
let sent = alice.send_text(alice_chat.id, "alice -> bob").await;
let msg_id = sent.sender_msg_id;
receive_imf(
&alice,
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: bob@example.net\n\
To: alicechat@example.org\n\
Subject: message opened\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <aranudiaerudiaduiaertd@example.com>\n\
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
\n\
\n\
--SNIPP\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
Read receipts do not guarantee sth. was read.\n\
\n\
\n\
--SNIPP\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.28.0\n\
Original-Recipient: rfc822;bob@example.com\n\
Final-Recipient: rfc822;bob@example.com\n\
Original-Message-ID: <{msg_id}>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--SNIPP--",
)
.as_bytes(),
false,
)
.await?;
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
assert_eq!(chats.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_from() {
// if there is no from given, from_id stays 0 which is just fine. These messages
@@ -1832,7 +1882,7 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
.await
.unwrap();
let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org")
let (msg_id, _) = rfc724_mid_exists(&claire, "non-dc-1@example.org")
.await
.unwrap()
.unwrap();
@@ -2634,6 +2684,36 @@ async fn test_read_receipts_dont_create_chats() -> Result<()> {
Ok(())
}
/// Test that read receipts don't unmark contacts as bots.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_read_receipts_dont_unmark_bots() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let ab_contact = alice.add_or_lookup_contact(bob).await;
ab_contact.id.mark_bot(alice, true).await?;
let alice_chat = alice.create_chat(bob).await;
// Alice sends and Bob receives a message.
bob.recv_msg(&alice.send_text(alice_chat.id, "Message").await)
.await;
let received_msg = bob.get_last_msg().await;
// Bob sends a read receipt.
let mdn_mimefactory =
crate::mimefactory::MimeFactory::from_mdn(bob, &received_msg, vec![]).await?;
let rendered_mdn = mdn_mimefactory.render(bob).await?;
let mdn_body = rendered_mdn.message;
// Alice receives the read receipt.
receive_imf(alice, mdn_body.as_bytes(), false).await?;
let msg = alice.get_last_msg_in(alice_chat.id).await;
assert_eq!(msg.state, MessageState::OutMdnRcvd);
let ab_contact = alice.add_or_lookup_contact(bob).await;
assert!(ab_contact.is_bot());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_gmx_forwarded_msg() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -3995,7 +4075,7 @@ async fn test_partial_group_consistency() -> Result<()> {
.unwrap();
// Bob receives partial message.
let msg_id = receive_imf_inner(
let msg_id = receive_imf_from_inbox(
&bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\
@@ -4048,7 +4128,7 @@ Chat-Group-Member-Added: charlie@example.com",
assert_eq!(contacts.len(), 3);
// Bob fully reives the partial message.
let msg_id = receive_imf_inner(
let msg_id = receive_imf_from_inbox(
&bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\

View File

@@ -15,7 +15,7 @@ use self::connectivity::ConnectivityStore;
use crate::config::Config;
use crate::contact::{ContactId, RecentlySeenLoop};
use crate::context::Context;
use crate::download::download_msg;
use crate::download::{download_msg, DownloadState};
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::events::EventType;
use crate::imap::{FolderMeaning, Imap};
@@ -70,8 +70,11 @@ impl SchedulerState {
context.new_msgs_notify.notify_one();
let ctx = context.clone();
match Scheduler::start(context).await {
Ok(scheduler) => *inner = InnerSchedulerState::Started(scheduler),
match Scheduler::start(&context).await {
Ok(scheduler) => {
*inner = InnerSchedulerState::Started(scheduler);
context.emit_event(EventType::ConnectivityChanged);
}
Err(err) => error!(&ctx, "Failed to start IO: {:#}", err),
}
}
@@ -116,6 +119,7 @@ impl SchedulerState {
debug_logging.loop_handle.abort();
}
let prev_state = std::mem::replace(&mut *inner, new_state);
context.emit_event(EventType::ConnectivityChanged);
match prev_state {
InnerSchedulerState::Started(scheduler) => scheduler.stop(context).await,
InnerSchedulerState::Stopped | InnerSchedulerState::Paused { .. } => (),
@@ -346,6 +350,16 @@ async fn download_msgs(context: &Context, imap: &mut Imap) -> Result<()> {
for msg_id in msg_ids {
if let Err(err) = download_msg(context, msg_id, imap).await {
warn!(context, "Failed to download message {msg_id}: {:#}.", err);
// Update download state to failure
// so it can be retried.
//
// On success update_download_state() is not needed
// as receive_imf() already
// set the state and emitted the event.
msg_id
.update_download_state(context, DownloadState::Failure)
.await?;
}
context
.sql
@@ -446,6 +460,10 @@ async fn inbox_loop(
warn!(ctx, "Failed to download messages: {:#}", err);
}
if let Err(err) = connection.fetch_metadata(&ctx).await {
warn!(ctx, "Failed to fetch metadata: {err:#}.");
}
fetch_idle(&ctx, &mut connection, FolderMeaning::Inbox).await;
}
};
@@ -459,6 +477,39 @@ async fn inbox_loop(
.await;
}
/// Convert folder meaning
/// used internally by [fetch_idle] and [Context::background_fetch]
pub async fn convert_folder_meaning(
ctx: &Context,
folder_meaning: FolderMeaning,
) -> Result<(Config, String)> {
let folder_config = match folder_meaning.to_config() {
Some(c) => c,
None => {
bail!("Bad folder meaning: {}", folder_meaning);
}
};
let folder = match ctx.get_config(folder_config).await {
Ok(folder) => folder,
Err(err) => {
bail!(
"Can not watch {} folder, failed to retrieve config: {:#}",
folder_config,
err
);
}
};
let watch_folder = if let Some(watch_folder) = folder {
watch_folder
} else {
bail!("Can not watch {} folder, not set", folder_config);
};
Ok((folder_config, watch_folder))
}
/// Implement a single iteration of IMAP loop.
///
/// This function performs all IMAP operations on a single folder, selecting it if necessary and
@@ -466,40 +517,20 @@ async fn inbox_loop(
/// critical operation fails such as fetching new messages fails, connection is reset via
/// `trigger_reconnect`, so a fresh one can be opened.
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: FolderMeaning) {
let folder_config = match folder_meaning.to_config() {
Some(c) => c,
None => {
error!(ctx, "Bad folder meaning: {}", folder_meaning);
let (folder_config, watch_folder) = match convert_folder_meaning(ctx, folder_meaning).await {
Ok(meaning) => meaning,
Err(error) => {
// Warning instead of error because the folder may not be configured.
// For example, this happens if the server does not have Sent folder
// but watching Sent folder is enabled.
warn!(ctx, "Error converting IMAP Folder name: {:?}", error);
connection.connectivity.set_not_configured(ctx).await;
connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
return;
}
};
let folder = match ctx.get_config(folder_config).await {
Ok(folder) => folder,
Err(err) => {
warn!(
ctx,
"Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err
);
connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
return;
}
};
let watch_folder = if let Some(watch_folder) = folder {
watch_folder
} else {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder_config);
connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
return;
};
// connect and fake idle if unable to connect
if let Err(err) = connection
@@ -772,7 +803,7 @@ async fn smtp_loop(
impl Scheduler {
/// Start the scheduler.
pub async fn start(ctx: Context) -> Result<Self> {
pub async fn start(ctx: &Context) -> Result<Self> {
let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (smtp_start_send, smtp_start_recv) = oneshot::channel();
@@ -782,7 +813,7 @@ impl Scheduler {
let mut oboxes = Vec::new();
let mut start_recvs = Vec::new();
let (conn_state, inbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (conn_state, inbox_handlers) = ImapConnectionState::new(ctx).await?;
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
let handle = {
let ctx = ctx.clone();
@@ -803,7 +834,7 @@ impl Scheduler {
),
] {
if should_watch? {
let (conn_state, handlers) = ImapConnectionState::new(&ctx).await?;
let (conn_state, handlers) = ImapConnectionState::new(ctx).await?;
let (start_send, start_recv) = oneshot::channel();
let ctx = ctx.clone();
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));

View File

@@ -10,6 +10,7 @@ use async_smtp::{self as smtp, EmailAddress, SmtpTransport};
use tokio::io::BufStream;
use tokio::task;
use crate::chat::{add_info_msg_with_cmd, ChatId};
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
@@ -26,6 +27,7 @@ use crate::provider::Socket;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str::unencrypted_email;
/// SMTP connection, write and read timeout.
const SMTP_TIMEOUT: Duration = Duration::from_secs(60);
@@ -584,7 +586,46 @@ pub(crate) async fn send_msg_to_smtp(
match status {
SendResult::Retry => {}
SendResult::Success | SendResult::Failure(_) => {
SendResult::Success => {
context
.sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await?;
}
SendResult::Failure(ref err) => {
if err.to_string().contains("Invalid unencrypted mail") {
let res = context
.sql
.query_row_optional(
"SELECT chat_id, timestamp FROM msgs WHERE id=?;",
(msg_id,),
|row| Ok((row.get::<_, ChatId>(0)?, row.get::<_, i64>(1)?)),
)
.await?;
if let Some((chat_id, timestamp_sort)) = res {
let addr = context.get_config(Config::ConfiguredAddr).await?;
let text = unencrypted_email(
context,
addr.unwrap_or_default()
.split('@')
.nth(1)
.unwrap_or_default(),
)
.await;
add_info_msg_with_cmd(
context,
chat_id,
&text,
crate::mimeparser::SystemMessage::InvalidUnencryptedMail,
timestamp_sort,
None,
None,
None,
)
.await?;
};
}
context
.sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
@@ -618,7 +659,7 @@ async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
let more_mdns = send_mdn(context, connection).await?;
if !more_mdns {
// No more MDNs to send.
// No more MDNs to send or one of them failed.
return Ok(());
}
}
@@ -752,7 +793,7 @@ async fn send_mdn_msg_id(
}
}
/// Tries to send a single MDN. Returns false if there are no MDNs to send.
/// Tries to send a single MDN. Returns true if more MDNs should be sent.
async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
if !mdns_enabled {

View File

@@ -419,6 +419,11 @@ pub enum StockMessage {
#[strum(props(fallback = "Member %1$s added."))]
MsgAddMember = 173,
#[strum(props(
fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
))]
InvalidUnencryptedMail = 174,
}
impl StockMessage {
@@ -1285,6 +1290,13 @@ pub(crate) async fn aeap_addr_changed(
.replace3(new_addr)
}
/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
translated(context, StockMessage::InvalidUnencryptedMail)
.await
.replace1(provider)
}
pub(crate) async fn aeap_explanation_and_link(
context: &Context,
old_addr: &str,

View File

@@ -883,7 +883,7 @@ mod tests {
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::contact::Contact;
use crate::receive_imf::{receive_imf, receive_imf_inner};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::test_utils::TestContext;
use crate::{message, sql};
@@ -1216,7 +1216,7 @@ mod tests {
let sent2 = alice.pop_sent_msg().await;
// Bob does not download instance but already receives update
receive_imf_inner(
receive_imf_from_inbox(
&bob,
&alice_instance.rfc724_mid,
sent1.payload().as_bytes(),
@@ -1231,7 +1231,7 @@ mod tests {
assert_eq!(bob_instance.download_state, DownloadState::Available);
// Bob downloads instance, updates should be assigned correctly
let received_msg = receive_imf_inner(
let received_msg = receive_imf_from_inbox(
&bob,
&alice_instance.rfc724_mid,
sent1.payload().as_bytes(),

View File

@@ -17,7 +17,7 @@ Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162][])
Client/server identification | IMAP ID extension ([RFC 2971][])
Authorization | OAuth2 ([RFC 6749][])
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Detect/prevent active attacks | [countermitm][] protocols
Detect/prevent active attacks | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
@@ -29,7 +29,7 @@ Return receipts | Message Disposition Notification (MDN, [RFC 8
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
[Autocrypt Level 1]: https://autocrypt.org/level1.html
[countermitm]: https://countermitm.readthedocs.io/en/latest/
[securejoin]: https://securejoin.readthedocs.io/en/latest/
[openpgp4fpr]: https://metacode.biz/openpgp/openpgp4fpr
[Autodiscover]: https://learn.microsoft.com/en-us/exchange/autodiscover-service-for-exchange-2013
[XEP-0392]: https://xmpp.org/extensions/xep-0392.html

View File

@@ -0,0 +1,66 @@
Return-Path: <37u6sZRoKALMghkXier-lfmi-mel-kXihkmbgZZhhZeX.Vhf@smtp-tls-reporting.bounces.google.com>
Delivered-To: root@nine.testrun.org
Received: from nine.testrun.org
by nine with LMTP
id UB0TOe/urGX8mhwAPdT8mA
(envelope-from <37u6sZRoKALMghkXier-lfmi-mel-kXihkmbgZZhhZeX.Vhf@smtp-tls-reporting.bounces.google.com>)
for <root@nine.testrun.org>; Sun, 21 Jan 2024 11:16:15 +0100
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=google.com; s=20230601; t=1705832174; x=1706436974; darn=nine.testrun.org;
h=to:from:subject:message-id:tls-report-submitter:tls-report-domain
:date:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=ij33gq0ofQz4EhX9TLi6EGnOILUSDSZtwZir1iybY6o=;
b=IQ/TiJ4wMppgECWZQaC8EE3Q3ON4VjB94gp/l4uxL0mSAKo+CeKn+wh5jJooKCN3uZ
wjp9w3+fcQq/3UvvthDzlcBHA2QHXkGC4ONliODX4uaWalRkc21ODHVvx8ILGuAFeKxw
dl6+hn3Qk56FbVdRNBrAKvx7YvJjQHecrO79AoURhcCVfNtqpwPXuok4c/w6TtLhLLsu
pwcPlkAJphdD5hvXLciHjRszvIWOYu9v2G0c4bcCXBRrXhNnIPLl+SO5FAkkyxNOgVmg
EYyBJJoUZH9njyHeazbDnlMCQ5aQBndrfnk357/jSR/Fx6Nti1SvguLUOZbVV2vhh4oD
R6bA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20230601; t=1705832174; x=1706436974;
h=to:from:subject:message-id:tls-report-submitter:tls-report-domain
:date:mime-version:x-gm-message-state:from:to:cc:subject:date
:message-id:reply-to;
bh=ij33gq0ofQz4EhX9TLi6EGnOILUSDSZtwZir1iybY6o=;
b=eYcl9flKTT6YIUTx01fK86e390qJCgQmR9RSDbCGiKTTCzJ1NQn1ev7pUhzOoQL44z
w27ZOgAeiz8eKHxXMQ0DQhjyQ3anHEqej4NJPJ5+7epL8eZQ7QDs2/EQmqJNe9DP4Bd7
sjq2QyUdi2UbU9OrxBL4mRKu8PRZyR4/0cgaJJIgphziUHZBRbfEksl8Ev5XBDMBy11x
1oZZSOmkqK2ujPZZiQrdbqOxWijd4bCpBj5gWH/M9jRI/gHCiIwF+ZaxIXQBoVIhvBKK
t0tADdYcd3qjN2gxr7PO04NaABJgxLGC9YFXH+jRPKdycAvRKwZYpRXuHA2bynvYryk8
MDJg==
X-Gm-Message-State: AOJu0YyM8qPQlVoa6jtAlpPtrku3niz3QTG1nLKht5uRJsjZg5pzwktC
kh/X3YhSj2u0uIzVNqlLH0Lo/XiBUTJbicLQrpfIKD5aeVC8LPhrWBE4mJ4eZ5mYtTLSmgbu3fr
mM4hyzb7+m+pqL0bi2IZTUQh4wbDop+p2LLA99g4Ezji4hkTbMzXy07ekctK/9/bcSBS2
X-Google-Smtp-Source: AGHT+IEPBnMUwx3a4EQI3kJIaLlQcaDz6nx+VMmBsWiYDbqDBgNl26HDryxj5uANI/wyiLBwSQcwyTfmJewf/23eykdQ83fh4kERed6SIB4=
MIME-Version: 1.0
X-Received: by 2002:a05:622a:124f:b0:42a:128:c13c with SMTP id
z15-20020a05622a124f00b0042a0128c13cmr470166qtx.12.1705832174325; Sun, 21 Jan
2024 02:16:14 -0800 (PST)
Date: Sun, 21 Jan 2024 02:16:14 -0800
TLS-Report-Domain: nine.testrun.org
TLS-Report-Submitter: google.com
Message-ID: <000000000000cc154d060f720057@google.com>
Subject: Report Domain: nine.testrun.org Submitter: google.com Report-ID: <2024.01.20T00.00.00Z+nine.testrun.org@google.com>
From: noreply-smtp-tls-reporting@google.com
To: root@nine.testrun.org
Content-Type: multipart/report; boundary="000000000000cc153d060f720056"; report-type=tlsrpt
--000000000000cc153d060f720056
Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes
This is an aggregate TLS report from google.com
--000000000000cc153d060f720056
Content-Type: application/tlsrpt+gzip;
name="google.com!nine.testrun.org!1705708800!1705795199!001.json.gz"
Content-Disposition: attachment;
filename="google.com!nine.testrun.org!1705708800!1705795199!001.json.gz"
Content-Transfer-Encoding: base64
H4sIAAAAAAAAAHVRTWvDMAz9K8HnOrhZx6hPu5Wd21NHKcZRM0MsBVsp6Ur++5R022FrQWB9vKcn
yVdFqXEYPh0HQo0ugrJqQ9S0ULyhL9VC1Y5BJ4eNlK4qs0uspxyHGVyZaqXNUldmZ4ydbS8swPoB
qnqyz2uxvRoXyhOy86wDnkhgOXKnuc06QUeJAzavzTxM6SlK11tah/qB8BEDQsmQOfVYym7C6agN
PkBW9v16Cy7TIjdP86Wb5sucf6AXLWxRFrw6Q8pyGFtsd9vzUhCRarAFyLTJwxQPtrijGd1wdI0g
q9VyXRmjDr/Na4ouoEjeow36gzJPyv+qB7lW7mN0aR6fiV2rc+895HzqxZV3+kNPPUqHl8U35ORC
2yf4WzfjeBi/APFqfhD/AQAA
--000000000000cc153d060f720056--