Compare commits

...
Author SHA1 Message Date
Floris Bruynooghe ce5c42e11e Use anyhow's with_context 2022-02-07 21:41:55 +01:00
Floris Bruynooghe a0df000fb1 small typo 2022-02-07 21:25:49 +01:00
Floris Bruynooghe 8bd58a0d51 Return selected account by ID
Otherwise you do not know which account needs unlocking if the
selected account is locked.
2022-02-07 21:19:48 +01:00
Floris Bruynooghe 2d14f1e187 Fixup naming a bit
Originally I was calling thing "password" a lot, but the consistent
naming is "passphrase" and "encrypted account".
2022-02-07 20:50:50 +01:00
Floris Bruynooghe cd6aba1e57 Missing rustfmt 2022-02-06 22:12:29 +01:00
Floris Bruynooghe 287e291485 Fixup doc links 2022-02-06 22:11:54 +01:00
Floris Bruynooghe 926e3208ef Add changelog 2022-02-06 22:10:01 +01:00
Floris Bruynooghe 059dd54b0e Remove now usused function 2022-02-06 22:07:30 +01:00
Floris Bruynooghe 8bc2ca7f25 Make working with encrypted storage RAII
This refactors the APIs to work with encrypted storage to folow the
Resource Acquisition Is Initialisation principle.  Having our
structures behave like this is beneficial because it reoves a lot of
edge-cases that would need to be handled.
2022-02-06 21:38:17 +01:00
link2xt 2e5e8f73c6 imap: simplify get_quota_roots() 2022-02-06 15:17:05 +00:00
link2xt ada5d38272 imap: remove unwrap() 2022-02-06 14:07:04 +00:00
link2xt c4b0f773db python: remove arbitrary timeouts from tests
pytest-timeout already handles all deadlocks and is configurable with
--timeout option. With this change it is possible to disable timeout
with --timeout 0 to run tests on extremely slow connections.
2022-02-06 12:52:48 +00:00
link2xt 276daf631e imap: move messages in batches
Also change how NO response is treated. NO response means there is an
error moving/copying the messages. When there are no matching
messages, the response is "OK No matching messages, so nothing copied"
according to some RFC 9051 examples.
2022-02-05 22:15:46 +00:00
link2xt fb19b58147 Reduce number of unsafe as conversions
Enable clippy::cast_lossless lint and get rid of
some conversions pointed out by  clippy::as_conversions.
2022-02-05 12:42:14 +00:00
dependabot[bot] 13a5e3cf6f Merge pull request #3055 from deltachat/dependabot/cargo/async-std-resolver-0.20.4 2022-02-04 21:39:59 +00:00
bjoern 1caf3caf1b do set_visibility() in a transaction (#3053)
this avoids archived chats containing fresh messages:

before, it could happen that between the two SQL calls
a new fresh message arrives,
unarchives the chat that is immediately archived by the second SQL call -
resulting in an archive chat containing fresh messages.

as fresh messages counter are shown on app icon etc.
this is pretty weird for the user as they do not see what is "fresh".

the other way round,
there is no transaction in receive_imf(),
however, receive_imf() only unarchives chats,
so that is visible and no big issue for the user.

the issue is rare at all,
however, annoying if you get that as the badge counter may be stuck at "1"
nearly forever (until you open the archived chat in question).
2022-02-03 20:40:24 +01:00
dependabot[bot] 564370f79a cargo: bump async-std-resolver from 0.20.3 to 0.20.4
Bumps [async-std-resolver](https://github.com/bluejekyll/trust-dns) from 0.20.3 to 0.20.4.
- [Release notes](https://github.com/bluejekyll/trust-dns/releases)
- [Changelog](https://github.com/bluejekyll/trust-dns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluejekyll/trust-dns/compare/v0.20.3...v0.20.4)

---
updated-dependencies:
- dependency-name: async-std-resolver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-02 21:14:26 +00:00
bjoern 24e749a2c9 prepare 1.75 (#3049)
* update changelog for 1.75.0

* bump version to 1.75.0
2022-02-01 14:00:40 +01:00
link2xt cccdc51ad4 Optimize delete_expired_imap_messages()
For me this reduced query time from 0.3 s to 0.05 s.
2022-01-31 20:34:01 +00:00
bjoern 99ddce6c3e prepare 1.74 (#3046)
* update changelog for 1.74.0

* bump version to 1.74.0
2022-01-31 19:53:50 +01:00
link2xt f68088cfb5 imap: avoid reconnection loop when message without Message-ID is marked as seen
- do not attempt to mark reserved meessages as seen when
  messages with empty Message-ID are marked as seen on IMAP
- do not reconnect on Seen flag synchronization failures This avoid
  reconnection loops in case of permanent errors in `sync_seen_flags`
2022-01-31 00:00:00 +00:00
Hocuri c8f56d748a Only fetch mvbox deltachat.h additions (#3045)
* Use the formatting of the rest of the file

* Add changes require restarting IO by calling dc_stop_io() and then dc_start_io(). comment
2022-01-31 17:46:18 +01:00
bjoern a43fc47bb6 update provider database (#3043)
* update provider database

ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`

* update changelog
2022-01-31 16:07:20 +01:00
bjoern 8c1bfac53b prepare 1.73 (#3042)
* update changelog for 1.73.0

* bump version to 1.73.0
2022-01-31 15:12:44 +01:00
Floris BruynoogheandHocuri 97853c3660 Flub/watch mvbox only (#3028)
* Make set_config() look a bit nicer

* Add OnlyFetchMvbox option

* Add test for the config

* Add option to only watch mvbox

This is supposed to support having a server-side rule which moves
emails to the mvbox already.  The new option makes sure the mvbox is
wathched and also makes sure no messages are feched from folders other
than the mvbox and the spam folder if enabled.  It does not interact
with the other settings.

* Fixup ignore conditions

* Cleanup some bits

* Watch the mvbox when `WatchMvboxOnly` is set

* Rename back to only_fetch_mvbox (flub said it's OK for him)

* typo

* clippy, more typos

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-01-31 13:39:48 +01:00
link2xt f304a30193 imap: fetch Inbox before scanning other folders 2022-01-31 12:03:21 +01:00
link2xt 7eadca3959 imap: do not synchronize Seen flags on unwatched folders
Synchronizing seen flags doubles the time required to scan all
folders. Delta Chat only marks messages as Seen on Inbox or DeltaChat,
so there is no need to check for Seen flag on other folders.
2022-01-30 20:00:00 +00:00
link2xt 8aa6decbf9 imap: call delete_expired_imap_messages() less often
This operation takes roughly 0.3 s on a moderate size database.
Calling it once before scanning all folders and scanning
the watched folder instead of each time after downloading
a message from a folder speeds up IMAP loop.
2022-01-30 20:49:32 +00:00
link2xt 7cf4bcaca2 imap: call delete_expired_imap_messages() less often
This operation takes roughly 0.3 s on a moderate size database.
Calling it once before scanning all folders and scanning
the watched folder instead of each time after downloading
a message from a folder speeds up IMAP loop.
2022-01-30 20:47:32 +00:00
dependabot[bot] 9ccd9c3e0e Merge pull request #3011 from deltachat/dependabot/cargo/serde-1.0.136 2022-01-30 20:13:39 +00:00
dependabot[bot] c6773a6303 Merge pull request #3020 from deltachat/dependabot/cargo/backtrace-0.3.64 2022-01-30 20:13:20 +00:00
link2xt e858a32aa1 smtp: cancel message sending by removing the message
This restores the logic removed in
afd8c0d879
2022-01-30 10:59:10 +00:00
B. Petersen 99f2680e2c fix splitting off text from webxdc messages
moreover, make the split check exhaustive
to avoid the same error on the next added Viewtype.
2022-01-30 11:49:09 +01:00
B. Petersen 7a9a323bac test sending webxdc+text 2022-01-30 11:49:09 +01:00
dependabot[bot] 62aa234352 cargo: bump backtrace from 0.3.63 to 0.3.64
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.63 to 0.3.64.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.63...0.3.64)

---
updated-dependencies:
- dependency-name: backtrace
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-30 09:54:28 +00:00
link2xt 0cb9e7922a Remove direct dependency on byteorder crate 2022-01-29 23:24:25 +00:00
link2xt e73107006e smtp: replace thiserror with anyhow 2022-01-29 16:41:47 +00:00
link2xt ca389cc6fc Move webxdc change to Unreleased 2022-01-29 16:41:04 +00:00
link2xt 60ec7f0cbf Move last changelog entry to Unreleased 2022-01-29 16:39:28 +00:00
B. Petersen d342d59e65 use webxdc app name in chatlist/quotes/replies/etc
this uses `get_webxdc_info().name` for chatlist etc.
the previuosly used static strings comes from a time
where we just did not had the correct name.

i was also thinking about adding `get_webxdc_info().summary`,
however, as this information is dynamic,
that may open several issues, eg. quoted text may change
so that the answer is out of context.
2022-01-29 16:48:24 +01:00
link2xt 2690fa2da5 Don't watch Sent folder by default 2022-01-29 11:35:02 +00:00
B. Petersen e411c394ca add a link to search for #webxdc on github to the webxdc-docs 2022-01-29 00:42:56 +01:00
B. Petersen d69f3ba225 adapt draft to new api 2022-01-28 21:10:34 +01:00
B. Petersen 739807b1a9 add links to webxdc development tool, simulator and to advanced examples 2022-01-28 17:42:32 +01:00
dependabot[bot] d029ea7f3f cargo: bump mailparse from 0.13.7 to 0.13.8
Bumps [mailparse](https://github.com/staktrace/mailparse) from 0.13.7 to 0.13.8.
- [Release notes](https://github.com/staktrace/mailparse/releases)
- [Commits](https://github.com/staktrace/mailparse/compare/v0.13.7...v0.13.8)

---
updated-dependencies:
- dependency-name: mailparse
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-27 21:27:05 +01:00
dependabot[bot] 11098cb869 cargo: bump libc from 0.2.113 to 0.2.114
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.113 to 0.2.114.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.113...0.2.114)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-27 21:26:23 +01:00
Hocuri f6807d6b22 Fix set_config_bool() (#3013) 2022-01-27 12:12:16 +01:00
dependabot[bot] 7fc9bacf54 cargo: bump serde from 1.0.135 to 1.0.136
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.135 to 1.0.136.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.135...v1.0.136)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-26 21:14:08 +00:00
33 changed files with 1212 additions and 795 deletions
+50
View File
@@ -1,5 +1,55 @@
# Changelog
## Unreleased
### Changes
- refactorings #3026
- move messages in batches #3058
- working with encrypted storage drastically changed to be RAII #3063
### Fixes
- avoid archived, fresh chats #3053
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
## 1.75.0
### Changes
- optimize `delete_expired_imap_messages()` #3047
## 1.74.0
### Fixes
- avoid reconnection loop when message without Message-ID is marked as seen #3044
## 1.73.0
### API changes
- added `only_fetch_mvbox` config #3028
### Changes
- don't watch Sent folder by default #3025
- use webxdc app name in chatlist/quotes/replies etc. #3027
- make it possible to cancel message sending by removing the message #3034,
this was previosuly removed in 1.71.0 #2939
- synchronize Seen flags only on watched folders to speed up
folder scanning #3041
- remove direct dependency on `byteorder` crate #3031
- refactorings #3023 #3013
- update provider database #3043
- improve documentation #3017 #3018 #3021
### Fixes
- fix splitting off text from webxdc messages #3032
- call slow `delete_expired_imap_messages()` less often #3037
- make synchronization of Seen status more robust in case unsolicited FETCH
result without UID is returned #3022
- fetch Inbox before scanning folders to ensure iOS does
not kill the app before it gets to fetch the Inbox in background #3040
## 1.72.0
### Fixes
Generated
+19 -20
View File
@@ -347,9 +347,9 @@ dependencies = [
[[package]]
name = "async-std-resolver"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed4e2c3da14d8ad45acb1e3191db7a918e9505b6f155b218e70a7c9a1a48c638"
checksum = "dbf3e776afdf3a2477ef4854b85ba0dff3bd85792f685fb3c68948b4d304e4f0"
dependencies = [
"async-std",
"async-trait",
@@ -420,9 +420,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.63"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
dependencies = [
"addr2line",
"cc",
@@ -1063,7 +1063,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.72.0"
version = "1.75.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1077,7 +1077,6 @@ dependencies = [
"backtrace",
"base64 0.13.0",
"bitflags",
"byteorder",
"chrono",
"criterion",
"deltachat_derive",
@@ -1144,7 +1143,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.72.0"
version = "1.75.0"
dependencies = [
"anyhow",
"async-std",
@@ -2122,9 +2121,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.113"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9"
checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
[[package]]
name = "libm"
@@ -2185,12 +2184,12 @@ dependencies = [
[[package]]
name = "mailparse"
version = "0.13.7"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d70ae0840b192a2f7d1dc46e75f38720a7e3c52dfdc968ba3202fa270668dc67"
checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32"
dependencies = [
"base64 0.13.0",
"charset",
"data-encoding",
"quoted_printable",
]
@@ -3301,9 +3300,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.135"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
@@ -3320,9 +3319,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.135"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
@@ -3871,9 +3870,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0d7f5db438199a6e2609debe3f69f808d074e0a2888ee0bccb45fe234d03f4"
checksum = "ca94d4e9feb6a181c690c4040d7a24ef34018d8313ac5044a61d21222ae24e31"
dependencies = [
"async-trait",
"cfg-if 1.0.0",
@@ -3895,9 +3894,9 @@ dependencies = [
[[package]]
name = "trust-dns-resolver"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ad17b608a64bd0735e67bde16b0636f8aa8591f831a25d18443ed00a699770"
checksum = "ecae383baad9995efaa34ce8e57d12c3f305e545887472a492b838f4b5cfb77a"
dependencies = [
"cfg-if 1.0.0",
"futures-util",
+1 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.72.0"
version = "1.75.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -30,7 +30,6 @@ async-trait = "0.1"
backtrace = "0.3"
base64 = "0.13"
bitflags = "1.3"
byteorder = "1.3"
chrono = "0.4"
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.72.0"
version = "1.75.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
+87 -54
View File
@@ -179,10 +179,12 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
// create/open/config/information
/**
* Create a new context object and try to open it without passphrase. If
* database is encrypted, the result is the same as using
* dc_context_new_closed() and the database should be opened with
* dc_context_open() before using.
* Create a new context object and try to open it without passphrase.
*
* If database is encrypted NULL is returned, encrypted databases should be
* opened using dc_context_new_encrypted().
*
* If the database does not yet exist a new one will be created.
*
* @memberof dc_context_t
* @param os_name Deprecated, pass NULL or empty string here.
@@ -192,54 +194,34 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
* On failure NULL is returned.
*
* If you want to use multiple context objects at the same time,
* this can be managed using dc_accounts_t.
*/
dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir);
/**
* Create a new context object. After creation it is usually opened with
* dc_context_open() and started with dc_start_io() so it is connected and
* mails are fetched.
* Create a new context object and try to open it with a passphrase.
*
* If the database does not yet exist a new one will be created. If the
* database is not encrypted this will fail.
*
* @memberof dc_context_t
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @param passphrase The passphrase to use. This MUST be non-NULL and MUST be valid
* UTF-8, if either of this is not true this fails and NULL is retruned.
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
* On failure NULL is returned.
*
* If you want to use multiple context objects at the same time,
* this can be managed using dc_accounts_t.
*/
dc_context_t* dc_context_new_closed (const char* dbfile);
/**
* Opens the database with the given passphrase. This can only be used on
* closed context, such as created by dc_context_new_closed(). If the database
* is new, this operation sets the database passphrase. For existing databases
* the passphrase should be the one used to encrypt the database the first
* time.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The passphrase to use with the database. Pass NULL or
* empty string to use no passphrase and no encryption.
* @return 1 if the database is opened with this passphrase, 0 if the
* passphrase is incorrect and on error.
*/
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Returns 1 if database is open.
*
* @memberof dc_context_t
* @param context The context object.
* @return 1 if database is open, 0 if database is closed
*/
int dc_context_is_open (dc_context_t *context);
dc_context_t* dc_context_new_encrypted (const char* dbfile, const char* passphrase);
/**
* Free a context object.
@@ -335,14 +317,20 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1=send a copy of outgoing messages to self.
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes (default),
* 0=do not watch the `Sent`-folder,
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
* 0=do not watch the `Sent`-folder (default),
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `mvbox_move` = 1=detect chat messages,
* move them to the `DeltaChat` folder,
* and watch the `DeltaChat` folder for updates (default),
* 0=do not move chat-messages
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
* `DeltaChat` folder. Messages will still be fetched from the
* spam folder and `sendbox_watch` will also still be respected
* if enabled.
* 0=watch all folders normally (default)
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only (default),
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
@@ -2631,21 +2619,22 @@ void dc_accounts_unref (dc_accounts_t* accounts);
uint32_t dc_accounts_add_account (dc_accounts_t* accounts);
/**
* Add a new closed account to the account manager.
* Internally, dc_context_new_closed() is called using a unique database-name
* in the directory specified at dc_accounts_new().
* Add a new account to the account manager, encrypted with a passphrase.
*
* If the function succeeds,
* dc_accounts_get_all() will return one more account
* and you can access the newly created account using dc_accounts_get_account().
* Moreover, the newly created account will be the selected one.
* The account's database will be encrypted with a key derived from the
* passphrase and will need to be opened with the passphrase to be usable.
*
* Encrypted accounts are not automatically opened when the account
* manager is created. They need to be opened using dc_accounts_load_encrypted(),
* to find unloaded encrypted accounts use dc_accounts_get_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return Account-id, use dc_accounts_get_account() to get the context object.
* @return Account-id, use dc_accounts_get_encrypted_account() to get the
* context object.
* On errors, 0 is returned.
*/
uint32_t dc_accounts_add_closed_account (dc_accounts_t* accounts);
uint32_t dc_accounts_add_encrypted_account(dc_accounts_t* accounts, char* passphrase);
/**
* Migrate independent accounts into accounts managed by the account manager.
@@ -2681,6 +2670,10 @@ int dc_accounts_remove_account (dc_accounts_t* accounts, uint32
/**
* List all accounts.
*
* Only lists loaded accounts, some accounts might be encrypted and need to be
* explicitly loaded using dc_accounts_get_encrypted() and
* dc_accounts_load_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return An array containing all account-ids,
@@ -2688,10 +2681,27 @@ int dc_accounts_remove_account (dc_accounts_t* accounts, uint32
*/
dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
/**
* Lists encrypted accounts.
*
* This returns all *locked* encrypted accounts which must be activated using
* dc_accounts_load_encrypted(). Once an encrypted account has been loaded it will
* show up in dc_accounts_get_all() and can be retrieved using
* dc_accounts_get_account().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return An array containing account-ids, use dc_array_get_id() to get the ids.
*/
dc_array_t* dc_accounts_get_encrypted (dc_accounts_t* accounts);
/**
* Get an account-context from an account-id.
*
* Only accounts returned by dc_accounts_get_all() can be retrieved with this
* function, encrypted accounts first need to be loaded using
* dc_accounts_load_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_get_all() or dc_accounts_add_account().
@@ -2702,9 +2712,30 @@ dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
*/
dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* Loads an encrypted account into the Account manager.
*
* Once the account has been loaded you can also access it using
* dc_accounts_get_account() like any other account. Use *dc_accounts_get_encrypted()
* to find accounts which need to be loaded like this.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_get_encrypted(),
* dc_accounts_add_encrypted_account().
* @param passphrase The passphrase to decrypt the account with.
* @return The account-context, this can be used most similar as a normal,
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* On failure NULL is returned.
*/
dc_context_t* dc_accounts_load_encrypted (dc_accounts_t* accounts, uint32_t account_id, char* passphrase);
/**
* Get the currently selected account.
* Get the currently selected account ID.
*
* If there is at least one account in the account-manager,
* there is always a selected one.
* To change the selected account, use dc_accounts_select_account();
@@ -2712,13 +2743,15 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return The account-context, this can be used most similar as a normal,
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* If there is no selected account, NULL is returned.
* @return The account ID of the selected account. The context of
* the selected account can then be retrieved using dc_accounts_get_account().
* Note that if the selected account is encrypted you may first have to
* load it using dc_accounts_load_encrypted(), you can verify if this is needed
* using dc_accounts_get_encrypted().
* If no account is selected 0 is returned. However this is only possible if
* there is no single account in the account manager.
*/
dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
uint32_t* dc_accounts_get_selected_account_id (dc_accounts_t* accounts);
/**
+101 -51
View File
@@ -16,6 +16,7 @@ extern crate serde_json;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::ffi::CStr;
use std::fmt::Write;
use std::ops::Deref;
use std::ptr;
@@ -94,20 +95,33 @@ pub unsafe extern "C" fn dc_context_new(
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *mut dc_context_t {
pub unsafe extern "C" fn dc_context_new_encrypted(
dbfile: *const libc::c_char,
passphrase: *const libc::c_char,
) -> *mut dc_context_t {
setup_panic!();
if dbfile.is_null() {
eprintln!("ignoring careless call to dc_context_new_closed()");
if dbfile.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_context_new_encrypted()");
return ptr::null_mut();
}
// Generate random ID if dc_accounts_t is not used.
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile).to_path_buf().into(),
let dbfile = as_path(dbfile).to_path_buf();
let cstr = CStr::from_ptr(passphrase);
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
eprintln!("passphrase was not UTF-8: {:#}", err);
return ptr::null_mut();
}
};
let ctx = block_on(Context::new_encrypted(
dbfile.into(),
id,
)) {
Ok(context) => Box::into_raw(Box::new(context)),
passphrase.to_string(),
));
match ctx {
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
@@ -115,35 +129,6 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_open(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_open()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.open(passphrase))
.log_err(ctx, "dc_context_open() failed")
.map(|b| b as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_is_open()");
return 0;
}
let ctx = &*context;
block_on(ctx.is_open()) as libc::c_int
}
/// Release the context structure.
///
/// This function releases the memory of the `dc_context_t` structure.
@@ -4083,18 +4068,17 @@ pub unsafe extern "C" fn dc_accounts_get_account(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_selected_account(
pub unsafe extern "C" fn dc_accounts_get_selected_account_id(
accounts: *mut dc_accounts_t,
) -> *mut dc_context_t {
) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
return ptr::null_mut();
return 0;
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.get_selected_account().await })
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
block_on(async move { accounts.read().await.get_selected_account_id().await })
.unwrap_or(0)
}
#[no_mangle]
@@ -4148,17 +4132,29 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
pub unsafe extern "C" fn dc_accounts_add_encrypted_account(
accounts: *mut dc_accounts_t,
passphrase: *const libc::c_char,
) -> u32 {
if accounts.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_with_password()");
return 0;
}
let accounts = &mut *accounts;
let accounts: &AccountsWrapper = &mut *accounts;
let cstr = CStr::from_ptr(passphrase);
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.add_closed_account().await {
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Passphrase was not UTF-8: {:#}",
err
)));
return 0;
}
};
match accounts.add_encrypted_account(passphrase.to_string()).await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
@@ -4243,6 +4239,60 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_encrypted(
accounts: *mut dc_accounts_t,
) -> *mut dc_array_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_all()");
return ptr::null_mut();
}
let accounts: &AccountsWrapper = &*accounts;
let list = block_on(async move { accounts.read().await.get_encrypted() });
let array: dc_array_t = list.into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_load_encrypted(
accounts: *mut dc_accounts_t,
account_id: u32,
passphrase: *const libc::c_char,
) -> *mut dc_context_t {
if accounts.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_context_new_encrypted()");
return ptr::null_mut();
}
let accounts: &AccountsWrapper = &*accounts;
let cstr = CStr::from_ptr(passphrase);
block_on(async move {
let mut accounts = accounts.write().await;
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Passphrase was not UTF-8: {:#}",
err
)));
return ptr::null_mut();
}
};
match accounts
.load_encrypted_account(account_id, passphrase.to_string())
.await
{
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to load encrypted account: {:#}",
err
)));
ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
if accounts.is_null() {
+34 -14
View File
@@ -68,38 +68,46 @@ The callback is called for updates sent by you or other peers.
### getAllUpdates()
```
payloads = window.webxdc.getAllUpdates()
```js
updates = await window.webxdc.getAllUpdates();
```
In case your Webxdc was just started,
you may want to reconstruct the state from the last run -
and also incorporate updates that may have arrived while the app was not running.
- `payloads`: the function returns all previous updates in an array,
- `updates`: All previous updates in an array,
eg. `[{payload: "foo"},{payload: "bar"}]`
if `webxdc.sendUpdate("foo"); webxdc.sendUpdate("bar");` was called on the last run.
if `webxdc.sendUpdate({payload: "foo"}); webxdc.sendUpdate({payload: "bar"};` was called on the last run.
### selfAddr()
The updates are wrapped into a Promise that you can `await` for.
If you are not in an async function and cannot use `await` therefore,
you can get the updates with `then()`:
```js
addr = window.webxdc.selfAddr()
window.webxdc.getAllUpdates().then(updates => {});
```
Returns the peer's own address.
### selfAddr
```js
window.webxdc.selfAddr
```
Property with the peer's own address.
This is esp. useful if you want to differ between different peers -
just send the address along with the payload,
and, if needed, compare the payload addresses against selfAddr() later on.
### selfName()
### selfName
```js
addr = window.webxdc.selfName()
window.webxdc.selfName
```
Returns the peer's own name.
Property with the peer's own name.
This is name chosen by the user in their settings,
if there is nothing set, that defaults to the peer's address.
@@ -128,7 +136,7 @@ round corners etc. will be added by the implementations as needed.
If no icon is set, a default icon will be used.
## Webxdc Example
## Webxdc Examples
The following example shows an input field and every input is show on all peers.
@@ -155,14 +163,26 @@ The following example shows an input field and every input is show on all peers
}
window.webxdc.setUpdateListener(receiveUpdate);
window.webxdc.getAllUpdates().forEach(receiveUpdate);
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
</script>
</body>
</html>
```
For a more advanved example, see https://github.com/r10s/webxdc-poll/ .
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
You can also use that repository as a template for your own app -
just clone and start adapting things to your need.
### Advanced Examples
- [2048](https://github.com/adbenitez/2048.xdc)
- [Draw](https://github.com/adbenitez/draw.xdc)
- [Poll](https://github.com/r10s/webxdc-poll/)
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc)
## Closing Remarks
+1 -3
View File
@@ -224,9 +224,7 @@ class DirectImap:
""" (blocking) wait for next idle message from server. """
assert self._idling
self.account.log("imap-direct: calling idle_check")
res = self.conn.idle_check(timeout=30)
if len(res) == 0:
raise TimeoutError
res = self.conn.idle_check()
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))
+2 -3
View File
@@ -241,7 +241,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
@@ -483,7 +482,7 @@ class BotProcess:
def kill(self) -> None:
self.popen.kill()
def wait(self, timeout=30) -> None:
def wait(self, timeout=None) -> None:
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
@@ -492,7 +491,7 @@ class BotProcess:
print("+++FNMATCH:", next_pattern)
ignored = []
while 1:
line = self.stdout_queue.get(timeout=15)
line = self.stdout_queue.get()
if line is None:
if ignored:
print("BOT stdout terminated after these lines")
+17 -15
View File
@@ -652,8 +652,6 @@ class TestOnlineAccount:
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
# rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(240)
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -892,11 +890,11 @@ class TestOnlineAccount:
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message2")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message3")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_forward_messages(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1834,7 +1832,6 @@ class TestOnlineAccount:
lp.sec("trigger ac setup message and return setupcode")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
setup_code = ac1.initiate_key_transfer()
ac2._evtracker.set_timeout(30)
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.is_setup_message()
@@ -1851,7 +1848,6 @@ class TestOnlineAccount:
def test_ac_setup_message_twice(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1)
ac2._evtracker.set_timeout(30)
acfactory.wait_configure_and_start_io()
lp.sec("trigger ac setup message but ignore")
@@ -2025,7 +2021,7 @@ class TestOnlineAccount:
lp.sec("ac1: send a message to group chat to promote the group")
chat.send_text("afterwards promoted")
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == \
@@ -2035,29 +2031,29 @@ class TestOnlineAccount:
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
contact2 = chat.add_contact("devnull@testrun.org")
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == "devnull@testrun.org"
lp.sec("ac1: remove address2")
chat.remove_contact(contact2)
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "removed"
assert ev.contact.addr == contact2.addr
assert ev.message.get_sender_contact().addr == ac1_addr
lp.sec("ac1: remove ac2 contact from chat")
chat.remove_contact(ac2)
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
@@ -2682,7 +2678,13 @@ class TestOnlineAccount:
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
ac1.direct_imap.idle_check(terminate=True)
while True:
if len(ac1.direct_imap.idle_check(terminate=True)) > 1:
# If length is 1, it's [(b'OK', b'Still here')]
# Could happen on very slow network.
#
# More is usually [(1, b'EXISTS'), (1, b'RECENT')]
break
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
lp.sec("Everything prepared, now see if DeltaChat finds the message (" + variant + ")")
+121 -32
View File
@@ -1,6 +1,6 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use async_std::channel::{self, Receiver, Sender};
use async_std::fs;
@@ -12,7 +12,7 @@ use uuid::Uuid;
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::context::{Context, ContextError};
use crate::events::{Event, EventType, Events};
/// Account manager, that can handle multiple accounts in a single place.
@@ -50,7 +50,7 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
pub async fn open(dir: PathBuf) -> Result<Self> {
async fn open(dir: PathBuf) -> Result<Self> {
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
@@ -91,6 +91,9 @@ impl Accounts {
}
/// Get the currently selected account.
///
/// If the selected account is encrypted and not yet loaded using
/// [`Accounts::load_encrypted_account`] `None` will be returned.
pub async fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account().await;
self.accounts.get(&id).cloned()
@@ -124,17 +127,41 @@ impl Accounts {
Ok(account_config.id)
}
/// Adds a new closed account.
pub async fn add_closed_account(&mut self) -> Result<u32> {
/// Adds an new encrypted account and opens it.
///
/// Creates a new account with encrypted database using the provided password. Returns
/// the account ID of the opened account.
pub async fn add_encrypted_account(&mut self, passphrase: String) -> Result<u32> {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new_closed(account_config.dbfile().into(), account_config.id).await?;
let ctx = Context::new_encrypted(
account_config.dbfile().into(),
account_config.id,
passphrase,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
/// Decrypts and open an existing account.
pub async fn load_encrypted_account(&mut self, id: u32, passphrase: String) -> Result<Context> {
let account_config = self
.config
.get_account(id)
.await
.with_context(|| format!("No such account with id {}", id))?;
let ctx = Context::new_encrypted(
account_config.dbfile().into(),
account_config.id,
passphrase,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(id, ctx.clone());
Ok(ctx)
}
/// Remove an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self.accounts.remove(&id);
@@ -228,6 +255,21 @@ impl Accounts {
self.accounts.keys().copied().collect()
}
/// Returns all encrypted accounts.
///
/// Note that we can't really distinguish between unreadable/corrupted accounts and
/// encrypted accounts. We consider all known accounts which failed to load encrypted,
/// they can be loaded using [`Accounts::load_encrypted_account`].
pub fn get_encrypted(&self) -> Vec<u32> {
let configured_ids: BTreeSet<u32> = self
.config
.all_configured_accounts()
.map(|cfg| cfg.id)
.collect();
let loaded_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
configured_ids.difference(&loaded_ids).copied().collect()
}
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
///
/// Returns whether all accounts finished their background work.
@@ -400,19 +442,29 @@ impl Config {
Ok(Config { file, inner })
}
/// Returns all account configurations.
fn all_configured_accounts(&self) -> impl Iterator<Item = &AccountConfig> {
self.inner.accounts.iter()
}
/// Loads all unencrypted accounts.
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(account_config.dbfile().into(), account_config.id)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
accounts.insert(account_config.id, ctx);
match Context::new(account_config.dbfile().into(), account_config.id).await {
Ok(ctx) => {
accounts.insert(account_config.id, ctx);
}
Err(ContextError::WrongKey) => {
continue;
}
Err(ContextError::Other(err)) => {
return Err(err.context(format!(
"failed to create context from file {}",
account_config.dbfile().display()
)));
}
}
}
Ok(accounts)
@@ -746,36 +798,73 @@ mod tests {
assert_eq!(accounts.accounts.len(), 0);
let account_id = accounts
.add_closed_account()
.add_encrypted_account("foobar".to_string())
.await
.context("failed to add closed account")?;
.context("failed to add encrypted account")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.id, account_id);
let passphrase_set_success = account
.open("foobar".to_string())
.await
.context("failed to set passphrase")?;
assert!(passphrase_set_success);
drop(accounts);
let accounts = Accounts::new(p.clone())
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create second accounts manager")?;
assert!(accounts.get_selected_account().await.is_none());
let id = accounts
.get_selected_account_id()
.await
.context("failed to get selected account id")?;
// Try wrong passphrase
assert!(accounts
.load_encrypted_account(id, "barfoo".to_string())
.await
.is_err());
let loaded_account = accounts
.load_encrypted_account(id, "foobar".to_string())
.await
.context("failed to load encrypted account")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.is_open().await, false);
assert_eq!(loaded_account.id, account.id);
// Try wrong passphrase.
assert_eq!(account.open("barfoo".to_string()).await?, false);
assert_eq!(account.open("".to_string()).await?, false);
Ok(())
}
assert_eq!(account.open("foobar".to_string()).await?, true);
assert_eq!(account.is_open().await, true);
#[async_std::test]
async fn test_get_encrypted() -> Result<()> {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create accounts manager")?;
let account_id = accounts
.add_encrypted_account("secret".to_string())
.await
.context("failed to add encrypted account")?;
drop(accounts);
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create second accounts manager")?;
let encrypted_ids = accounts.get_encrypted();
assert_eq!(vec![account_id], encrypted_ids);
for id in encrypted_ids {
let res = accounts
.load_encrypted_account(id, "secret".to_string())
.await;
assert!(res.is_ok());
}
let encrypted_ids = accounts.get_encrypted();
assert!(encrypted_ids.is_empty());
Ok(())
}
+15 -16
View File
@@ -474,22 +474,21 @@ impl ChatId {
self
);
if visibility == ChatVisibility::Archived {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)
.await?;
}
context
.sql
.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
)
.transaction(move |transaction| {
if visibility == ChatVisibility::Archived {
transaction.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)?;
}
transaction.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
)?;
Ok(())
})
.await?;
context.emit_event(EventType::MsgsChanged {
@@ -961,7 +960,7 @@ impl std::fmt::Display for ChatId {
/// well as query for a [ChatId].
impl rusqlite::types::ToSql for ChatId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -971,7 +970,7 @@ impl rusqlite::types::ToSql for ChatId {
impl rusqlite::types::FromSql for ChatId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= std::u32::MAX as i64 {
if 0 <= val && val <= i64::from(std::u32::MAX) {
Ok(ChatId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))
+32 -13
View File
@@ -65,7 +65,7 @@ pub enum Config {
#[strum(props(default = "1"))]
MdnsEnabled,
#[strum(props(default = "1"))]
#[strum(props(default = "0"))]
SentboxWatch,
#[strum(props(default = "1"))]
@@ -74,6 +74,13 @@ pub enum Config {
#[strum(props(default = "0"))]
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
/// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
///
/// This will not entirely disable other folders, e.g. the spam folder will also still
/// be watched for new messages.
#[strum(props(default = "0"))]
OnlyFetchMvbox,
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
@@ -225,6 +232,11 @@ impl Context {
Ok(self.get_config_int(key).await? != 0)
}
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -233,7 +245,7 @@ impl Context {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
x => Ok(Some(i64::from(x))),
}
}
@@ -255,7 +267,7 @@ impl Context {
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(x as i64)),
x => Ok(Some(i64::from(x))),
}
}
@@ -281,35 +293,29 @@ impl Context {
}
}
self.emit_event(EventType::SelfavatarChanged);
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
let ret = self.sql.set_raw_config(key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
ret?
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
Ok(())
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, if value { Some("1") } else { None })
self.set_config(key, if value { Some("1") } else { Some("0") })
.await?;
Ok(())
}
@@ -401,4 +407,17 @@ mod tests {
Ok(())
}
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
#[async_std::test]
async fn test_set_config_bool() -> Result<()> {
let t = TestContext::new().await;
// We need some config that defaults to true
let c = Config::E2eeEnabled;
assert_eq!(t.get_config_bool(c).await?, true);
t.set_config_bool(c, false).await?;
assert_eq!(t.get_config_bool(c).await?, false);
Ok(())
}
}
+1 -1
View File
@@ -443,7 +443,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxMove).await?;
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, create_mvbox).await?;
+83 -110
View File
@@ -5,7 +5,7 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::time::{Instant, SystemTime};
use anyhow::{bail, ensure, Result};
use anyhow::{bail, Context as _, Result};
use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
@@ -25,7 +25,7 @@ use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
use crate::sql::{Sql, SqlOpenError};
#[derive(Clone, Debug)]
pub struct Context {
@@ -103,74 +103,62 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
res
}
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("wrong passphrase or unencrypted context")]
WrongKey,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<SqlOpenError> for ContextError {
fn from(source: SqlOpenError) -> Self {
match source {
SqlOpenError::WrongKey => Self::WrongKey,
SqlOpenError::Other(err) => Self::Other(err),
}
}
}
impl Context {
/// Creates new context and opens the database.
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
let context = Self::new_closed(dbfile, id).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
context.sql.open(&context, "".to_string()).await?;
}
Ok(context)
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context, ContextError> {
Context::new_common(dbfile, id, None).await
}
/// Creates new context without opening the database.
pub async fn new_closed(dbfile: PathBuf, id: u32) -> Result<Context> {
pub async fn new_encrypted(
dbfile: PathBuf,
id: u32,
passphrase: String,
) -> Result<Context, ContextError> {
Context::new_common(dbfile, id, Some(passphrase)).await
}
async fn new_common(
dbfile: PathBuf,
id: u32,
passphrase: Option<String>,
) -> Result<Context, ContextError> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
let blobdir = dbfile.with_file_name(blob_fname);
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
async_std::fs::create_dir_all(&blobdir)
.await
.context("Failed to create blobdir")?;
}
let context = Context::with_blobdir(dbfile, blobdir, id).await?;
Ok(context)
}
/// Opens the database with the given passphrase.
///
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
/// errors.
pub async fn open(&self, passphrase: String) -> Result<bool> {
if self.sql.check_passphrase(passphrase.clone()).await? {
self.sql.open(self, passphrase).await?;
Ok(true)
} else {
Ok(false)
}
}
/// Returns true if database is open.
pub async fn is_open(&self) -> bool {
self.sql.is_open().await
}
/// Tests the database passphrase.
///
/// Returns true if passphrase is correct.
///
/// Fails if database is already open.
pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
self.sql.check_passphrase(passphrase).await
}
pub(crate) async fn with_blobdir(
dbfile: PathBuf,
blobdir: PathBuf,
id: u32,
) -> Result<Context> {
ensure!(
blobdir.is_dir().await,
"Blobdir does not exist: {}",
blobdir.display()
);
let sql = match passphrase {
Some(passphrase) => Sql::new_encrypted(dbfile, passphrase).await?,
None => Sql::new(dbfile).await?,
};
let inner = InnerContext {
id,
blobdir,
running_state: RwLock::new(Default::default()),
sql: Sql::new(dbfile),
sql,
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
@@ -189,6 +177,7 @@ impl Context {
let ctx = Context {
inner: Arc::new(inner),
};
ctx.sql.run_migrations(&ctx).await?;
Ok(ctx)
}
@@ -358,6 +347,7 @@ impl Context {
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
let folders_configured = self
.sql
.get_raw_config_int("folders_configured")
@@ -422,6 +412,7 @@ impl Context {
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("sentbox_move", sentbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
@@ -678,21 +669,16 @@ mod tests {
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::Message;
use crate::test_utils::TestContext;
use anyhow::Context as _;
use std::time::Duration;
use strum::IntoEnumIterator;
use tempfile::tempdir;
#[async_std::test]
async fn test_wrong_db() -> Result<()> {
let tmp = tempfile::tempdir()?;
async fn test_wrong_db() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123")?;
let res = Context::new(dbfile.into(), 1).await?;
// Broken database is indistinguishable from encrypted one.
assert_eq!(res.is_open().await, false);
Ok(())
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_err());
}
#[async_std::test]
@@ -847,16 +833,6 @@ mod tests {
assert!(blobdir.is_dir());
}
#[async_std::test]
async fn test_wrong_blogdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_err());
}
#[async_std::test]
async fn test_sqlite_parent_not_exists() {
let tmp = tempfile::tempdir().unwrap();
@@ -868,24 +844,6 @@ mod tests {
assert!(dbfile2.is_file());
}
#[async_std::test]
async fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile.into(), blobdir, 1).await;
assert!(res.is_err());
}
#[async_std::test]
async fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1).await;
assert!(res.is_err());
}
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = TestContext::new().await;
@@ -1046,27 +1004,42 @@ mod tests {
}
#[async_std::test]
async fn test_check_passphrase() -> Result<()> {
let dir = tempdir()?;
async fn test_reopen_unencrypted() {
let dir = tempfile::tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(dbfile.clone().into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
drop(context);
// First time: creates new db.
Context::new(dbfile.clone().into(), 1).await.unwrap();
let id = 2;
let context = Context::new(dbfile.into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
assert_eq!(context.open("false".to_string()).await?, false);
assert_eq!(context.open("foo".to_string()).await?, true);
// Opening it encrypted should now fail.
let res = Context::new_encrypted(dbfile.clone().into(), 1, "secret".to_string()).await;
assert!(matches!(res, Err(ContextError::WrongKey)));
Ok(())
// But opening normally still works.
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_ok());
}
#[async_std::test]
async fn test_reopen_ecrypted() {
let dir = tempfile::tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
// First time: creates new db.
Context::new_encrypted(dbfile.clone().into(), 1, "secret".to_string())
.await
.unwrap();
// Opening it unencrypted should now fail.
let res = Context::new(dbfile.clone().into(), 1).await;
assert!(matches!(res, Err(ContextError::WrongKey)));
// Wrong password also fails.
let res = Context::new_encrypted(dbfile.clone().into(), 1, "oops".to_string()).await;
assert!(matches!(res, Err(ContextError::WrongKey)));
// Finally using the right password still works.
let res = Context::new_encrypted(dbfile.into(), 1, "secret".to_string()).await;
assert!(res.is_ok());
}
}
+2 -2
View File
@@ -1156,8 +1156,8 @@ INSERT INTO msgs
stmt.execute(paramsv![
rfc724_mid,
chat_id,
if trash { 0 } else { from_id as i32 },
if trash { 0 } else { to_id as i32 },
if trash { 0 } else { i64::from(from_id) },
if trash { 0 } else { i64::from(to_id) },
sort_timestamp,
sent_timestamp,
rcvd_timestamp,
+17 -55
View File
@@ -71,7 +71,7 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
the function may return negative values. */
let lt = Local::now();
lt.offset().local_minus_utc() as i64
i64::from(lt.offset().local_minus_utc())
}
// timesmearing
@@ -195,44 +195,26 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
}
/* Message-ID tools */
/// Generate an ID. The generated ID should be as short and as unique as possible:
/// - short, because it may also used as part of Message-ID headers or in QR codes
/// - unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
/// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
///
/// Additional information when used as a message-id or group-id:
/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
/// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
/// - the group-id should be a string with the characters [a-zA-Z0-9\-_]
pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible:
- short, because it may also used as part of Message-ID headers or in QR codes
- unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
If possible, RNG of OpenSSL is used.
Additional information when used as a message-id or group-id:
- for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
- for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
- the group-id should be a string with the characters [a-zA-Z0-9\-_] */
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
let mut rng = thread_rng();
let buf: [u32; 3] = [rng.gen(), rng.gen(), rng.gen()];
encode_66bits_as_base64(buf[0usize], buf[1usize], buf[2usize])
}
// Generate 72 random bits.
let mut arr = [0u8; 9];
rng.fill(&mut arr[..]);
/// Encode 66 bits as a base64 string.
/// This is useful for ID generating with short strings as we save 5 character
/// in each id compared to 64 bit hex encoding. For a typical group ID, these
/// are 10 characters (grpid+msgid):
/// hex: 64 bit, 4 bits/character, length = 64/4 = 16 characters
/// base64: 64 bit, 6 bits/character, length = 64/6 = 11 characters (plus 2 additional bits)
/// Only the lower 2 bits of `fill` are used.
fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
use byteorder::{BigEndian, WriteBytesExt};
let mut wrapped_writer = Vec::new();
{
let mut enc = base64::write::EncoderWriter::new(&mut wrapped_writer, base64::URL_SAFE);
enc.write_u32::<BigEndian>(v1).unwrap();
enc.write_u32::<BigEndian>(v2).unwrap();
enc.write_u8(((fill & 0x3) as u8) << 6).unwrap();
enc.finish().unwrap();
}
assert_eq!(wrapped_writer.pop(), Some(b'A')); // Remove last "A"
String::from_utf8(wrapped_writer).unwrap()
// Take 11 base64 characters containing 66 random bits.
base64::encode(&arr).chars().take(11).collect()
}
/// Function generates a Message-ID that can be used for a new outgoing message.
@@ -780,26 +762,6 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert_eq!(buf.len(), 11);
}
#[test]
fn test_encode_66bits_as_base64() {
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 0),
"ASNFZ4mrze8"
);
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 1),
"ASNFZ4mrze9"
);
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 2),
"ASNFZ4mrze-"
);
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 3),
"ASNFZ4mrze_"
);
}
#[test]
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid
+5 -6
View File
@@ -451,12 +451,11 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
.execute(
"UPDATE imap
SET target=''
WHERE EXISTS (
SELECT * FROM msgs
WHERE rfc724_mid=imap.rfc724_mid
AND ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)",
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
)
+254 -158
View File
@@ -9,7 +9,7 @@ use std::{
collections::{BTreeMap, BTreeSet},
};
use anyhow::{anyhow, bail, format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_imap::types::{
Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse,
};
@@ -28,7 +28,6 @@ use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf_inner, from_field_to_contact_id, get_prefetch_parent_message, ReceivedMsg,
};
use crate::ephemeral::delete_expired_imap_messages;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
@@ -459,18 +458,12 @@ impl Imap {
.await
.context("fetch_new_messages")?;
// Mark expired messages for deletion.
delete_expired_imap_messages(context).await?;
self.move_messages(context, watch_folder)
.await
.context("move_messages")?;
self.delete_messages(context, watch_folder)
.await
.context("delete_messages")?;
self.sync_seen_flags(context, watch_folder)
.await
.context("sync_seen_flags")?;
Ok(())
}
@@ -671,6 +664,11 @@ impl Imap {
folder: &str,
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder).await? {
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
let new_emails = self.select_with_uidvalidity(context, folder).await?;
if !new_emails && !fetch_existing_msgs {
@@ -829,18 +827,98 @@ impl Imap {
Ok(read_cnt > 0)
}
/// Moves batch of messages identified by their UID from the currently
/// selected folder to the target folder.
async fn move_message_batch(
&mut self,
context: &Context,
set: &str,
row_ids: Vec<i64>,
target: &str,
) -> Result<()> {
if self.config.can_move {
let session = self
.session
.as_mut()
.context("no session while attempting to MOVE messages")?;
match session.uid_mv(set, &target).await {
Ok(()) => {
// Messages are moved or don't exist, IMAP returns OK response in both cases.
context
.sql
.execute(
format!(
"DELETE FROM imap WHERE id IN ({})",
row_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot delete moved messages from imap table")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {} moved to {}",
set, target
)));
return Ok(());
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {} to {}: {}",
set,
target,
err
);
}
}
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
);
}
// Server does not support MOVE or MOVE failed.
// Copy the message to the destination folder and mark the record for deletion.
let session = self
.session
.as_mut()
.context("no session while attempting to COPY messages")?;
match session.uid_copy(&set, &target).await {
Ok(()) => {
context
.sql
.execute(
format!(
"UPDATE imap SET target='' WHERE id IN ({})",
row_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot plan deletion of copied messages")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {} copied to {}",
set, target
)));
Ok(())
}
Err(err) => Err(err.into()),
}
}
/// Moves messages.
///
/// This is the only place where messages are moved on the IMAP server.
async fn move_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let rows = context
let mut rows = context
.sql
.query_map(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND target != folder
AND target != '' -- Not planned for deletion.
ORDER BY id",
ORDER BY target, uid",
paramsv![folder],
|row| {
let rowid: i64 = row.get(0)?;
@@ -850,105 +928,57 @@ impl Imap {
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
.await?
.into_iter()
.peekable();
self.prepare(context).await?;
self.select_folder(context, Some(folder)).await?;
for (rowid, uid, target) in rows {
// TODO: batch moves of messages with the same destination.
let set = uid.to_string();
while let Some((_, _, target)) = rows.peek().cloned() {
// Construct next request for the target folder.
let mut uid_set = String::new();
let mut rowid_set = Vec::new();
if self.config.can_move {
if let Some(session) = &mut self.session {
match session.uid_mv(&set, &target).await {
Ok(_) => {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP message {}/{} moved to {}",
folder, uid, target
)));
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(async_imap::error::Error::No(text)) => {
// "NO" response, probably the message is moved already.
info!(
context,
"IMAP message {}/{} cannot be moved: {}", folder, uid, text
);
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
target,
err
);
}
while uid_set.len() < 1000 {
// Construct a new range.
if let Some((start_rowid, start_uid, _)) =
rows.next_if(|(_, _, start_target)| start_target == &target)
{
rowid_set.push(start_rowid);
let mut end_uid = start_uid;
while let Some((next_rowid, next_uid, _)) =
rows.next_if(|(_, next_uid, next_target)| {
next_target == &target && *next_uid == end_uid + 1
})
{
end_uid = next_uid;
rowid_set.push(next_rowid);
}
let uid_range = UidRange {
start: start_uid,
end: end_uid,
};
if !uid_set.is_empty() {
uid_set.push(',');
}
uid_set.push_str(&uid_range.to_string());
} else {
bail!("No session while attempting to move the message");
break;
}
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
folder,
uid,
target
);
}
// Server does not support MOVE or MOVE failed.
// Copy the message to the destination folder and mark the record for deletion.
if let Some(session) = &mut self.session {
match session.uid_copy(&set, &target).await {
Ok(_) => {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP message {}/{} copied to {}",
folder, uid, target
)));
// Plan deletion of the original message.
context
.sql
.execute("UPDATE imap SET target='' WHERE id=?", paramsv![rowid])
.await?;
}
Err(async_imap::error::Error::No(text)) => {
// "NO" response, probably the message is moved already.
info!(
context,
"IMAP message {}/{} cannot be copied: {}", folder, uid, text
);
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(err) => {
warn!(
context,
"Could not copy message {}/{}: {}", folder, uid, err
);
// Break the loop to avoid moving messages out of order.
// We can't proceed until this message is moved or copied.
break;
}
}
} else {
bail!("No session while attempting to copy the message");
}
// Execute request.
self.move_message_batch(context, &uid_set, rowid_set, &target)
.await
.with_context(|| {
format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?;
}
Ok(())
@@ -1013,7 +1043,9 @@ impl Imap {
return Ok(());
}
self.select_folder(context, Some(folder)).await?;
self.select_folder(context, Some(folder))
.await
.context("failed to select folder")?;
let session = self
.session
.as_mut()
@@ -1036,66 +1068,49 @@ impl Imap {
}
let mut updated_chat_ids = BTreeSet::new();
let uid_validity = get_uidvalidity(context, folder).await?;
let mut highest_modseq = get_modseq(context, folder).await?;
let uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get UID validity for folder {}", folder))?;
let mut highest_modseq = get_modseq(context, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {}", folder))?;
let mut list = session
.uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {})", highest_modseq))
.await
.context("failed to fetch flags")?;
while let Some(fetch) = list.next().await {
let msg = fetch?;
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
let fetch = fetch.context("failed to get FETCH result")?;
let uid = if let Some(uid) = fetch.uid {
uid
} else {
info!(context, "FETCH result contains no UID, skipping");
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
if is_seen {
if let Some((msg_id, chat_id)) = context
.sql
.query_row_optional(
"SELECT id, chat_id FROM msgs
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
AND uid=?3
LIMIT 1
)",
paramsv![&folder, uid_validity, msg.uid],
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;
Ok((msg_id, chat_id))
},
)
.await?
if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
.await
.with_context(|| {
format!("failed to update seen status for msg {}/{}", folder, uid)
})?
{
let updated = context
.sql
.execute(
"UPDATE msgs SET state=?1
WHERE (state=?2 OR state=?3)
AND id=?4",
paramsv![
MessageState::InSeen,
MessageState::InFresh,
MessageState::InNoticed,
msg_id
],
)
.await?
> 0;
if updated {
updated_chat_ids.insert(chat_id);
let modseq = msg.modseq.unwrap_or_default();
if modseq > highest_modseq {
highest_modseq = modseq;
}
}
updated_chat_ids.insert(chat_id);
}
}
if let Some(modseq) = fetch.modseq {
if modseq > highest_modseq {
highest_modseq = modseq;
}
} else {
warn!(context, "FETCH result contains no MODSEQ");
}
}
set_modseq(context, folder, highest_modseq).await?;
set_modseq(context, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {}", folder))?;
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
}
@@ -1300,7 +1315,8 @@ impl Imap {
let folder = folder.clone();
// safe, as we checked above that there is a body.
let body = body.unwrap();
let body = body
.context("we checked that message has body right above, but it has vanished")?;
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
match dc_receive_imf_inner(
@@ -1355,7 +1371,7 @@ impl Imap {
bail!("Can't set flag, should reconnect");
}
let session = self.session.as_mut().context("No session").unwrap();
let session = self.session.as_mut().context("No session")?;
let query = format!("+FLAGS ({})", flag);
let mut responses = session
.uid_store(uid_set, &query)
@@ -1615,16 +1631,13 @@ impl Imap {
self.config.can_check_quota
}
pub async fn get_quota_roots(
pub(crate) async fn get_quota_roots(
&mut self,
mailbox_name: &str,
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
if let Some(session) = self.session.as_mut() {
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
} else {
Err(anyhow!("Not connected to IMAP, no session"))
}
let session = self.session.as_mut().context("no session")?;
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
}
}
@@ -1658,7 +1671,11 @@ async fn spam_target_folder(
}
}
if needs_move_to_mvbox(context, headers).await? {
if needs_move_to_mvbox(context, headers).await?
// We don't want to move the message to the inbox or sentbox where we wouldn't
// fetch it again:
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
} else if needs_move_to_sentbox(context, folder, headers).await? {
Ok(Some(Config::ConfiguredSentboxFolder))
@@ -1980,6 +1997,70 @@ fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}
/// Marks messages in `msgs` table as seen, searching for them by UID.
///
/// Returns updated chat ID if any message was marked as seen.
async fn mark_seen_by_uid(
context: &Context,
folder: &str,
uid_validity: u32,
uid: u32,
) -> Result<Option<ChatId>> {
if let Some((msg_id, chat_id)) = context
.sql
.query_row_optional(
"SELECT id, chat_id FROM msgs
WHERE id > 9 AND rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
AND uid=?3
LIMIT 1
)",
paramsv![&folder, uid_validity, uid],
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;
Ok((msg_id, chat_id))
},
)
.await
.with_context(|| {
format!(
"failed to get msg and chat ID for IMAP message {}/{}",
folder, uid
)
})?
{
let updated = context
.sql
.execute(
"UPDATE msgs SET state=?1
WHERE (state=?2 OR state=?3)
AND id=?4",
paramsv![
MessageState::InSeen,
MessageState::InFresh,
MessageState::InNoticed,
msg_id
],
)
.await
.with_context(|| format!("failed to update msg {} state", msg_id))?
> 0;
if updated {
Ok(Some(chat_id))
} else {
// Message state has not chnaged.
Ok(None)
}
} else {
// There is no message is `msgs` table matchng the given UID.
Ok(None)
}
}
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// This function is used to update our uid_next after fetching messages.
@@ -2076,6 +2157,21 @@ pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result
}
}
/// Whether to ignore fetching messages from a folder.
///
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
/// not explicitly watched should not be fetched.
async fn should_ignore_folder(context: &Context, folder: &str) -> Result<bool> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false);
}
if context.is_sentbox(folder).await? {
// Still respect the SentboxWatch setting.
return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
}
Ok(!(context.is_mvbox(folder).await? || context.is_spam_folder(folder).await?))
}
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
-1
View File
@@ -157,7 +157,6 @@ impl Imap {
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
match self.fetch_new_messages(context, &watch_folder, false).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
+18 -15
View File
@@ -10,7 +10,8 @@ use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
@@ -20,7 +21,7 @@ impl Imap {
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
return Ok(false);
}
}
info!(context, "Starting full folder scan");
@@ -98,24 +99,26 @@ impl Imap {
}
last_scan.replace(Instant::now());
Ok(())
Ok(true)
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.get_config_bool(Config::SentboxWatch).await? {
res.push(Config::ConfiguredSentboxFolder);
}
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}
Ok(res)
}
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
let mut res = Vec::new();
if let Some(inbox_folder) = context.get_config(Config::ConfiguredInboxFolder).await? {
res.push(inbox_folder);
}
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
(Config::MvboxMove, Config::ConfiguredMvboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await? {
if let Some(folder) = context.get_config(*configured).await? {
res.push(folder);
}
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
}
}
Ok(res)
+2 -2
View File
@@ -197,7 +197,7 @@ impl Job {
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
self.desired_timestamp,
self.tries as i64,
i64::from(self.tries),
self.param.to_string(),
self.job_id as i32,
],
@@ -676,7 +676,7 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
if seconds < 1 {
seconds = 1;
}
seconds as i64
i64::from(seconds)
}
}
}
+2 -1
View File
@@ -7,7 +7,8 @@
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow
clippy::needless_borrow,
clippy::cast_lossless
)]
#![allow(
clippy::match_bool,
+4 -2
View File
@@ -253,7 +253,8 @@ impl LoginParam {
sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(key, self.imap.port as i32).await?;
sql.set_raw_config_int(key, i32::from(self.imap.port))
.await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(key, Some(&self.imap.user)).await?;
@@ -273,7 +274,8 @@ impl LoginParam {
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
sql.set_raw_config_int(key, i32::from(self.smtp.port))
.await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
+3 -3
View File
@@ -180,10 +180,10 @@ impl rusqlite::types::ToSql for MsgId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
return Err(rusqlite::Error::ToSqlConversionFailure(
format_err!("Invalid MsgId").into(),
format_err!("Invalid MsgId {}", self.0).into(),
));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -194,7 +194,7 @@ impl rusqlite::types::FromSql for MsgId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Would be nice if we could use match here, but alas.
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= std::u32::MAX as i64 {
if 0 <= val && val <= i64::from(std::u32::MAX) {
Ok(MsgId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))
+12 -10
View File
@@ -403,16 +403,18 @@ impl MimeMessage {
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if let [textpart, filepart] = &self.parts[..] {
let need_drop = {
textpart.typ == Viewtype::Text
&& (filepart.typ == Viewtype::Image
|| filepart.typ == Viewtype::Gif
|| filepart.typ == Viewtype::Sticker
|| filepart.typ == Viewtype::Audio
|| filepart.typ == Viewtype::Voice
|| filepart.typ == Viewtype::Video
|| filepart.typ == Viewtype::File)
};
let need_drop = textpart.typ == Viewtype::Text
&& match filepart.typ {
Viewtype::Image
| Viewtype::Gif
| Viewtype::Sticker
| Viewtype::Audio
| Viewtype::Voice
| Viewtype::Video
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
};
if need_drop {
let mut filepart = self.parts.swap_remove(1);
+122 -12
View File
@@ -366,7 +366,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
}
});
// fastmail.md: fastmail.com
// fastmail.md: 123mail.org, 150mail.com, 150ml.com, 16mail.com, 2-mail.com, 4email.net, 50mail.com, airpost.net, allmail.net, bestmail.us, cluemail.com, elitemail.org, emailcorner.net, emailengine.net, emailengine.org, emailgroups.net, emailplus.org, emailuser.net, eml.cc, f-m.fm, fast-email.com, fast-mail.org, fastem.com, fastemail.us, fastemailer.com, fastest.cc, fastimap.com, fastmail.cn, fastmail.co.uk, fastmail.com, fastmail.com.au, fastmail.de, fastmail.es, fastmail.fm, fastmail.fr, fastmail.im, fastmail.in, fastmail.jp, fastmail.mx, fastmail.net, fastmail.nl, fastmail.org, fastmail.se, fastmail.to, fastmail.tw, fastmail.uk, fastmail.us, fastmailbox.net, fastmessaging.com, fea.st, fmail.co.uk, fmailbox.com, fmgirl.com, fmguy.com, ftml.net, h-mail.us, hailmail.net, imap-mail.com, imap.cc, imapmail.org, inoutbox.com, internet-e-mail.com, internet-mail.org, internetemails.net, internetmailing.net, jetemail.net, justemail.net, letterboxes.org, mail-central.com, mail-page.com, mailandftp.com, mailas.com, mailbolt.com, mailc.net, mailcan.com, mailforce.net, mailftp.com, mailhaven.com, mailingaddress.org, mailite.com, mailmight.com, mailnew.com, mailsent.net, mailservice.ms, mailup.net, mailworks.org, ml1.net, mm.st, myfastmail.com, mymacmail.com, nospammail.net, ownmail.net, petml.com, postinbox.com, postpro.net, proinbox.com, promessage.com, realemail.net, reallyfast.biz, reallyfast.info, rushpost.com, sent.as, sent.at, sent.com, speedpost.net, speedymail.org, ssl-mail.com, swift-mail.com, the-fastest.net, the-quickest.com, theinternetemail.com, veryfast.biz, veryspeedy.net, warpmail.net, xsmail.com, yepmail.net, your-mail.com
static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
id: "fastmail",
status: Status::Preparation,
@@ -389,13 +389,6 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
port: 465,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.fastmail.com",
port: 587,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
@@ -716,8 +709,8 @@ static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "mail.ru",
status: Status::Ok,
before_login_hint: "Не рекомендуется использовать mail.ru, потому что он разряжает вашу батарею быстрее, чем другие провайдеры.",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
@@ -905,7 +898,7 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "outlook.com",
status: Status::Ok,
@@ -1495,7 +1488,123 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("example.net", &*P_EXAMPLE_COM),
("123mail.org", &*P_FASTMAIL),
("150mail.com", &*P_FASTMAIL),
("150ml.com", &*P_FASTMAIL),
("16mail.com", &*P_FASTMAIL),
("2-mail.com", &*P_FASTMAIL),
("4email.net", &*P_FASTMAIL),
("50mail.com", &*P_FASTMAIL),
("airpost.net", &*P_FASTMAIL),
("allmail.net", &*P_FASTMAIL),
("bestmail.us", &*P_FASTMAIL),
("cluemail.com", &*P_FASTMAIL),
("elitemail.org", &*P_FASTMAIL),
("emailcorner.net", &*P_FASTMAIL),
("emailengine.net", &*P_FASTMAIL),
("emailengine.org", &*P_FASTMAIL),
("emailgroups.net", &*P_FASTMAIL),
("emailplus.org", &*P_FASTMAIL),
("emailuser.net", &*P_FASTMAIL),
("eml.cc", &*P_FASTMAIL),
("f-m.fm", &*P_FASTMAIL),
("fast-email.com", &*P_FASTMAIL),
("fast-mail.org", &*P_FASTMAIL),
("fastem.com", &*P_FASTMAIL),
("fastemail.us", &*P_FASTMAIL),
("fastemailer.com", &*P_FASTMAIL),
("fastest.cc", &*P_FASTMAIL),
("fastimap.com", &*P_FASTMAIL),
("fastmail.cn", &*P_FASTMAIL),
("fastmail.co.uk", &*P_FASTMAIL),
("fastmail.com", &*P_FASTMAIL),
("fastmail.com.au", &*P_FASTMAIL),
("fastmail.de", &*P_FASTMAIL),
("fastmail.es", &*P_FASTMAIL),
("fastmail.fm", &*P_FASTMAIL),
("fastmail.fr", &*P_FASTMAIL),
("fastmail.im", &*P_FASTMAIL),
("fastmail.in", &*P_FASTMAIL),
("fastmail.jp", &*P_FASTMAIL),
("fastmail.mx", &*P_FASTMAIL),
("fastmail.net", &*P_FASTMAIL),
("fastmail.nl", &*P_FASTMAIL),
("fastmail.org", &*P_FASTMAIL),
("fastmail.se", &*P_FASTMAIL),
("fastmail.to", &*P_FASTMAIL),
("fastmail.tw", &*P_FASTMAIL),
("fastmail.uk", &*P_FASTMAIL),
("fastmail.us", &*P_FASTMAIL),
("fastmailbox.net", &*P_FASTMAIL),
("fastmessaging.com", &*P_FASTMAIL),
("fea.st", &*P_FASTMAIL),
("fmail.co.uk", &*P_FASTMAIL),
("fmailbox.com", &*P_FASTMAIL),
("fmgirl.com", &*P_FASTMAIL),
("fmguy.com", &*P_FASTMAIL),
("ftml.net", &*P_FASTMAIL),
("h-mail.us", &*P_FASTMAIL),
("hailmail.net", &*P_FASTMAIL),
("imap-mail.com", &*P_FASTMAIL),
("imap.cc", &*P_FASTMAIL),
("imapmail.org", &*P_FASTMAIL),
("inoutbox.com", &*P_FASTMAIL),
("internet-e-mail.com", &*P_FASTMAIL),
("internet-mail.org", &*P_FASTMAIL),
("internetemails.net", &*P_FASTMAIL),
("internetmailing.net", &*P_FASTMAIL),
("jetemail.net", &*P_FASTMAIL),
("justemail.net", &*P_FASTMAIL),
("letterboxes.org", &*P_FASTMAIL),
("mail-central.com", &*P_FASTMAIL),
("mail-page.com", &*P_FASTMAIL),
("mailandftp.com", &*P_FASTMAIL),
("mailas.com", &*P_FASTMAIL),
("mailbolt.com", &*P_FASTMAIL),
("mailc.net", &*P_FASTMAIL),
("mailcan.com", &*P_FASTMAIL),
("mailforce.net", &*P_FASTMAIL),
("mailftp.com", &*P_FASTMAIL),
("mailhaven.com", &*P_FASTMAIL),
("mailingaddress.org", &*P_FASTMAIL),
("mailite.com", &*P_FASTMAIL),
("mailmight.com", &*P_FASTMAIL),
("mailnew.com", &*P_FASTMAIL),
("mailsent.net", &*P_FASTMAIL),
("mailservice.ms", &*P_FASTMAIL),
("mailup.net", &*P_FASTMAIL),
("mailworks.org", &*P_FASTMAIL),
("ml1.net", &*P_FASTMAIL),
("mm.st", &*P_FASTMAIL),
("myfastmail.com", &*P_FASTMAIL),
("mymacmail.com", &*P_FASTMAIL),
("nospammail.net", &*P_FASTMAIL),
("ownmail.net", &*P_FASTMAIL),
("petml.com", &*P_FASTMAIL),
("postinbox.com", &*P_FASTMAIL),
("postpro.net", &*P_FASTMAIL),
("proinbox.com", &*P_FASTMAIL),
("promessage.com", &*P_FASTMAIL),
("realemail.net", &*P_FASTMAIL),
("reallyfast.biz", &*P_FASTMAIL),
("reallyfast.info", &*P_FASTMAIL),
("rushpost.com", &*P_FASTMAIL),
("sent.as", &*P_FASTMAIL),
("sent.at", &*P_FASTMAIL),
("sent.com", &*P_FASTMAIL),
("speedpost.net", &*P_FASTMAIL),
("speedymail.org", &*P_FASTMAIL),
("ssl-mail.com", &*P_FASTMAIL),
("swift-mail.com", &*P_FASTMAIL),
("the-fastest.net", &*P_FASTMAIL),
("the-quickest.com", &*P_FASTMAIL),
("theinternetemail.com", &*P_FASTMAIL),
("veryfast.biz", &*P_FASTMAIL),
("veryspeedy.net", &*P_FASTMAIL),
("warpmail.net", &*P_FASTMAIL),
("xsmail.com", &*P_FASTMAIL),
("yepmail.net", &*P_FASTMAIL),
("your-mail.com", &*P_FASTMAIL),
("firemail.at", &*P_FIREMAIL_DE),
("firemail.de", &*P_FIREMAIL_DE),
("five.chat", &*P_FIVE_CHAT),
@@ -1539,6 +1648,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("outlook.de", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("posteo.af", &*P_POSTEO),
("posteo.at", &*P_POSTEO),
@@ -1743,4 +1853,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 11));
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));
+47 -13
View File
@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{bail, Context as _, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
@@ -8,8 +8,10 @@ use async_std::{
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::ephemeral::delete_expired_imap_messages;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::log::LogExt;
use crate::smtp::{send_smtp_messages, Smtp};
use self::connectivity::ConnectivityStore;
@@ -160,25 +162,57 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
// Scan other folders before fetching from watched folder. This may result in the
// messages being moved into the watched folder, for example from the Spam folder to
// the Inbox folder.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
if let Err(err) = connection.scan_folders(ctx).await {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
// Mark expired messages for deletion.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages failed")
{
warn!(ctx, "{:#}", err);
}
// fetch
// Fetch the watched folder.
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
connection.connectivity.set_connected(ctx).await;
// idle
@@ -341,7 +375,7 @@ impl Scheduler {
}))
};
if ctx.get_config_bool(Config::MvboxMove).await? {
if ctx.should_watch_mvbox().await? {
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
+5 -15
View File
@@ -5,6 +5,7 @@ use async_std::sync::{Mutex, RwLockReadGuard};
use crate::dc_tools::time;
use crate::events::EventType;
use crate::imap::scan_folders::get_watched_folder_configs;
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
@@ -362,17 +363,14 @@ impl Context {
[
(
Config::ConfiguredInboxFolder,
None,
inbox.state.connectivity.clone(),
),
(
Config::ConfiguredMvboxFolder,
Some(Config::MvboxMove),
mvbox.state.connectivity.clone(),
),
(
Config::ConfiguredSentboxFolder,
Some(Config::SentboxWatch),
sentbox.state.connectivity.clone(),
),
],
@@ -391,20 +389,12 @@ impl Context {
// - "Sent": Connected
// =============================================================================================
let watched_folders = get_watched_folder_configs(self).await?;
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
for (folder, watch, state) in &folders_states {
let w = if let Some(watch_config) = *watch {
self.get_config(watch_config)
.await
.ok_or_log(self)
.flatten()
== Some("1".to_string())
} else {
true
};
for (folder, state) in &folders_states {
let mut folder_added = false;
if w {
if watched_folders.contains(folder) {
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
if let Some(foldername) = f {
+20 -36
View File
@@ -4,10 +4,10 @@ pub mod send;
use std::time::{Duration, SystemTime};
use anyhow::{format_err, Context as _};
use anyhow::{bail, format_err, Context as _, Result};
use async_smtp::smtp::client::net::ClientTlsParameters;
use async_smtp::smtp::response::{Category, Code, Detail};
use async_smtp::{error, smtp, EmailAddress, ServerAddress};
use async_smtp::{smtp, EmailAddress, ServerAddress};
use crate::constants::DC_LP_AUTH_OAUTH2;
use crate::events::EventType;
@@ -23,28 +23,6 @@ use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
/// SMTP write and read timeout in seconds.
const SMTP_TIMEOUT: u64 = 30;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Bad parameters")]
BadParameters,
#[error("Invalid login address {address}: {error}")]
InvalidLoginAddress {
address: String,
#[source]
error: error::Error,
},
#[error("SMTP failed to connect: {0}")]
ConnectionFailure(#[source] smtp::error::Error),
#[error("SMTP oauth2 error {address}")]
Oauth2 { address: String },
#[error("TLS error {0}")]
Tls(#[from] async_native_tls::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Default)]
pub(crate) struct Smtp {
transport: Option<smtp::SmtpTransport>,
@@ -135,14 +113,11 @@ impl Smtp {
}
if lp.server.is_empty() || lp.port == 0 {
return Err(Error::BadParameters);
bail!("bad connection parameters");
}
let from =
EmailAddress::new(addr.to_string()).map_err(|err| Error::InvalidLoginAddress {
address: addr.to_string(),
error: err,
})?;
let from = EmailAddress::new(addr.to_string())
.with_context(|| format!("invalid login address {}", addr))?;
self.from = Some(from);
@@ -163,9 +138,7 @@ impl Smtp {
let send_pw = &lp.password;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;
if access_token.is_none() {
return Err(Error::Oauth2 {
address: addr.to_string(),
});
bail!("SMTP OAuth 2 error {}", addr);
}
let user = &lp.user;
(
@@ -209,9 +182,7 @@ impl Smtp {
}
let mut trans = client.into_transport();
if let Err(err) = trans.connect().await {
return Err(Error::ConnectionFailure(err));
}
trans.connect().await.context("SMTP failed to connect")?;
self.transport = Some(trans);
self.last_success = Some(SystemTime::now());
@@ -398,6 +369,19 @@ pub(crate) async fn send_msg_to_smtp(
)
.collect::<Vec<_>>();
// If there is a msg-id and it does not exist in the db, cancel sending. this happens if
// dc_delete_msgs() was called before the generated mime was sent out.
if !message::exists(context, msg_id)
.await
.with_context(|| format!("failed to check message {} existence", msg_id))?
{
info!(
context,
"Sending of message {} was cancelled by the user.", msg_id
);
return Ok(());
}
let status = match smtp_send(
context,
&recipients_list,
+69 -127
View File
@@ -7,10 +7,10 @@ use std::collections::HashSet;
use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{bail, Context as _, Result};
use anyhow::{anyhow, Context as _, Result};
use async_std::path::PathBuf;
use async_std::prelude::*;
use rusqlite::{config::DbConfig, Connection, OpenFlags};
use rusqlite::{config::DbConfig, OpenFlags};
use crate::blob::BlobObject;
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
@@ -49,40 +49,47 @@ pub struct Sql {
is_encrypted: RwLock<Option<bool>>,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum SqlOpenError {
#[error("wrong passphrase or unencrypted context")]
WrongKey,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl Sql {
pub fn new(dbfile: PathBuf) -> Sql {
Self {
/// Opens or creates a new (unencrypted) database.
///
/// Note after creating this you **MUST** call [`Sql::run_migrations`].
pub(crate) async fn new(dbfile: PathBuf) -> Result<Sql, SqlOpenError> {
let sql = Self {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
}
};
sql.open("".into()).await.map(|_| sql)
}
/// Tests SQLCipher passphrase.
/// Opens or creates an encrypted database.
///
/// Returns true if passphrase is correct, i.e. the database is new or can be unlocked with
/// this passphrase, and false if the database is already encrypted with another passphrase or
/// corrupted.
/// If the database does not exist this creates a new encrypted database with the
/// provided passphrase. If the database does exist attempts to open it with the
/// provided passphrase.
///
/// Fails if database is already open.
pub async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
if self.is_open().await {
bail!("Database is already opened.");
/// Note after creating this you **MUST** call [`Sql::run_migrations`].
pub(crate) async fn new_encrypted(
dbfile: PathBuf,
passphrase: String,
) -> Result<Sql, SqlOpenError> {
if passphrase.is_empty() {
return Err(anyhow!("Empty passphrase").into());
}
// Hold the lock to prevent other thread from opening the database.
let _lock = self.pool.write().await;
// Test that the key is correct using a single connection.
let connection = Connection::open(&self.dbfile)?;
connection
.pragma_update(None, "key", &passphrase)
.context("failed to set PRAGMA key")?;
let key_is_correct = connection
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.is_ok();
Ok(key_is_correct)
let sql = Self {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
};
sql.open(passphrase).await.map(|_| sql)
}
/// Checks if there is currently a connection to the underlying Sqlite database.
@@ -194,11 +201,18 @@ impl Sql {
Ok(pool)
}
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
async fn try_open(&self, dbfile: &Path, passphrase: String) -> Result<(), SqlOpenError> {
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase)?);
{
let conn = self.get_conn().await?;
let res = conn.query_row("SELECT count(*) FROM sqlite_master", params![], |_row| {
Ok(())
});
if res.is_err() {
// This hides SqliteFailure "NotADatabase"
return Err(SqlOpenError::WrongKey);
}
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
@@ -206,21 +220,22 @@ impl Sql {
// When auto_vacuum is INCREMENTAL, it is possible to
// use PRAGMA incremental_vacuum to return unused
// database pages to the filesystem.
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?;
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())
.context("auto_vacuum pragma failed")?;
// journal_mode is persisted, it is sufficient to change it only for one handle.
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
conn.pragma_update(None, "journal_mode", &"WAL".to_string())
.context("journal_mode pragma failed")?;
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())
.context("synchronous pragma failed")?;
}
self.run_migrations(context).await?;
Ok(())
}
pub async fn run_migrations(&self, context: &Context) -> Result<()> {
pub(crate) async fn run_migrations(&self, context: &Context) -> Result<()> {
// (1) update low-level database structure.
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.
@@ -290,28 +305,31 @@ impl Sql {
}
}
info!(context, "Migrations finished");
Ok(())
}
/// Opens the provided database and runs any necessary migrations.
/// Opens the provided database.
///
/// To open an unencrypted database provide an empty passphrase, if the passphrase is
/// wrong an error is returned.
///
/// If a database is already open, this will return an error.
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
async fn open(&self, passphrase: String) -> Result<(), SqlOpenError> {
if self.is_open().await {
error!(
context,
"Cannot open, database \"{:?}\" already opened.", self.dbfile,
);
bail!("SQL database is already opened.");
return Err(anyhow!("SQL database is already opened.").into());
}
let passphrase_nonempty = !passphrase.is_empty();
if let Err(err) = self.try_open(context, &self.dbfile, passphrase).await {
self.close().await;
Err(err)
} else {
info!(context, "Opened database {:?}.", self.dbfile);
*self.is_encrypted.write().await = Some(passphrase_nonempty);
Ok(())
match self.try_open(&self.dbfile, passphrase).await {
Ok(()) => {
*self.is_encrypted.write().await = Some(passphrase_nonempty);
Ok(())
}
Err(err) => {
self.close().await;
Err(err)
}
}
}
@@ -888,7 +906,7 @@ mod tests {
t.sql.close().await;
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
t.sql.open(&t, "".to_string()).await.unwrap();
t.sql.open("".to_string()).await.unwrap();
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
@@ -906,54 +924,10 @@ mod tests {
}
}
/// Regression test.
///
/// Previously the code checking for existence of `config` table
/// checked it with `PRAGMA table_info("config")` but did not
/// drain `SqlitePool.fetch` result, only using the first row
/// returned. As a result, prepared statement for `PRAGMA` was not
/// finalized early enough, leaving reader connection in a broken
/// state after reopening the database, when `config` table
/// existed and `PRAGMA` returned non-empty result.
///
/// Statements were not finalized due to a bug in sqlx:
/// <https://github.com/launchbadge/sqlx/issues/1147>
#[async_std::test]
async fn test_db_reopen() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new(dbfile.into());
// Create database with all the tables.
sql.open(&t, "".to_string()).await.unwrap();
sql.close().await;
// Reopen the database
sql.open(&t, "".to_string()).await?;
sql.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?);",
paramsv!("foo", "bar"),
)
.await?;
let value: Option<String> = sql
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv!("foo"))
.await?;
assert_eq!(value.unwrap(), "bar");
Ok(())
}
#[async_std::test]
async fn test_migration_flags() -> Result<()> {
let t = TestContext::new().await;
t.evtracker.get_info_contains("Opened database").await;
t.evtracker.get_info_contains("Migrations finished").await;
// as migrations::run() was already executed on context creation,
// another call should not result in any action needed.
@@ -988,36 +962,4 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_check_passphrase() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new(dbfile.clone().into());
sql.check_passphrase("foo".to_string()).await?;
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database first time")?;
sql.close().await;
// Reopen the database
let sql = Sql::new(dbfile.into());
// Test that we can't open encrypted database without a passphrase.
assert!(sql.open(&t, "".to_string()).await.is_err());
// Now open the database with passpharse, it should succeed.
sql.check_passphrase("foo".to_string()).await?;
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database second time")?;
Ok(())
}
}
+4 -1
View File
@@ -139,7 +139,10 @@ impl Message {
}
Viewtype::Webxdc => {
append_text = true;
"Webxdc".to_string()
self.get_webxdc_info(context)
.await
.map(|info| info.name)
.unwrap_or_else(|_| "ErrWebxdcName".to_string())
}
Viewtype::Text | Viewtype::Unknown => {
if self.param.get_cmd() != SystemMessage::LocationOnly {
+61 -1
View File
@@ -75,7 +75,7 @@ impl StatusUpdateId {
impl rusqlite::types::ToSql for StatusUpdateId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -488,6 +488,7 @@ mod tests {
add_contact_to_chat, create_group_chat, forward_msgs, send_msg, send_text_msg, ChatId,
ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::contact::Contact;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
@@ -1514,4 +1515,63 @@ sth_for_the = "future""#
Ok(())
}
#[async_std::test]
async fn test_webxdc_chatlist_summary() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let mut instance = create_webxdc_instance(
&t,
"with-minimal-manifest.xdc",
include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
)
.await?;
send_msg(&t, chat_id, &mut instance).await?;
let chatlist = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chatlist.len(), 1);
let summary = chatlist.get_summary(&t, 0, None).await?;
assert_eq!(summary.text, "nice app!".to_string());
Ok(())
}
#[async_std::test]
async fn test_webxdc_and_text() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Alice sends instance and adds some text
let alice_chat = alice.create_chat(&bob).await;
let mut alice_instance = create_webxdc_instance(
&alice,
"minimal.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
)
.await?;
alice_instance.set_text(Some("user added text".to_string()));
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
let alice_instance = alice.get_last_msg().await;
assert_eq!(
alice_instance.get_text(),
Some("user added text".to_string())
);
// Bob receives that instance
let sent1 = alice.pop_sent_msg().await;
bob.recv_msg(&sent1).await;
let bob_instance = bob.get_last_msg().await;
assert_eq!(bob_instance.get_text(), Some("user added text".to_string()));
// Alice's second device receives the instance as well
let alice2 = TestContext::new_alice().await;
alice2.recv_msg(&sent1).await;
let alice2_instance = alice2.get_last_msg().await;
assert_eq!(
alice2_instance.get_text(),
Some("user added text".to_string())
);
Ok(())
}
}