Compare commits

...

157 Commits

Author SHA1 Message Date
bjoern
7c5eb0ae37 prepare 1.76 (#3082)
* update changelog for 1.76.0

* bump version to 1.76.0
2022-02-20 22:58:46 -05:00
link2xt
36bce6c468 Remove unused async-std feature 2022-02-19 11:30:48 +00:00
Hocuri
19a32cdfd3 Let MS Exchange MDNs mark the In-Reply-To message as read (#3075)
Fix https://github.com/deltachat/deltachat-core-rust/issues/2891
2022-02-17 09:56:00 +01:00
link2xt
d708f386a1 smtp: set message state to failed when retry limit is exceeded 2022-02-13 08:59:52 +00:00
link2xt
0f837f4bed Fix a comment typo 2022-02-12 16:29:42 +00:00
link2xt
242e8e2bb3 smtp: remove the message in case of permanent failure
When `smtp_send` returns `Status::Finished`,
the message should be removed from the queue even in case of
failure, such as a permanent error.

In addition to this bugfix, move the retry count increase to
the beginning of `send_msg_to_smtp` to ensure no message is
retried infinitely even in case of similar bugs.
2022-02-12 16:20:13 +00:00
link2xt
1d56b24b67 cargo update 2022-02-12 16:19:44 +00:00
Hocuri
bb9138708a Fix disappearing drafts (#3067) 2022-02-10 10:05:30 +01:00
Hocuri
34f5510f1f Don't directly download messages from the Spam folder (#3015)
fix #3007

My approach is:

We don't download any messages from the spam folder anymore, and only download them if they were moved out. This means that is-it-spam logic only resides in spam_target_folder(). This has some implications, see the comments.
2022-02-10 09:06:22 +01:00
link2xt
6c6d47c89c Fix CI
timeout_func_only makes pytest-rerunfailures work with pytest-timeout,
but it only works with default timeout_method.

See pytest-rerunfailures issue for details:
https://github.com/pytest-dev/pytest-rerunfailures/issues/99
2022-02-08 20:50:11 +00:00
link2xt
196075c031 imap: batch message deletion 2022-02-06 11:42:30 +00: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 Bruynooghe
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
B. Petersen
57ea4c1d92 bump version to 1.72.0 2022-01-25 18:18:18 +01:00
B. Petersen
bcdd15ef3a update changelog for 1.72.0 2022-01-25 18:18:18 +01:00
Hocuri
5f939c3123 Fix: Run migrations after importing backup again (#3006)
As of #2980, the migrations were not run after importing a backup. This lead to errors like "Failed to send message: no such table: smtp" if you imported a backup from a previous DC version.
2022-01-24 18:36:28 +03:00
B. Petersen
2446fc44ad fix typo in mergeable description 2022-01-24 13:19:20 +01:00
dependabot[bot]
9ba8dd91df Merge pull request #2990 from deltachat/dependabot/cargo/stop-token-0.7.0 2022-01-23 21:23:56 +00:00
B. Petersen
10e1cdbc52 bump version to 1.71.0 2022-01-23 20:58:49 +01:00
B. Petersen
46eceb38d5 update changelog for 1.71.0
as we had the changelog-chat only in the last days,
- add missing pr
- update some wrong references
- unify some layout and wordings,
  streamline headlines to "API Changes", "Changes" and "Fixes",
  finer subdivisions only raise noise, duplicates and discussions
  and, if not used consequently, do not add much benefit.
2022-01-23 20:58:49 +01:00
link2xt
81de882e2f Add Protected Headers standard to standards.md 2022-01-23 19:47:10 +00:00
link2xt
593e07cdff cargo update 2022-01-23 19:31:34 +00:00
B. Petersen
8ca54f616e raise webxdc sending limit to 640 kb
as discussed in dev chat,
100 kb is too low if one wants to use any game framework.
also, many game that doesn't have poor graphics
or uses magic CSS-tricks to do fancy draws,
will have more than 100 kb probably just in tiles and sprites.

even for regions with low resources,
100 kb is low for a game -
esp. if we compare that with stickers that are send around as well
are not deduplicated and also have several 100 kb in size -
for much less effect.

we should still encourage ppl to create tiny apps,
however, a too low limit also restricts possibilities wrt adaption.
2022-01-23 12:48:25 +01:00
link2xt
f7f899f0a4 smtp: retry immediately if connection is stale 2022-01-22 21:17:01 +00:00
B. Petersen
05a3c0c89b webxdc: synchronous state for read-only-chats
already before,
we did _not_ sent updates to contact requests or other read-only-chats.
however, we _did_ modify the local database,
so that getAllUpdates() returns an update that was not sent out to other peers.

this is fixed by checking can_send() soon.

that way, all peers have the same state
also for contact requests or other read-only-chats
and webxdc in contact requests can be opened as usual.

further ui improvements may be needed for contact requests
(maybe allow the webxdc to know about that state or
maybe show a warning in the ui somewhere),
however, this is not part of this pr.
2022-01-22 21:28:47 +01:00
link2xt
f21691c122 Add "database_encrypted" field to Context.get_info() 2022-01-22 14:43:37 +00:00
B. Petersen
836e26d8d0 refine mergeable summary 2022-01-22 15:27:25 +01:00
B. Petersen
8a7c1fe4cb mergeable: adding #skip-changelog to pr description skips the corresponding test 2022-01-22 14:35:12 +01:00
bjoern
7f43d3bb37 let sending invalid webxdc fail (#2993)
* let sending invalid webxdc fail

invalid webxdc can still be send as Viewtype::File, however
(maybe one want to discuss errors or so ;)

* clarify the supported zip compression methods
2022-01-22 11:19:21 +01:00
bjoern
11b975ab19 use new webxdc logo (#2994)
* use webxdc logo

* optimize png
2022-01-22 11:01:05 +01:00
bjoern
315e4215d9 make update messages work if a key is missing (#2998)
* add a test for unencrypted replies to encrypted webxdc instances

* make update messages work if a key is missing

even in opportunistic chats,
replies to encrypted messages are forced to be encrypted,
if that is not possbile, message sending fails.

while this is okay to not leak previously send text messages,
the quotes as used by webxdc are more artificial,
currently only the static text "Webxdc".

* changelog ...
2022-01-22 10:56:15 +01:00
dependabot[bot]
e35e6c44cf cargo: bump stop-token from 0.6.1 to 0.7.0
Bumps [stop-token](https://github.com/async-rs/stop-token) from 0.6.1 to 0.7.0.
- [Release notes](https://github.com/async-rs/stop-token/releases)
- [Commits](https://github.com/async-rs/stop-token/commits)

---
updated-dependencies:
- dependency-name: stop-token
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-20 21:11:49 +00:00
Floris Bruynooghe
260cb78e3a Re-write the blob filename creation loop
This was written in a way which attempted to avoid easily creating an
infinite loop.  But really that's a python idiom and doesn't work very
well in Rust.  Worse, as shown by #2972 it is really easy to still get
this wrong.  Instead do this the rust way, this way the compiler can
also reason properly about the branches and what is unreachable
removing some bogus dead code.
2022-01-20 21:55:32 +01:00
link2xt
a1f04d2129 imex: use param2 for passphrase 2022-01-16 13:22:08 +00:00
bjoern
9b562eebcd handle parent for webxdc info-messages (#2984)
* set parent for webxdc info-messages

* test parent() for info-messages

* add dc_msg_get_parent() ffi
2022-01-19 11:46:32 +01:00
Simon Laux
1d175c4557 update changelog for 1.71 (#2968)
* update changelog for 1.71

* Apply suggestions from code review

Co-authored-by: bjoern <r10s@b44t.com>

Co-authored-by: bjoern <r10s@b44t.com>
2022-01-17 18:39:24 +01:00
Simon Laux
f755070080 apply wording suggestion from @r10s 2022-01-17 18:37:53 +01:00
Simon Laux
1c6c72a0fe try fixing the mergeable configuration 2022-01-17 18:37:53 +01:00
Simon Laux
1755f2ea3d update readme in deltachat-ffi to try out the changelog check 2022-01-17 18:37:53 +01:00
Simon Laux
498cc6c80b add mergable changelog test 2022-01-17 18:37:53 +01:00
bjoern
8d3227a92b fix webxdc forwarding and drafts (#2979)
* fix forwarding webxdc instances, add a test for that

* adapt webxdc test to fail on info-messages added when in draft mode

* do not add info-messages when in draft-mode

* half the number of instance-loads per webxdc update send/receive
2022-01-17 14:23:35 +01:00
Hocuri
c6d855084e Save "configured" flag later (#2974)
While experimenting with encrypted storage, once configuring failed between 920 and 940. But as the "configured" config had already been written after progress 910, some part of the code thought we are configured, some didn't.
2022-01-16 20:37:28 +01:00
Hocuri
827b3f8aeb Create parent directory if creating a new file fails (#2978)
With this PR, my encrypted-storage Android PR now works, at least I
couldn't find any further bugs.

Without it, configuring fails with: `Failed to create blob
icon-saved-messages-1803424689.png in
/data/user/0/com.b44t.messenger.beta/files/accounts/0e402b37dcd14a9586aea46294c908f2/dc.db-blobs:
No such file or directory (os error 2)`.

Also see https://github.com/deltachat/deltachat-core-rust/pull/2972.
2022-01-16 20:37:02 +01:00
link2xt
fb95573000 cargo update 2022-01-16 09:56:20 +00:00
dependabot[bot]
f026bd455f Merge pull request #2966 from deltachat/dependabot/cargo/tagger-4.2.1 2022-01-16 09:46:41 +00:00
dependabot[bot]
f0b92a5757 Merge pull request #2976 from deltachat/dependabot/cargo/smallvec-1.8.0 2022-01-16 09:45:28 +00:00
dependabot[bot]
d7c6f1e63b cargo: bump smallvec from 1.7.0 to 1.8.0
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-14 21:19:37 +00:00
Hocuri
1e9e308df3 Fix clippy errors (#2973) 2022-01-14 17:34:19 +01:00
missytake
c9a70f149d Merge pull request #2967 from deltachat/timeout_config_wait_finish
allow timeout for internal configure tracker API
2022-01-13 15:12:37 +01:00
holger krekel
d4ff47b6ac allow timeout for internal configure tracker API 2022-01-13 14:57:43 +01:00
dependabot[bot]
8b4b241403 cargo: bump tagger from 4.0.1 to 4.2.1
Bumps [tagger](https://github.com/tiby312/tagger) from 4.0.1 to 4.2.1.
- [Release notes](https://github.com/tiby312/tagger/releases)
- [Commits](https://github.com/tiby312/tagger/commits)

---
updated-dependencies:
- dependency-name: tagger
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-13 10:13:45 +00:00
B. Petersen
6dcd6947d7 update changelog 2022-01-13 11:11:50 +01:00
B. Petersen
327328412a let render_webxdc_status_update_object() return an Option; add a test for that 2022-01-13 11:11:50 +01:00
B. Petersen
42f9ef00b9 wrap update-item-array into an update-object on the wire; this allows to add other members in the future. the updates the peers see is not changed 2022-01-13 11:11:50 +01:00
B. Petersen
8c2ea0fa26 swap paramters in sendUpdate(); the 'descr' may be split up in the future, so it makes sense to have that at the end 2022-01-13 11:11:50 +01:00
B. Petersen
14e9afaf42 rename 'User Guide' to 'Developer Reference' 2022-01-13 11:11:50 +01:00
B. Petersen
a3a101641a use well-known icon-filenames instead of manifest 2022-01-13 11:11:50 +01:00
B. Petersen
8bd93fe495 fix typo in deltachat.h doc 2022-01-13 11:11:50 +01:00
bjoern
56df22bca7 Update deltachat-ffi/deltachat.h
Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
2022-01-13 11:11:50 +01:00
B. Petersen
5f32a6738a avoid double ZIP-parsing in get_webxdc_info() and do not read blob for just checking existance of a file 2022-01-13 11:11:50 +01:00
B. Petersen
1c081935fb tweak documentation, remove general explanation from api-reference, this is already done in the overview 2022-01-13 11:11:50 +01:00
B. Petersen
8fd4d00776 adapt to new set_quote() api 2022-01-13 11:11:50 +01:00
B. Petersen
7d04ea58c3 tweak ffi-doc a little 2022-01-13 11:11:50 +01:00
B. Petersen
2cc84a0f0d document manifest.toml 2022-01-13 11:11:50 +01:00
B. Petersen
8f715532cb read manifest.toml and add get_webxdc_info() 2022-01-13 11:11:50 +01:00
B. Petersen
5a77df7cc5 document window.webxdc.selfName() 2022-01-13 11:11:50 +01:00
bjoern
59658f2b0b Update draft/webxdc-user-guide.md
Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
2022-01-13 11:11:50 +01:00
holger krekel
0b983906da refinements 2022-01-13 11:11:50 +01:00
holger krekel
cd1f164d18 update the webxdc language, fix some typos 2022-01-13 11:11:50 +01:00
B. Petersen
e2a6ac6625 send status updates to self also for drafts 2022-01-13 11:11:50 +01:00
B. Petersen
8e8c10c438 rename w30 to webxdc 2022-01-13 11:11:50 +01:00
holger krekel
7ff25f282e another update 2022-01-13 11:11:50 +01:00
holger krekel
b8dc608032 some updates/refinements 2022-01-13 11:11:50 +01:00
holger krekel
d7e699320b shift to webxdc naming (#2933) 2022-01-13 11:11:50 +01:00
B. Petersen
ef333da770 draft a w30 user guide 2022-01-13 11:11:50 +01:00
B. Petersen
575a389b08 adapt to new test apis 2022-01-13 11:11:50 +01:00
B. Petersen
9bc0824be6 allow accessing zip-archives with absolute paths 2022-01-13 11:11:50 +01:00
B. Petersen
b656a60234 check that the w30 app is actually an zip-archive with an index.html 2022-01-13 11:11:50 +01:00
B. Petersen
bd988d805c better distinguish between update-items and payloads: update-items contain a payload (and maybe more in the future) 2022-01-13 11:11:50 +01:00
B. Petersen
7ad7ccb8fe send status-update-event also for self-sent updates 2022-01-13 11:11:50 +01:00
B. Petersen
e30c535f18 wrap payload to a json-structure that can be extended as needed 2022-01-13 11:11:50 +01:00
B. Petersen
de7706f622 wrap payloads to json-object on the wire 2022-01-13 11:11:50 +01:00
B. Petersen
de20e4c9dd basic w30 sending and receiving 2022-01-13 11:11:50 +01:00
holger krekel
41f9314e2a provide a higher level view of web30 based on the conversations i started about it around November 14th 2021 2022-01-13 11:11:50 +01:00
B. Petersen
2280ce349a add Message.parent() (Message.quoted_messages requires a text) 2022-01-13 11:11:50 +01:00
B. Petersen
7aa05e1c9f create table to track w30 status updates 2022-01-13 11:11:50 +01:00
B. Petersen
69d174c9e8 draft ffi for w30 2022-01-13 11:11:50 +01:00
link2xt
3c38fa6b70 Add API for passphrase-protected accounts
To create encrypted account with account manager, call
dc_accounts_add_closed_account(). Open this account with
dc_context_open() using the passphrase you want to use for encryption.

When application is loaded next time and account manager is created,
it will open all accounts that have no passphrase set. For encrypted
accounts dc_context_is_open() will return 0. To open them, call
dc_context_open() with the correct passphrase. After opening, call
dc_context_start_io() on this account or just dc_accounts_start_io()
to start all accounts that are not started yet.

Support for legacy SQLite-based backup format is removed in this
commit.
2022-01-06 08:54:58 +00:00
B. Petersen
728c8b4663 pull in update for mail.de in provider-db 2022-01-11 15:00:26 +01:00
B. Petersen
fab9563cd5 update provider database
ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`
2022-01-11 15:00:26 +01:00
dependabot[bot]
20fe2473e1 Merge pull request #2961 from deltachat/dependabot/cargo/tagger-4.0.1 2022-01-11 04:25:38 +00:00
dependabot[bot]
3815062c11 cargo: bump tagger from 3.3.0 to 4.0.1
Bumps [tagger](https://github.com/tiby312/tagger) from 3.3.0 to 4.0.1.
- [Release notes](https://github.com/tiby312/tagger/releases)
- [Commits](https://github.com/tiby312/tagger/commits)

---
updated-dependencies:
- dependency-name: tagger
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-11 04:01:52 +00:00
B. Petersen
581ea9fda0 remove outdated DC_STR_STATUSLINE link 2022-01-10 21:32:51 +01:00
link2xt
bfa641cea8 Error handling refactoring
- Replace .ok_or_else() and .map_err() with anyhow::Context where possible.
- Use .context() to check Option for None when it's an error
- Resultify Chatlist.get_chat_id()
- Add useful .context() to some errors
- IMAP error handling cleanup
2022-01-07 14:22:37 +00:00
link2xt
29c58efeb3 Remove default signature advertsing Delta Chat 2022-01-07 13:56:58 +00:00
link2xt
27eb82c556 context: hide ongoing process internals from public API
Also hide derive_blobdir and derive_walfile.
2022-01-07 06:25:03 +00:00
link2xt
652d67a20f Revert flaky "sql: enable auto_vacuum on all connections"
It results in "database is locked" errors on CI.

This reverts commit ce0984f02f.
2022-01-07 00:18:21 +03:00
link2xt
ce0984f02f sql: enable auto_vacuum on all connections 2022-01-06 09:03:50 +00:00
bjoern
b3e3b1e245 allow removing quotes on existing drafts (#2950)
allow `dc_msg_set_quote(msg, NULL)` and `msg.set_quote(None)`
to simplify draft handling keeping message-ids (as needed for webxdc updates).

closes #2948
2022-01-05 23:59:04 +01:00
dependabot[bot]
c69ee180af Merge pull request #2944 from deltachat/dependabot/cargo/rustyline-9.1.2 2022-01-05 14:46:28 +00:00
link2xt
bba3a25371 Add CONDSTORE to standards.md 2022-01-03 23:12:29 +00:00
link2xt
095b358aca Test multidevice synchronization of Seen status 2022-01-03 23:12:29 +00:00
link2xt
833e5f46cc Synchronize seen status across devices
Seen status is only synchronized on servers supporting IMAP CONDSTORE
extension. At the end of fetch loop iteration, flags are fetched for
all messages modified since previous synchronization and highest
modification sequence is stored into `imap_sync` table.
2022-01-03 23:12:29 +00:00
link2xt
3e0ce0e07a test_no_old_msg_is_fresh: compare DC_EVENT_MSGS_NOTICED argument to chat id
data1 of DC_EVENT_MSGS_NOTICED contains chat id, not message id
2022-01-03 23:12:29 +00:00
link2xt
1f31dd12fc Replace BTreeMap with BTreeSet in markseen_msgs 2022-01-03 23:12:29 +00:00
link2xt
f63efc29bf Resultify update_msg_state 2022-01-03 23:12:29 +00:00
link2xt
3e394f14e8 sql: disable cipher_memory_security SQLCipher PRAGMA
It slows down dc_get_chat_msgs() from ~50ms to seconds on Android. It
is disabled by default in SQLCipher 4.5.0, but currently SQLCipher 4.4.3
is bundled so this PRAGMA has to be disabled manually.
2022-01-03 23:03:59 +00:00
dependabot[bot]
ff6ffa1656 cargo: bump rustyline from 9.1.1 to 9.1.2
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 9.1.1 to 9.1.2.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v9.1.1...v9.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 21:12:32 +00:00
link2xt
304c259a57 Update resync_folder_uids to use imap table 2022-01-02 17:04:16 +00:00
link2xt
630754b52e dc_receive_imf: don't fail on invalid address in the To field
This is an irrecoverable error, dc_receive_imf must not fail on it
as it prevents last seen UID from advancing, so the same message
is prefetched on each iteration of IMAP loop.
2022-01-02 00:20:44 +00:00
link2xt
afd8c0d879 Add smtp table
It replaces SendMsgToSmtp job.

Prepared outgoing SMTP payloads are stored in the database now rather
than files in blobdir.
2022-01-01 19:14:53 +00:00
B. Petersen
6316ee7c9b add editable "summary" to dc_msg_get_webxdc_info()
the summary can be modified by the apps using
`sendUpdate({summary: "foo", payload: ...})`

the summary is updated when there is no newer update
and chat will be informed by the change as usual by
`DC_EVENT_MSGS_CHANGED`.
2022-01-16 00:30:53 +01:00
B. Petersen
b6b8d11881 add option to trigger an info-message from an webxdc-update 2022-01-16 00:30:53 +01:00
Hocuri
8753fd5887 Actually return a sensible error from create_new_file() 2022-01-15 18:33:17 +01:00
bjoern
5aaafb5ac1 limit webxdc-sizes for a better ux (#2971)
* limit webxdc-sizes for a better ux

* Update src/webxdc.rs

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

* Update src/webxdc.rs

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
2022-01-15 00:12:00 +01:00
B. Petersen
a043557c44 move payload to {payload:PAYLOAD} to allow additional parameters
additional parameters will be `summary` and `msg` (for an info-message)
right now, and maybe more in the future (`aggregated`, `fast`, `to` ...),
so adding just more parameters easily gets wild.

also, this makes adaptions easier as no ffi needs to be adapted
when we add more parameters.

finally, sending is in-sync with receiving, database and wire now,
one receives similar objects as one sends,
which also looks like the right thing :)

for now, `sendUpdate()` also allows to use the old, unwrapped payload
to not immediately break existing `.xdc`, however, that will be removed soon,
probably before the first release.
2022-01-15 00:10:59 +01:00
B. Petersen
4af4914e32 simplify WebxdcStatusUpdate handling 2022-01-15 00:10:59 +01:00
dependabot[bot]
e35b3f1e80 cargo: bump quote from 1.0.10 to 1.0.14
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.10 to 1.0.14.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.10...1.0.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 12:28:40 +01:00
dependabot[bot]
ff8859b9db cargo: bump syn from 1.0.83 to 1.0.84
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.83 to 1.0.84.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.83...1.0.84)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 12:27:51 +01:00
Simon Laux
937ff5a378 add more links to the language bindings in readme (#2895)
Co-authored-by: bjoern <r10s@b44t.com>
2022-01-01 12:25:01 +01:00
80 changed files with 5780 additions and 2300 deletions

26
.github/mergeable.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
version: 2
mergeable:
- when: pull_request.*
name: "Changelog check"
validate:
- do: or
validate:
- do: description
must_include:
regex: '#skip-changelog'
- do: and
validate:
- do: dependent
changed:
file: 'src/**'
required: ['CHANGELOG.md']
- do: dependent
changed:
file: 'deltachat-ffi/**'
required: ['CHANGELOG.md']
fail:
- do: checks
status: 'action_required'
payload:
title: Changlog might need an update
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."

View File

@@ -1,11 +1,117 @@
# Changelog
## Unreleased
## 1.76.0
### Changes
- move messages in batches #3058
- delete messages in batches #3060
- python: remove arbitrary timeouts from tests #3059
- refactorings #3026
### Fixes
- avoid archived, fresh chats #3053
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
- Fix a bug where messages in the Spam folder created contact requests #3015
- Fix a bug where drafts disappeared after some days #3067
- Parse MS Exchange read receipts and mark the original message as read #3075
- do not retry message sending infinitely in case of permanent SMTP failure #3070
- set message state to failed when retry limit is exceeded #3072
## 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
- run migrations on backup import #3006
## 1.71.0
### API Changes
- added APIs to handle database passwords: `dc_context_new_closed()`, `dc_context_open()`,
`dc_context_is_open()` and `dc_accounts_add_closed_account()` #2956 #2972
- use second parameter of `dc_imex` to provide backup passphrase #2980
- added `DC_MSG_WEBXDC`, `dc_send_webxdc_status_update()`,
`dc_get_webxdc_status_updates()`, `dc_msg_get_webxdc_blob()`, `dc_msg_get_webxdc_info()`
and `DC_EVENT_WEBXDC_STATUS_UPDATE` #2826 #2971 #2975 #2977 #2979 #2993 #2994 #2998 #3001 #3003
- added `dc_msg_get_parent()` #2984
- added `dc_msg_force_plaintext()` API for bots #2847
- allow removing quotes on drafts `dc_msg_set_quote(msg, NULL)` #2950
- removed `mvbox_watch` option; watching is enabled when `mvbox_move` is enabled #2906
- removed `inbox_watch` option #2922
- deprecated `os_name` in `dc_context_new()`, pass `NULL` or an empty string #2956
### Changes
- start making it possible to write to mailing lists #2736
- add `hop_info` to `dc_get_info()` #2751 #2914 #2923
- add information about whether the database is encrypted or not to `dc_get_info()` #3000
- selfstatus now defaults to empty #2951 #2960
- validate detached cryptographic signatures as used eg. by Thunderbird #2865
- do not change the draft's `msg_id` on updates and sending #2887
- add `imap` table to keep track of message UIDs #2909 #2938
- replace `SendMsgToSmtp` jobs which stored outgoing messages in blobdir with `smtp` SQL table #2939 #2996
- sql: enable `auto_vacuum=INCREMENTAL` #2931
- sql: build rusqlite with sqlcipher #2934
- synchronize Seen status across devices #2942
- `dc_preconfigure_keypair` now takes ascii armored keys instead of base64 #2862
- put removed member in Bcc instead of To in the message about removal #2864
- improve group updates #2889
- re-write the blob filename creation loop #2981
- update provider database (11 Jan 2022) #2959
- python: allow timeout for internal configure tracker API #2967
- python: remove API deprecated in Python 3.10 #2907
- refactorings #2932 #2957 #2947
- improve tests #2863 #2866 #2881 #2908 #2918 #2901 #2973
- improve documentation #2880 #2886 #2895
- improve ci #2919 #2926 #2969 #2999
### Fixes
- fix leaving groups #2929
- fix unread count #2861
- make `add_parts()` not early-exit #2879
- recognize MS Exchange read receipts as read receipts #2890
- create parent directory if creating a new file fails #2978
- save "configured" flag later #2974
- improve log #2928
- `dc_receive_imf`: don't fail on invalid address in the To field #2940
- Removed `mvbox_watch` option. #2906
It is automatically enabled whenever `mvbox_move` is enabled.
## 1.70.0

551
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.70.0"
version = "1.76.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -12,6 +12,9 @@ debug = 0
[profile.release]
lto = true
[patch.crates-io]
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" }
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -21,13 +24,12 @@ async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-std-resolver = "0.20"
async-std = { version = "1", features = ["unstable"] }
async-std = { version = "1" }
async-tar = { version = "0.4", default-features=false }
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" }
@@ -63,7 +65,7 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
stop-token = "0.6"
stop-token = "0.7"
strum = "0.23"
strum_macros = "0.23"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
@@ -74,8 +76,9 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "3.3.0"
tagger = "4.2.1"
textwrap = "0.14.2"
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"

View File

@@ -125,11 +125,11 @@ $ cargo test -- --ignored
Language bindings are available for:
- [C](https://c.delta.chat)
- [Node.js](https://www.npmjs.com/package/deltachat-node)
- [Python](https://py.delta.chat)
- [Go](https://github.com/deltachat/go-deltachat/)
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js** \[[📂 source](https://github.com/deltachat/deltachat-node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library

BIN
assets/icon-webxdc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

181
assets/icon-webxdc.svg Normal file
View File

@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="80mm"
height="297mm"
viewBox="0 0 80 297"
version="1.1"
id="svg71"
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
sodipodi:docname="icon-webxdc.svg"
inkscape:export-filename="C:\Users\user\OneDrive - BFW-Leipzig\Documents\LogoDC\finalohnerand.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<metadata
id="metadata856">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
id="namedview73"
pagecolor="#767676"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:document-units="mm"
showgrid="false"
showborder="false"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:zoom="1.4142136"
inkscape:cx="-90.271136"
inkscape:cy="-1233.1209"
inkscape:window-width="1864"
inkscape:window-height="1027"
inkscape:window-x="56"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:snap-global="false"
showguides="false"
inkscape:guide-bbox="true"
inkscape:document-rotation="0"
units="px">
<sodipodi:guide
position="-154.76097,641.11689"
orientation="0,-1"
id="guide21118" />
<sodipodi:guide
position="-60.286487,633.36619"
orientation="0,-1"
id="guide21120" />
</sodipodi:namedview>
<defs
id="defs68">
<linearGradient
id="linearGradient4375">
<stop
style="stop-color:#364e59;stop-opacity:1;"
offset="0"
id="stop4377" />
<stop
style="stop-color:#364e59;stop-opacity:0;"
offset="1"
id="stop4379" />
</linearGradient>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#1a1a1a;stroke:#000000;stroke-width:0.167903"
id="rect880"
width="79.8321"
height="79.8321"
x="-64.03286"
y="-375.9097"
ry="0" />
<path
inkscape:connector-curvature="0"
id="path3799-2"
d="m -24.089585,-372.59579 c -19.986026,0.24336 -36.196903,16.666 -36.196903,36.67011 0,20.00409 16.210877,36.03233 36.196903,35.78912 19.0024236,-0.076 14.5340713,-10.6146 35.538854,-0.85693 -11.50627538,-17.97454 0.390097,-20.36737 0.658079,-35.81316 0,-20.00411 -16.2108788,-36.03235 -36.196911,-35.78914 z"
style="fill:#364e59;fill-opacity:1;stroke:none;stroke-width:1.93355"
sodipodi:nodetypes="sscccs" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M -54.193871,-325.26419 Z"
id="path3846" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M -49.397951,-326.67773 Z"
id="path3848" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -49.397951,-326.67773 v 0 0"
id="path3850" />
<path
style="fill:none;stroke:#000000;stroke-width:0.01;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -51.35133,-325.0334 -7.964067,5.98895 z"
id="path3965" />
<path
inkscape:connector-curvature="0"
id="path11037"
d="m -24.089585,-372.19891 c -19.986026,0.24156 -36.196903,16.54352 -36.196903,36.40062 0,7.86524 2.543315,15.1113 6.857155,20.97971 6.577146,8.94734 11.123515,9.77363 11.123515,9.77363 1.343237,1.78324 10.270932,4.3223 10.270932,4.3223 l 16.791727,-70.86654 -0.468369,-0.33457 c 0.458597,0.26445 0.428277,-0.27515 -8.378035,-0.27515 z"
style="fill:#7cc5cc;fill-opacity:1;stroke:none;stroke-width:1.92643"
sodipodi:nodetypes="sssccccss" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M -49.944239,-310.69957 Z"
id="path13674" />
<g
id="g15178"
transform="matrix(0.79975737,0,0,0.79975737,53.088959,-63.716396)">
<rect
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
id="rect15072"
width="29.897917"
height="6.8791666"
x="-334.4964"
y="-154.51025"
transform="rotate(45)" />
<rect
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
id="rect15072-5"
width="29.897917"
height="6.8791666"
x="147.63107"
y="-334.4964"
transform="rotate(-45)"
inkscape:transform-center-x="-0.74835017"
inkscape:transform-center-y="0.37417525" />
</g>
<g
id="g22468"
transform="translate(3.3033974)">
<g
id="g15178-0"
transform="matrix(-0.79975737,0,0,0.79975737,-103.11028,-63.716404)"
style="fill:#7cc5cc;fill-opacity:1">
<rect
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
id="rect15072-2"
width="29.897917"
height="6.8791666"
x="-334.4964"
y="-154.51025"
transform="rotate(45)" />
<rect
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
id="rect15072-5-5"
width="29.897917"
height="6.8791666"
x="147.63107"
y="-334.4964"
transform="rotate(-45)"
inkscape:transform-center-x="-0.74835017"
inkscape:transform-center-y="0.37417525" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.70.0"
version = "1.76.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -1,5 +1,9 @@
# Delta Chat C Interface
## Installation
see `Installing libdeltachat system wide` in [../README.md](../README.md)
## Documentation
To generate the C Interface documentation,

View File

@@ -179,24 +179,66 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
// create/open/config/information
/**
* Create a new context object. After creation it is usually
* opened, connected and mails are fetched.
* 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.
*
* @memberof dc_context_t
* @param os_name is only for decorative use.
* You can give the name of the app, the operating system,
* the used environment and/or the version here.
* @param os_name Deprecated, pass NULL or empty string here.
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @param blobdir Deprecated, pass NULL or an empty string here.
* @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.
*/
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.
*
* @memberof dc_context_t
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @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.
*
* 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);
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);
/**
@@ -279,7 +321,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
* - `selfstatus` = Own status to display e.g. in email footers, defaults to a standard text defined by #DC_STR_STATUSLINE
* - `selfstatus` = Own status to display e.g. in email footers, defaults to empty
* - `selfavatar` = File containing avatar. Will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* NULL to remove the avatar.
@@ -293,14 +335,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)=
@@ -940,6 +988,64 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* An webxdc instance send a status update to its other members.
*
* In js-land, that would be mapped to sth. as:
* ```
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
* ```
* `context` and `msg_id` is not needed in js as that is unique within an webxdc instance.
* See dc_get_webxdc_status_updates() for the receiving counterpart.
*
* If the webxdc instance is a draft, the update is not send immediately.
* Instead, the updates are collected and sent out in batch when the instance is actually sent.
* This allows preparing webxdc instances,
* eg. defining a poll with predefined answers.
*
* Other members will be informed by #DC_EVENT_WEBXDC_STATUS_UPDATE that there is a new update.
* You will also get the #DC_EVENT_WEBXDC_STATUS_UPDATE yourself
* and the update you're sent will also be included in dc_get_webxdc_status_updates().
*
* @memberof dc_context_t
* @param context The context object
* @param msg_id id of the message with the webxdc instance
* @param json program-readable data, the actual payload
* @param descr user-visible description of the json-data,
* in case of a chess game, eg. the move.
* @return 1=success, 0=error
*/
int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const char* json, const char* descr);
/**
* Get webxdc status updates.
* The status updates may be sent by yourself or by other members using dc_send_webxdc_status_update().
* In both cases, you will be informed by #DC_EVENT_WEBXDC_STATUS_UPDATE
* whenever there is a new update.
*
* In js-land, that would be mapped to sth. as:
* ```
* window.webxdc.setUpdateListener((update) => {
* if (update.payload.action === "move") {
* print(update.payload.src)
* print(update.payload.dest)
* }
* });
* ```
*
* @memberof dc_context_t
* @param context The context object
* @param msg_id id of the message with the webxdc instance
* @param status_update_id Can be used to filter out only a concrete status update.
* When set to 0, all known status updates are returned.
* @return JSON-array containing the requested updates,
* each element was created by dc_send_webxdc_status_update()
* on this or other devices.
* If there are no updates, an empty JSON-array is returned.
*/
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t status_update_id);
/**
* Save a draft for a chat in the database.
*
@@ -1923,8 +2029,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
#define DC_IMEX_EXPORT_SELF_KEYS 1 // param1 is a directory where the keys are written to
#define DC_IMEX_IMPORT_SELF_KEYS 2 // param1 is a directory where the keys are searched in and read from
#define DC_IMEX_EXPORT_BACKUP 11 // param1 is a directory where the backup is written to
#define DC_IMEX_IMPORT_BACKUP 12 // param1 is the file with the backup to import
#define DC_IMEX_EXPORT_BACKUP 11 // param1 is a directory where the backup is written to, param2 is a passphrase to encrypt the backup
#define DC_IMEX_IMPORT_BACKUP 12 // param1 is the file with the backup to import, param2 is the backup's passphrase
/**
@@ -1933,14 +2039,16 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* What to do is defined by the _what_ parameter which may be one of the following:
*
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`
* encrypted with the passphrase given as `param2`. If `param2` is NULL or empty string,
* the backup is not encrypted.
* The backup contains all contacts, chats, images and other data and device independent settings.
* The backup does not contain device dependent settings as ringtones or LED notification settings.
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
* the format is `delta-chat-<day>-<number>.tar`
*
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. The file is normally
* created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
* is only possible as long as the context is not configured or used in another way.
*
* - **DC_IMEX_EXPORT_SELF_KEYS** (1) - Export all private keys and all public keys of the user to the
@@ -2470,6 +2578,7 @@ void dc_str_unref (char* str);
* To make this possible, some dc_context_t functions must not be called
* when using the account manager:
* - use dc_accounts_add_account() and dc_accounts_get_account() instead of dc_context_new()
* - use dc_accounts_add_closed_account() instead of dc_context_new_closed()
* - use dc_accounts_start_io() and dc_accounts_stop_io() instead of dc_start_io() and dc_stop_io()
* - use dc_accounts_maybe_network() instead of dc_maybe_network()
* - use dc_accounts_get_event_emitter() instead of dc_get_event_emitter()
@@ -2527,6 +2636,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().
*
* 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.
*
* @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.
* On errors, 0 is returned.
*/
uint32_t dc_accounts_add_closed_account (dc_accounts_t* accounts);
/**
* Migrate independent accounts into accounts managed by the account manager.
@@ -3558,6 +3683,45 @@ char* dc_msg_get_filename (const dc_msg_t* msg);
char* dc_msg_get_filemime (const dc_msg_t* msg);
/**
* Return file from inside an webxdc message.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
* @param filename The name inside the archive,
* can be given as an absolute path (`/file.png`)
* or as a relative path (`file.png`, no leading slash)
* @param ret_bytes Pointer to a size_t. The size of the blob will be written here.
* @return The blob must be released using dc_str_unref() after usage.
* NULL if there is no such file in the archive or on errors.
*/
char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char* filename, size_t* ret_bytes);
/**
* Get info from a webxdc message, in JSON format.
* The returned JSON string has the following key/values:
*
* - name: The name of the app.
* Defaults to the filename if not set in the manifest.
* - icon: App icon file name.
* Defaults to an standard icon if nothing is set in the manifest.
* To get the file, use dc_msg_get_webxdc_blob().
* App icons should should be square,
* the implementations will add round corners etc. as needed.
* - summary: short string describing the state of the app,
* sth. as "2 votes", "Highscore: 123",
* can be changed by the apps and defaults to an empty string.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
* @return a UTF8-encoded JSON string containing all requested info.
* Must be freed using dc_str_unref().
* NULL is never returned.
*/
char* dc_msg_get_webxdc_info (const dc_msg_t* msg);
/**
* Get the size of the file. Returns the size of the file associated with a
* message, if applicable.
@@ -3809,8 +3973,11 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* These messages are typically shown in the center of the chat view,
* dc_msg_get_text() returns a descriptive text about what is going on.
*
* For informational messages created by Webxdc apps,
* dc_msg_get_parent() usually returns the Webxdc instance;
* UIs can use that to scroll to the Webxdc app when the info is tapped.
*
* There is no need to perform any action when seeing such a message - this is already done by the core.
* Typically, these messages are displayed in the center of the chat.
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -4165,7 +4332,8 @@ void dc_msg_latefiling_mediasize (dc_msg_t* msg, int width, int hei
*
* @memberof dc_msg_t
* @param msg The message object to set the reply to.
* @param quote The quote to set for msg.
* @param quote The quote to set for the message object given as `msg`.
* NULL removes an previously set quote.
*/
void dc_msg_set_quote (dc_msg_t* msg, const dc_msg_t* quote);
@@ -4210,6 +4378,23 @@ char* dc_msg_get_quoted_text (const dc_msg_t* msg);
*/
dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
/**
* Get parent message, if available.
*
* Used for Webxdc-info-messages
* to jump to the corresponding instance that created the info message.
*
* For quotes, please use the more specialized
* dc_msg_get_quoted_text() and dc_msg_get_quoted_msg().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The parent message or NULL.
* Must be freed using dc_msg_unref() after usage.
*/
dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg);
/**
* Force the message to be sent in plain text.
*
@@ -4714,6 +4899,15 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
/**
* The message is a webxdc instance.
*
* To send data to a webxdc instance, use dc_send_webxdc_status_update()
*/
#define DC_MSG_WEBXDC 80
/**
* @}
*/
@@ -5407,6 +5601,21 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_SELFAVATAR_CHANGED 2110
/**
* webxdc status update received.
* To get the received status update, use dc_get_webxdc_status_updates().
* To send status updates, use dc_send_webxdc_status_update().
*
* Note, that you do not get events that arrive when the app is not running;
* instead, you can use dc_get_webxdc_status_updates() to get all status updates
* and catch up that way.
*
* @param data1 (int) msg_id
* @param data2 (int) status_update_id
*/
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
/**
* @}
*/
@@ -5633,12 +5842,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_FILE 12
/// "Sent with my Delta Chat Messenger: https://delta.chat"
///
/// Used as the default footer
/// if nothing else is set by the dc_set_config()-option `selfstatus`.
#define DC_STR_STATUSLINE 13
/// "Group name changed from %1$s to %2$s."
///
/// Used in status messages for group name changes.

View File

@@ -27,6 +27,7 @@ use async_std::sync::RwLock;
use async_std::task::{block_on, spawn};
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use num_traits::{FromPrimitive, ToPrimitive};
use rand::Rng;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
@@ -36,6 +37,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateId;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -75,7 +77,6 @@ pub unsafe extern "C" fn dc_context_new(
}
let ctx = if blobdir.is_null() || *blobdir == 0 {
use rand::Rng;
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
block_on(Context::new(as_path(dbfile).to_path_buf().into(), id))
@@ -86,12 +87,63 @@ pub unsafe extern "C" fn dc_context_new(
match ctx {
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
eprintln!("failed to create context: {}", err);
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *mut dc_context_t {
setup_panic!();
if dbfile.is_null() {
eprintln!("ignoring careless call to dc_context_new_closed()");
return ptr::null_mut();
}
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile).to_path_buf().into(),
id,
)) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
}
}
}
#[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.
@@ -449,6 +501,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
EventType::ImexFileWritten(_) => 0,
EventType::SecurejoinInviterProgress { contact_id, .. }
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -490,6 +543,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
EventType::SecurejoinInviterProgress { progress, .. }
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
EventType::WebxdcStatusUpdate {
status_update_id, ..
} => status_update_id.to_u32() as libc::c_int,
}
}
@@ -531,6 +587,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::SecurejoinJoinerProgress { .. }
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate { .. }
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -820,6 +877,52 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
msg_id: u32,
json: *const libc::c_char,
descr: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
return 0;
}
let ctx = &*context;
block_on(ctx.send_webxdc_status_update(
MsgId::new(msg_id),
&to_string_lossy(json),
&to_string_lossy(descr),
))
.log_err(ctx, "Failed to send webxdc update")
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_webxdc_status_updates(
context: *mut dc_context_t,
msg_id: u32,
status_update_id: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_webxdc_status_updates()");
return "".strdup();
}
let ctx = &*context;
block_on(ctx.get_webxdc_status_updates(
MsgId::new(msg_id),
if status_update_id == 0 {
None
} else {
Some(StatusUpdateId::new(status_update_id))
},
))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1919,7 +2022,7 @@ pub unsafe extern "C" fn dc_imex(
context: *mut dc_context_t,
what_raw: libc::c_int,
param1: *const libc::c_char,
_param2: *const libc::c_char,
param2: *const libc::c_char,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_imex()");
@@ -1932,12 +2035,13 @@ pub unsafe extern "C" fn dc_imex(
return;
}
};
let passphrase = to_opt_string_lossy(param2);
let ctx = &*context;
if let Some(param1) = to_opt_string_lossy(param1) {
spawn(async move {
imex::imex(ctx, what, param1.as_ref())
imex::imex(ctx, what, param1.as_ref(), passphrase)
.await
.log_err(ctx, "IMEX failed")
});
@@ -2443,7 +2547,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
return 0;
}
let ffi_list = &*chatlist;
ffi_list.list.get_chat_id(index as usize).to_u32()
let ctx = &*ffi_list.context;
match ffi_list.list.get_chat_id(index as usize) {
Ok(chat_id) => chat_id.to_u32(),
Err(err) => {
warn!(ctx, "get_chat_id failed: {}", err);
0
}
}
}
#[no_mangle]
@@ -2960,6 +3071,61 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
ffi_msg.message.get_filename().unwrap_or_default().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
msg: *mut dc_msg_t,
filename: *const libc::c_char,
ret_bytes: *mut libc::size_t,
) -> *mut libc::c_char {
if msg.is_null() || filename.is_null() || ret_bytes.is_null() {
eprintln!("ignoring careless call to dc_msg_get_webxdc_blob()");
return ptr::null_mut();
}
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
let blob = block_on(async move {
ffi_msg
.message
.get_webxdc_blob(ctx, &to_string_lossy(filename))
.await
});
match blob {
Ok(blob) => {
*ret_bytes = blob.len();
let ptr = libc::malloc(*ret_bytes);
libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes);
ptr as *mut libc::c_char
}
Err(err) => {
eprintln!("failed read blob from archive: {}", err);
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_webxdc_info()");
return "".strdup();
}
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
block_on(async move {
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {}", err);
return "".strdup();
}
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
.strdup()
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
@@ -3372,17 +3538,21 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
return;
}
let ffi_msg = &mut *msg;
let ffi_quote = &*quote;
if ffi_msg.context != ffi_quote.context {
eprintln!("ignoring attempt to quote message from a different context");
return;
}
let quote_msg = if quote.is_null() {
None
} else {
let ffi_quote = &*quote;
if ffi_msg.context != ffi_quote.context {
eprintln!("ignoring attempt to quote message from a different context");
return;
}
Some(&ffi_quote.message)
};
block_on(async move {
ffi_msg
.message
.set_quote(&*ffi_msg.context, &ffi_quote.message)
.set_quote(&*ffi_msg.context, quote_msg)
.await
.log_err(&*ffi_msg.context, "failed to set quote")
.ok();
@@ -3425,6 +3595,29 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_t {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_parent()");
return ptr::null_mut();
}
let ffi_msg: &MessageWrapper = &*msg;
let context = &*ffi_msg.context;
let res = block_on(async move {
ffi_msg
.message
.parent(context)
.await
.log_err(context, "failed to get parent message")
.unwrap_or(None)
});
match res {
Some(message) => Box::into_raw(Box::new(MessageWrapper { context, message })),
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
if msg.is_null() {
@@ -3954,6 +4147,30 @@ 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()");
return 0;
}
let accounts = &mut *accounts;
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.add_closed_account().await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to add account: {:#}",
err
)));
0
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_remove_account(
accounts: *mut dc_accounts_t,

View File

@@ -0,0 +1,194 @@
# Webxdc Developer Reference
## Webxdc File Format
- a **Webxdc app** is a **ZIP-file** with the extension `.xdc`
- the ZIP-file must use the default compression methods as of RFC 1950,
this is "Deflate" or "Store"
- the ZIP-file must contain at least the file `index.html`
- if the Webxdc app is started, `index.html` is opened in a restricted webview
that allow accessing resources only from the ZIP-file
## Webxdc API
There are some additional APIs available once `webxdc.js` is included
(the file will be provided by the concrete implementations,
no need to add `webxdc.js` to your ZIP-file):
```html
<script src="webxdc.js"></script>
```
### sendUpdate()
```js
window.webxdc.sendUpdate(update, descr);
```
Webxdc apps are usually shared in a chat and run independently on each peer.
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
- `update`: an object with the following fields:
- `update.payload`: any javascript primitive, array or object.
- `update.info`: optional, short, informational message that will be added to the chat,
eg. "Alice voted" or "Bob scored 123 in MyGame";
usually only one line of text is shown,
use this option sparingly to not spam the chat.
- `update.summary`: optional, short text, shown beside app icon;
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
- `descr`: short, human-readable description what this update is about.
this is shown eg. as a fallback text in an email program.
All peers, including the sending one,
will receive the update by the callback given to `setUpdateListener()`.
There are situations where the user cannot send messages to a chat,
eg. contact requests or if the user has left a group.
In these cases, you can still call `sendUpdate()`,
however, the update won't be sent to other peers
and you won't get the update by `setUpdateListener()` nor by `getAllUpdates()`.
### setUpdateListener()
```js
window.webxdc.setUpdateListener((update) => {});
```
With `setUpdateListener()` you define a callback that receives the updates
sent by `sendUpdate()`.
- `update`: passed to the callback on updates with the following fields:
`update.payload`: equals the payload given to `sendUpdate()`
The callback is called for updates sent by you or other peers.
### 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.
- `updates`: All previous updates in an array,
eg. `[{payload: "foo"},{payload: "bar"}]`
if `webxdc.sendUpdate({payload: "foo"}); webxdc.sendUpdate({payload: "bar"};` was called on the last run.
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
window.webxdc.getAllUpdates().then(updates => {});
```
### 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
```js
window.webxdc.selfName
```
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.
## manifest.toml
If the ZIP-file contains a `manifest.toml` in its root directory,
some basic information are read and used from there.
the `manifest.toml` has the following format
```toml
name = "My App Name"
```
- **name** - The name of the app.
If no name is set or if there is no manifest, the filename is used as the app name.
## App Icon
If the ZIP-root contains an `icon.png` or `icon.jpg`,
these files are used as the icon for the app.
The icon should be a square at reasonable width/height;
round corners etc. will be added by the implementations as needed.
If no icon is set, a default icon will be used.
## Webxdc Examples
The following example shows an input field and every input is show on all peers.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="webxdc.js"></script>
</head>
<body>
<input id="input" type="text"/>
<a href="" onclick="sendMsg(); return false;">Send</a>
<p id="output"></p>
<script>
function sendMsg() {
msg = document.getElementById("input").value;
window.webxdc.sendUpdate({payload: msg}, 'Someone typed "'+msg+'".');
}
function receiveUpdate(update) {
document.getElementById('output').innerHTML += update.payload + "<br>";
}
window.webxdc.setUpdateListener(receiveUpdate);
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
</script>
</body>
</html>
```
[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
- older devices might not have the newest js features in their webview,
you may want to transpile your code down to an older js version eg. with https://babeljs.io
- there are tons of ideas for enhancements of the API and the file format,
eg. in the future, we will may define icon- and manifest-files,
allow to aggregate the state or add metadata.

View File

@@ -387,6 +387,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
sendsyncmsg\n\
sendupdate <msg-id> <json status update>\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
@@ -471,20 +472,32 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?;
imex(
&context,
ImexMode::ExportBackup,
dir.as_ref(),
Some(arg2.to_string()),
)
.await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?;
imex(
&context,
ImexMode::ImportBackup,
arg1.as_ref(),
Some(arg2.to_string()),
)
.await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?;
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?;
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
@@ -563,7 +576,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}{}",
chat_prefix(&chat),
@@ -907,6 +920,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Some(msg_id) => println!("sync message sent as {}.", msg_id),
None => println!("sync message not needed."),
},
"sendupdate" => {
ensure!(
!arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <json status update> expected"
);
let msg_id = MsgId::new(arg1.parse()?);
context
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
.await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
@@ -1142,7 +1165,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if 0 != i {
res += ", ";
}
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
}
}

View File

@@ -169,7 +169,7 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 35] = [
const CHAT_COMMANDS: [&str; 36] = [
"listchats",
"listarchived",
"chat",
@@ -191,6 +191,7 @@ const CHAT_COMMANDS: [&str; 35] = [
"sendfile",
"sendhtml",
"sendsyncmsg",
"sendupdate",
"videochat",
"draft",
"listmedia",

View File

@@ -179,6 +179,12 @@ class Account(object):
"""
return True if lib.dc_is_configured(self._dc_context) else False
def is_open(self) -> bool:
"""Determine if account is open
:returns True if account is open."""
return True if lib.dc_context_is_open(self._dc_context) else False
def set_avatar(self, img_path: Optional[str]) -> None:
"""Set self avatar.

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))

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")

View File

@@ -90,11 +90,11 @@ class ConfigureTracker:
if data1 is None or evdata == data1:
break
def wait_finish(self):
def wait_finish(self, timeout=None):
""" wait until configure is completed.
Raise Exception if Configure failed
"""
if not self._configure_events.get():
if not self._configure_events.get(timeout=timeout):
content = "\n".join(map(str, self._ffi_events))
raise ConfigureFailed(content)

View File

@@ -41,8 +41,8 @@ class TestOfflineAccountBasic:
def test_wrong_db(self, tmpdir):
p = tmpdir.join("hello.db")
p.write("123")
with pytest.raises(ValueError):
Account(p.strpath)
account = Account(p.strpath)
assert not account.is_open()
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
@@ -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)
@@ -737,7 +735,6 @@ class TestOnlineAccount:
# make sure we are not sending message to ourselves
assert self_addr not in ev.data2
assert other_addr in ev.data2
ev = ac1._evtracker.get_matching("DC_EVENT_DELETED_BLOB_FILE")
lp.sec("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
@@ -753,7 +750,6 @@ class TestOnlineAccount:
# now make sure we are sending message to ourselves too
assert self_addr in ev.data2
assert other_addr in ev.data2
ev = ac1._evtracker.get_matching("DC_EVENT_DELETED_BLOB_FILE")
assert ac1.direct_imap.idle_wait_for_seen()
# Second client receives only second message, but not the first
@@ -894,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()
@@ -1071,6 +1067,38 @@ class TestOnlineAccount:
is_seen = b'\\Seen' in flags
assert is_seen
def test_multidevice_sync_seen(self, acfactory, lp):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
ac1_clone = acfactory.clone_online_account(ac1)
acfactory.wait_configure_and_start_io()
ac1.set_config("bcc_self", "1")
ac1_clone.set_config("bcc_self", "1")
ac1_chat = ac1.create_chat(ac2)
ac1_clone_chat = ac1_clone.create_chat(ac2)
ac2_chat = ac2.create_chat(ac1)
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
ac2_chat.send_text("Hi")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert ac1_chat.count_fresh_messages() == 1
assert ac1_clone_chat.count_fresh_messages() == 1
assert ac1_message.is_in_fresh
assert ac1_clone_message.is_in_fresh
lp.sec("ac1 marks message as seen on the first device")
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
lp.sec("ac1 clone detects that message is marked as seen")
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
def test_message_override_sender_name(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -1370,11 +1398,13 @@ class TestOnlineAccount:
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
def test_dont_show_emails(self, acfactory, lp):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
ac1 = acfactory.get_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.org").create_chat()
@@ -1382,6 +1412,7 @@ class TestOnlineAccount:
acfactory.wait_configure(ac1)
ac1.direct_imap.create_folder("Drafts")
ac1.direct_imap.create_folder("Sent")
ac1.direct_imap.create_folder("Spam")
acfactory.wait_configure_and_start_io()
# Wait until each folder was selected once and we are IDLEing again:
@@ -1406,6 +1437,15 @@ class TestOnlineAccount:
message in Sent
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Spam", """
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Spam
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
@@ -1419,6 +1459,10 @@ class TestOnlineAccount:
assert msg.text == "subj message in Sent"
assert len(msg.chat.get_messages()) == 1
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
ac1.direct_imap.select_folder("Spam")
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
@@ -1456,7 +1500,7 @@ class TestOnlineAccount:
ac1_clone.create_chat(ac2).send_text("Hi back")
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == first_msg_id.id
assert ev.data1 == first_msg_id.chat.id
assert ac1.create_chat(ac2).count_fresh_messages() == 0
assert len(list(ac1.get_fresh_messages())) == 0
@@ -1804,7 +1848,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()
@@ -1821,7 +1864,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")
@@ -1995,7 +2037,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()) == \
@@ -2005,29 +2047,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
@@ -2474,8 +2516,7 @@ class TestOnlineAccount:
lp.sec("ac2: deleting all messages except third")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
for msg in to_delete:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
@@ -2652,7 +2693,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 + ")")

View File

@@ -36,7 +36,8 @@ def test_wrong_db(tmpdir):
# write an invalid database file
p.write("x123" * 10)
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
assert not lib.dc_context_is_open(context)
def test_empty_blobdir(tmpdir):

View File

@@ -79,7 +79,7 @@ addopts = -v -ra --strict-markers
norecursedirs = .tox
xfail_strict=true
timeout = 90
timeout_method = thread
timeout_func_only = True
markers =
ignored: ignore this test in default test runs, use --ignored to run.

View File

@@ -56,8 +56,13 @@ impl Accounts {
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists().await, "accounts.toml does not exist");
let config = Config::from_file(config_file).await?;
let accounts = config.load_accounts().await?;
let config = Config::from_file(config_file)
.await
.context("failed to load accounts config")?;
let accounts = config
.load_accounts()
.await
.context("failed to load accounts")?;
let emitter = EventEmitter::new();
@@ -66,7 +71,9 @@ impl Accounts {
emitter.sender.send(events.get_emitter()).await?;
for account in accounts.values() {
emitter.add_account(account).await?;
emitter.add_account(account).await.with_context(|| {
format!("failed to add account {} to event emitter", account.id)
})?;
}
Ok(Self {
@@ -104,7 +111,9 @@ impl Accounts {
Ok(())
}
/// Add a new account.
/// Add a new account and opens it.
///
/// Returns account ID.
pub async fn add_account(&mut self) -> Result<u32> {
let account_config = self.config.new_account(&self.dir).await?;
@@ -115,6 +124,17 @@ impl Accounts {
Ok(account_config.id)
}
/// Adds a new closed account.
pub async fn add_closed_account(&mut self) -> 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?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
/// Remove an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self.accounts.remove(&id);
@@ -182,7 +202,7 @@ impl Accounts {
match res {
Ok(_) => {
let ctx = Context::with_blobdir(new_dbfile, new_blobdir, account_config.id).await?;
let ctx = Context::new(new_dbfile, account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -383,7 +403,15 @@ impl Config {
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?;
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);
}
@@ -408,8 +436,13 @@ impl Config {
self.sync().await?;
self.select_account(id).await.expect("just added");
let cfg = self.get_account(id).await.expect("just added");
self.select_account(id)
.await
.context("failed to select just added account")?;
let cfg = self
.get_account(id)
.await
.context("failed to get just added account")?;
Ok(cfg)
}
@@ -701,4 +734,49 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_encrypted_account() -> 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")?;
assert_eq!(accounts.accounts.len(), 0);
let account_id = accounts
.add_closed_account()
.await
.context("failed to add closed 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())
.await
.context("failed to create second accounts manager")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.is_open().await, false);
// Try wrong passphrase.
assert_eq!(account.open("barfoo".to_string()).await?, false);
assert_eq!(account.open("".to_string()).await?, false);
assert_eq!(account.open("foobar".to_string()).await?, true);
assert_eq!(account.is_open().await, true);
Ok(())
}
}

View File

@@ -2,7 +2,7 @@
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
use anyhow::{bail, format_err, Error, Result};
use anyhow::{bail, Context as _, Error, Result};
use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
@@ -139,15 +139,14 @@ impl str::FromStr for Aheader {
};
let public_key: SignedPublicKey = attributes
.remove("keydata")
.ok_or_else(|| format_err!("keydata attribute is not found"))
.context("keydata attribute is not found")
.and_then(|raw| {
SignedPublicKey::from_base64(&raw)
.map_err(|_| format_err!("Autocrypt key cannot be decoded"))
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
})
.and_then(|key| {
key.verify()
.and(Ok(key))
.map_err(|_| format_err!("Autocrypt key cannot be verified"))
.context("autocrypt key cannot be verified")
})?;
let prefer_encrypt = attributes

View File

@@ -24,6 +24,7 @@ use crate::constants::{
};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message;
/// Represents a file in the blob directory.
@@ -63,7 +64,7 @@ impl<'a> BlobObject<'a> {
) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name);
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
let (name, mut file) = BlobObject::create_new_file(context, blobdir, &stem, &ext).await?;
file.write_all(data)
.await
.map_err(|err| BlobError::WriteFailure {
@@ -87,13 +88,16 @@ impl<'a> BlobObject<'a> {
// Creates a new file, returning a tuple of the name and the handle.
async fn create_new_file(
context: &Context,
dir: &Path,
stem: &str,
ext: &str,
) -> Result<(String, fs::File), BlobError> {
let max_attempt = 15;
const MAX_ATTEMPT: u32 = 16;
let mut attempt = 0;
let mut name = format!("{}{}", stem, ext);
for attempt in 0..max_attempt {
loop {
attempt += 1;
let path = dir.join(&name);
match fs::OpenOptions::new()
.create_new(true)
@@ -103,24 +107,20 @@ impl<'a> BlobObject<'a> {
{
Ok(file) => return Ok((name, file)),
Err(err) => {
if attempt == max_attempt {
if attempt >= MAX_ATTEMPT {
return Err(BlobError::CreateFailure {
blobdir: dir.to_path_buf(),
blobname: name,
cause: err,
});
} else if attempt == 1 && !dir.exists().await {
fs::create_dir_all(dir).await.ok_or_log(context);
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
}
}
}
}
// This is supposed to be unreachable, but the compiler doesn't know.
Err(BlobError::CreateFailure {
blobdir: dir.to_path_buf(),
blobname: name,
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
})
}
/// Creates a new blob object with unique name by copying an existing file.
@@ -149,7 +149,7 @@ impl<'a> BlobObject<'a> {
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
let (name, mut dst_file) =
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?;
let name_for_err = name.clone();
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
{

View File

@@ -5,7 +5,7 @@ use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
@@ -32,11 +32,16 @@ use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer a
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::job::{self, Action};
use crate::location;
use crate::message::{self, Message, MessageState, MsgId};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::webxdc::WEBXDC_SUFFIX;
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone)]
@@ -438,6 +443,7 @@ impl ChatId {
&msg_text,
cmd,
dc_create_smeared_timestamp(context).await,
None,
)
.await?;
}
@@ -468,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 {
@@ -620,7 +625,10 @@ impl ChatId {
/// Returns `true`, if message was deleted, `false` otherwise.
async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
match self.get_draft_msg_id(context).await? {
Some(msg_id) => Ok(msg_id.delete_from_db(context).await.is_ok()),
Some(msg_id) => {
msg_id.delete_from_db(context).await?;
Ok(true)
}
None => Ok(false),
}
}
@@ -640,8 +648,11 @@ impl ChatId {
.param
.get_blob(Param::File, context, !msg.is_increation())
.await?
.ok_or_else(|| format_err!("No file stored in params"))?;
.context("no file stored in params")?;
msg.param.set(Param::File, blob.as_name());
if blob.suffix() == Some(WEBXDC_SUFFIX) {
msg.viewtype = Viewtype::Webxdc;
}
}
}
@@ -949,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)
}
@@ -959,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))
@@ -1276,7 +1287,7 @@ impl Chat {
msg.param.set_int(Param::AttachGroupImage, 1);
self.param.remove(Param::Unpromoted);
self.update_param(context).await?;
// send_sync_msg() is called (usually) a moment later at Job::send_msg_to_smtp()
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
// when the group-creation message is actually sent though SMTP -
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
context.sync_qr_code_tokens(Some(self.id)).await?;
@@ -1793,6 +1804,7 @@ pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Webxdc => true,
}
}
@@ -1804,9 +1816,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.param
.get_blob(Param::File, context, !msg.is_increation())
.await?
.ok_or_else(|| {
format_err!("Attachment missing for message of type #{}", msg.viewtype)
})?;
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
if msg.viewtype == Viewtype::Image {
if let Err(e) = blob.recode_to_image_size(context).await {
@@ -1825,16 +1835,30 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if let Some((better_type, better_mime)) =
message::guess_msgtype_from_suffix(&blob.to_abs_path())
{
msg.viewtype = better_type;
if !msg.param.exists(Param::MimeType) {
msg.param.set(Param::MimeType, better_mime);
if better_type != Viewtype::Webxdc
|| context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await
.is_ok()
{
msg.viewtype = better_type;
if !msg.param.exists(Param::MimeType) {
msg.param.set(Param::MimeType, better_mime);
}
}
}
} else if !msg.param.exists(Param::MimeType) {
} else if msg.viewtype == Viewtype::Webxdc {
context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await?;
}
if !msg.param.exists(Param::MimeType) {
if let Some((_, mime)) = message::guess_msgtype_from_suffix(&blob.to_abs_path()) {
msg.param.set(Param::MimeType, mime);
}
}
info!(
context,
"Attaching \"{}\" for message type #{}.",
@@ -1938,41 +1962,25 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
/// Tries to send a message synchronously.
///
/// Directly opens an smtp
/// connection and sends the message, bypassing the job system. If this fails, it writes a send job to
/// the database.
/// Creates a new message in `smtp` table, then drectly opens an SMTP connection and sends the
/// message. If this fails, the message remains in the database to be sent later.
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
if let Some(mut job) = prepare_send_msg(context, chat_id, msg).await? {
if let Some(rowid) = prepare_send_msg(context, chat_id, msg).await? {
let mut smtp = crate::smtp::Smtp::new();
send_msg_to_smtp(context, &mut smtp, rowid)
.await
.context("failed to send message, queued for later sending")?;
let status = job.send_msg_to_smtp(context, &mut smtp).await;
match status {
job::Status::Finished(Ok(_)) => {
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
});
Ok(msg.id)
}
_ => {
job.save(context).await?;
Err(format_err!(
"failed to send message, queued for later sending"
))
}
}
} else {
// Nothing to do
Ok(msg.id)
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
});
}
Ok(msg.id)
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
if let Some(send_job) = prepare_send_msg(context, chat_id, msg).await? {
job::add(context, send_job).await?;
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
@@ -1981,16 +1989,19 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
if msg.param.exists(Param::SetLatitude) {
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
}
context.interrupt_smtp(InterruptInfo::new(false)).await;
}
Ok(msg.id)
}
/// Returns rowid from `smtp` table.
async fn prepare_send_msg(
context: &Context,
chat_id: ChatId,
msg: &mut Message,
) -> Result<Option<crate::job::Job>> {
) -> Result<Option<i64>> {
// dc_prepare_msg() leaves the message state to OutPreparing, we
// only have to change the state to OutPending in this case.
// Otherwise we still have to prepare the message, which will set
@@ -2004,11 +2015,145 @@ async fn prepare_send_msg(
chat_id.is_unset() || chat_id == msg.chat_id,
"Inconsistent chat ID"
);
message::update_msg_state(context, msg.id, MessageState::OutPending).await;
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
}
let job = job::send_msg_job(context, msg.id).await?;
let row_id = create_send_msg_job(context, msg.id).await?;
Ok(row_id)
}
Ok(job)
/// Constructs a job for sending a message and inserts into `smtp` table.
///
/// Returns rowid if job was created or `None` if SMTP job is not needed, e.g. when sending to a
/// group with only self and no BCC-to-self configured.
///
/// The caller has to interrupt SMTP loop or otherwise process a new row.
async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<i64>> {
let mut msg = Message::load_from_db(context, msg_id).await?;
msg.try_calc_and_set_dimensions(context)
.await
.context("failed to calculate media dimensions")?;
/* create message */
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "job: cannot get selfavatar-state: {}", err);
false
}
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?;
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately.
if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await? != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
msg_id.set_delivered(context).await?;
return Ok(None);
}
let rendered_msg = match mimefactory.render(context).await {
Ok(res) => Ok(res),
Err(err) => {
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
Err(err)
}
}?;
if needs_encryption && !rendered_msg.is_encrypted {
/* unrecoverable */
message::set_msg_failed(
context,
msg_id,
Some("End-to-end-encryption unavailable unexpectedly."),
)
.await;
bail!(
"e2e encryption unavailable {} - {:?}",
msg_id,
needs_encryption
);
}
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
}
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
{
error!(context, "Failed to set msg_location_id: {:?}", err);
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!(context, "Failed to delete sync ids: {:?}", err);
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
}
}
if rendered_msg.is_encrypted && !needs_encryption {
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.update_param(context).await;
}
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
let recipients = recipients.join(" ");
msg.subject = rendered_msg.subject.clone();
msg.update_subject(context).await;
let row_id = context
.sql
.insert(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
paramsv![
&rendered_msg.rfc724_mid,
recipients,
&rendered_msg.message,
msg_id
],
)
.await?;
Ok(Some(row_id))
}
pub async fn send_text_msg(
@@ -2993,6 +3138,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
msg.param.remove(Param::OverrideSenderDisplayname);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimfactory
@@ -3024,8 +3171,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
curr_timestamp += 1;
if let Some(send_job) = job::send_msg_job(context, new_msg_id).await? {
job::add(context, send_job).await?;
if create_send_msg_job(context, new_msg_id).await?.is_some() {
context.interrupt_smtp(InterruptInfo::new(false)).await;
}
}
created_chats.push(chat_id);
@@ -3239,6 +3386,7 @@ pub(crate) async fn add_info_msg_with_cmd(
text: &str,
cmd: SystemMessage,
timestamp: i64,
parent: Option<&Message>,
) -> Result<MsgId> {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
@@ -3250,7 +3398,7 @@ pub(crate) async fn add_info_msg_with_cmd(
let row_id =
context.sql.insert(
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,type,state,txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);",
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to) VALUES (?,?,?, ?,?,?, ?,?,?, ?,?);",
paramsv![
chat_id,
DC_CONTACT_ID_INFO,
@@ -3262,6 +3410,7 @@ pub(crate) async fn add_info_msg_with_cmd(
rfc724_mid,
ephemeral_timer,
param.to_string(),
parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
]
).await?;
@@ -3277,7 +3426,15 @@ pub(crate) async fn add_info_msg(
text: &str,
timestamp: i64,
) -> Result<MsgId> {
add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown, timestamp).await
add_info_msg_with_cmd(
context,
chat_id,
text,
SystemMessage::Unknown,
timestamp,
None,
)
.await
}
#[cfg(test)]
@@ -3441,6 +3598,58 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let quote1 =
Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?)
.await?;
let quote2 =
Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote2".to_string()).await?)
.await?;
// save a draft
let mut draft = Message::new(Viewtype::Text);
draft.set_text(Some("draft text".to_string()));
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("draft text".to_string()));
assert!(test.quoted_text().is_none());
assert!(test.quoted_message(&t).await?.is_none());
// add quote to same message object
draft.set_quote(&t, Some(&quote1)).await?;
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("draft text".to_string()));
assert_eq!(test.quoted_text(), Some("quote1".to_string()));
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id);
// change quote on same message object
draft.set_text(Some("another draft text".to_string()));
draft.set_quote(&t, Some(&quote2)).await?;
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("another draft text".to_string()));
assert_eq!(test.quoted_text(), Some("quote2".to_string()));
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id);
// remove quote on same message object
draft.set_quote(&t, None).await?;
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("another draft text".to_string()));
assert!(test.quoted_text().is_none());
assert!(test.quoted_message(&t).await?.is_none());
Ok(())
}
#[async_std::test]
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
@@ -3862,7 +4071,7 @@ mod tests {
assert_eq!(chats.len(), 1);
// after the device-chat and all messages are deleted, a re-adding should do nothing
chats.get_chat_id(0).delete(&t).await.ok();
chats.get_chat_id(0).unwrap().delete(&t).await.ok();
add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.ok();
@@ -4027,7 +4236,7 @@ mod tests {
.unwrap();
let mut result = Vec::new();
for chatlist_index in 0..chatlist.len() {
result.push(chatlist.get_chat_id(chatlist_index))
result.push(chatlist.get_chat_id(chatlist_index).unwrap())
}
result
}
@@ -4211,34 +4420,37 @@ mod tests {
assert_eq!(msg.get_text().unwrap(), "foo info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::Unknown);
assert!(msg.parent(&t).await?.is_none());
assert!(msg.quoted_message(&t).await?.is_none());
Ok(())
}
#[async_std::test]
async fn test_add_info_msg_with_cmd() {
async fn test_add_info_msg_with_cmd() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let msg_id = add_info_msg_with_cmd(
&t,
chat_id,
"foo bar info",
SystemMessage::EphemeralTimerChanged,
10000,
None,
)
.await
.unwrap();
.await?;
let msg = Message::load_from_db(&t, msg_id).await.unwrap();
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert_eq!(msg.get_text().unwrap(), "foo bar info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged);
assert!(msg.parent(&t).await?.is_none());
assert!(msg.quoted_message(&t).await?.is_none());
let msg2 = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_id(), msg2.get_id());
Ok(())
}
#[async_std::test]
@@ -4390,7 +4602,8 @@ mod tests {
);
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let msg = alice.pop_sent_msg().await.payload();
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Gr.").count(), 2);
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Gr.").count(), 1);
@@ -4410,7 +4623,8 @@ mod tests {
// Bob answers - simulate a normal MUA by not setting `Chat-*`-headers;
// moreover, Bob's SMTP-server also replaces the `Message-ID:`-header
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
let msg = bob.pop_sent_msg().await.payload();
let sent_msg = bob.pop_sent_msg().await;
let msg = sent_msg.payload();
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
let msg = msg.replace("Chat-", "XXXX-");
assert_eq!(msg.match_indices("Chat-").count(), 0);
@@ -4450,7 +4664,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat.id);
assert_eq!(chats.get_chat_id(0)?, chat.id);
assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1);
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
@@ -4498,7 +4712,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
assert!(Chat::load_from_db(&t, chat_id)
.await
.unwrap()
@@ -4546,7 +4760,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0)?;
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
@@ -4555,13 +4769,13 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0)?;
assert!(chat_id.is_archived_link());
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0)?;
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
Ok(())
@@ -4746,7 +4960,7 @@ mod tests {
// Bob quotes received message and sends a reply to Alice.
let mut reply = Message::new(Viewtype::Text);
reply.set_text(Some("Reply".to_owned()));
reply.set_quote(&bob, &received_msg).await?;
reply.set_quote(&bob, Some(&received_msg)).await?;
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
alice.recv_msg(&sent_reply).await;
let received_reply = alice.get_last_msg().await;

View File

@@ -1,6 +1,6 @@
//! # Chat list module.
use anyhow::{bail, ensure, Result};
use anyhow::{ensure, Context as _, Result};
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -271,21 +271,23 @@ impl Chatlist {
/// Get a single chat ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_chat().
pub fn get_chat_id(&self, index: usize) -> ChatId {
match self.ids.get(index) {
Some((chat_id, _msg_id)) => *chat_id,
None => ChatId::new(0),
}
pub fn get_chat_id(&self, index: usize) -> Result<ChatId> {
let (chat_id, _msg_id) = self
.ids
.get(index)
.context("chatlist index is out of range")?;
Ok(*chat_id)
}
/// Get a single message ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_msg().
pub fn get_msg_id(&self, index: usize) -> Result<Option<MsgId>> {
match self.ids.get(index) {
Some((_chat_id, msg_id)) => Ok(*msg_id),
None => bail!("Chatlist index out of range"),
}
let (_chat_id, msg_id) = self
.ids
.get(index)
.context("chatlist index is out of range")?;
Ok(*msg_id)
}
/// Returns a summary for a given chatlist index.
@@ -299,11 +301,10 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => bail!("Chatlist index out of range"),
};
let (chat_id, lastmsg_id) = self
.ids
.get(index)
.context("chatlist index is out of range")?;
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
}
@@ -395,9 +396,9 @@ mod tests {
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0), chat_id3);
assert_eq!(chats.get_chat_id(1), chat_id2);
assert_eq!(chats.get_chat_id(2), chat_id1);
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);
// New drafts are sorted to the top
// We have to set a draft on the other two messages, too, as
@@ -414,7 +415,7 @@ mod tests {
}
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);
// check chatlist query and archive functionality
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
@@ -445,7 +446,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0))
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
.is_self_talk());
@@ -454,7 +455,7 @@ mod tests {
.await
.unwrap();
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t, chats.get_chat_id(0))
assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
.is_self_talk());
@@ -527,7 +528,7 @@ mod tests {
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
@@ -583,7 +584,7 @@ mod tests {
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
assert_eq!(chats.get_chat_id(0)?, chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
@@ -594,7 +595,7 @@ mod tests {
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
assert_eq!(chats.get_chat_id(0)?, chat_id);
// revert name change, this again changes the name of the one-to-one-chat to the email-address
let test_id = Contact::create(&t, "", "bob@example.org").await?;

View File

@@ -10,11 +10,9 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
use crate::events::EventType;
use crate::job;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::stock_str;
/// The available configuration keys.
#[derive(
@@ -67,7 +65,7 @@ pub enum Config {
#[strum(props(default = "1"))]
MdnsEnabled,
#[strum(props(default = "1"))]
#[strum(props(default = "0"))]
SentboxWatch,
#[strum(props(default = "1"))]
@@ -76,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,
@@ -200,7 +205,6 @@ impl Context {
// Default values
match key {
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
_ => Ok(key.get_str("default").map(|s| s.to_string())),
}
@@ -228,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
@@ -236,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))),
}
}
@@ -258,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))),
}
}
@@ -284,55 +293,29 @@ impl Context {
}
}
self.emit_event(EventType::SelfavatarChanged);
Ok(())
}
Config::Selfstatus => {
let def = stock_str::status_line(self).await;
let val = if value.is_none() || value.unwrap() == def {
None
} else {
value
};
self.sql.set_raw_config(key, val).await?;
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(())
}
Config::DeleteServerAfter => {
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
job::schedule_resync(self).await?;
ret
}
_ => {
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(())
}
@@ -424,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(())
}
}

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?;
@@ -454,11 +454,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
drop(imap);
progress!(ctx, 910);
// configuration success - write back the configured parameters with the
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -476,6 +473,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 940);
update_device_chats_handle.await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
Ok(())
}

View File

@@ -295,6 +295,9 @@ pub enum Viewtype {
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an webxdc instance.
Webxdc = 80,
}
impl Default for Viewtype {
@@ -339,6 +342,7 @@ mod tests {
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
}
#[test]

View File

@@ -42,8 +42,6 @@ impl Deref for Context {
#[derive(Debug)]
pub struct InnerContext {
/// Database file path
pub(crate) dbfile: PathBuf,
/// Blob directory path
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
@@ -84,7 +82,7 @@ pub struct InnerContext {
#[derive(Debug)]
pub struct RunningState {
pub ongoing_running: bool,
ongoing_running: bool,
shall_stop_ongoing: bool,
cancel_sender: Option<Sender<()>>,
}
@@ -106,10 +104,19 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
}
impl Context {
/// Creates new context.
/// Creates new context and opens the database.
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
// pretty_env_logger::try_init_timed().ok();
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)
}
/// Creates new context without opening the database.
pub async fn new_closed(dbfile: PathBuf, id: u32) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -117,7 +124,35 @@ impl Context {
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
}
Context::with_blobdir(dbfile, blobdir, id).await
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(
@@ -134,9 +169,8 @@ impl Context {
let inner = InnerContext {
id,
blobdir,
dbfile,
running_state: RwLock::new(Default::default()),
sql: Sql::new(),
sql: Sql::new(dbfile),
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
@@ -155,7 +189,6 @@ impl Context {
let ctx = Context {
inner: Arc::new(inner),
};
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
Ok(ctx)
}
@@ -193,7 +226,7 @@ impl Context {
/// Returns database file path.
pub fn get_dbfile(&self) -> &Path {
self.dbfile.as_path()
self.sql.dbfile.as_path()
}
/// Returns blob directory path.
@@ -224,7 +257,7 @@ impl Context {
// Ongoing process allocation/free/check
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
if self.has_ongoing().await {
bail!("There is already another ongoing process running.");
}
@@ -240,7 +273,7 @@ impl Context {
Ok(receiver)
}
pub async fn free_ongoing(&self) {
pub(crate) async fn free_ongoing(&self) {
let s_a = &self.running_state;
let mut s = s_a.write().await;
@@ -249,7 +282,7 @@ impl Context {
s.cancel_sender.take();
}
pub async fn has_ongoing(&self) -> bool {
pub(crate) async fn has_ongoing(&self) -> bool {
let s_a = &self.running_state;
let s = s_a.read().await;
@@ -274,7 +307,7 @@ impl Context {
};
}
pub async fn shall_stop_ongoing(&self) -> bool {
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
self.running_state.read().await.shall_stop_ongoing
}
@@ -325,6 +358,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")
@@ -350,6 +384,13 @@ impl Context {
res.insert("number_of_contacts", contacts.to_string());
res.insert("database_dir", self.get_dbfile().display().to_string());
res.insert("database_version", dbversion.to_string());
res.insert(
"database_encrypted",
self.sql
.is_encrypted()
.await
.map_or_else(|| "closed".to_string(), |b| b.to_string()),
);
res.insert("journal_mode", journal_mode);
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
@@ -382,6 +423,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);
@@ -574,14 +616,14 @@ impl Context {
Ok(spam.as_deref() == Some(folder_name))
}
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
pub(crate) fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
dbfile.with_file_name(blob_fname)
}
pub fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
pub(crate) fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
let mut wal_fname = OsString::new();
wal_fname.push(dbfile.file_name().unwrap_or_default());
wal_fname.push("-wal");
@@ -638,16 +680,21 @@ 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() {
let tmp = tempfile::tempdir().unwrap();
async fn test_wrong_db() -> Result<()> {
let tmp = tempfile::tempdir()?;
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_err());
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(())
}
#[async_std::test]
@@ -999,4 +1046,29 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_check_passphrase() -> Result<()> {
let dir = tempdir()?;
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);
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);
Ok(())
}
}

View File

@@ -17,7 +17,9 @@ use crate::constants::{
Blocked, Chattype, ShowEmails, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_LAST_SPECIAL,
DC_CONTACT_ID_SELF,
};
use crate::contact::{addr_cmp, normalize_name, Contact, Origin, VerifiedStatus};
use crate::contact::{
addr_cmp, may_be_valid_addr, normalize_name, Contact, Origin, VerifiedStatus,
};
use crate::context::Context;
use crate::dc_tools::{dc_extract_grpid_from_rfc724_mid, dc_smeared_time};
use crate::download::DownloadState;
@@ -191,7 +193,6 @@ pub(crate) async fn dc_receive_imf_inner(
&mut mime_parser,
imf_raw,
incoming,
incoming_origin,
server_folder,
&to_ids,
&rfc724_mid,
@@ -207,7 +208,7 @@ pub(crate) async fn dc_receive_imf_inner(
prevent_rename,
)
.await
.map_err(|err| err.context("add_parts error"))?;
.context("add_parts error")?;
if from_id > DC_CONTACT_ID_LAST_SPECIAL {
contact::update_last_seen(context, from_id, sent_timestamp).await?;
@@ -256,6 +257,15 @@ pub(crate) async fn dc_receive_imf_inner(
}
}
if let Some(ref status_update) = mime_parser.webxdc_status_update {
if let Err(err) = context
.receive_status_update(insert_msg_id, status_update)
.await
{
warn!(context, "receive_imf cannot update status: {}", err);
}
}
if let Some(avatar_action) = &mime_parser.user_avatar {
if from_id != 0
&& context
@@ -404,7 +414,6 @@ async fn add_parts(
mime_parser: &mut MimeMessage,
imf_raw: &[u8],
incoming: bool,
incoming_origin: Origin,
server_folder: &str,
to_ids: &[u32],
rfc724_mid: &str,
@@ -421,7 +430,6 @@ async fn add_parts(
) -> Result<ReceivedMsg> {
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
let mut incoming_origin = incoming_origin;
let parent = get_parent_message(context, mime_parser).await?;
@@ -665,7 +673,6 @@ async fn add_parts(
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if create_blocked == Blocked::Request && parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
@@ -676,9 +683,6 @@ async fn add_parts(
context,
"Message is a reply to a known message, mark sender as known.",
);
if !incoming_origin.is_known() {
incoming_origin = Origin::IncomingReplyTo;
}
}
}
}
@@ -690,15 +694,6 @@ async fn add_parts(
} else {
MessageState::InFresh
};
let is_spam = (chat_id_blocked == Blocked::Request)
&& !incoming_origin.is_known()
&& (is_dc_message == MessengerMessage::No)
&& context.is_spam_folder(server_folder).await?;
if is_spam {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is probably spam (TRASH)");
}
} else {
// Outgoing
@@ -858,6 +853,15 @@ async fn add_parts(
info!(context, "Existing non-decipherable message. (TRASH)");
}
if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
if let Some(part) = mime_parser.parts.first() {
if part.typ == Viewtype::Text && part.msg.is_empty() {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is a status update only (TRASH)");
}
}
}
if is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
}
@@ -1136,8 +1140,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,
@@ -1843,11 +1847,11 @@ async fn create_or_lookup_mailinglist(
param,
)
.await
.map_err(|err| {
err.context(format!(
.with_context(|| {
format!(
"Failed to create mailinglist '{}' for grpid={}",
&name, &listid
))
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await?;
@@ -2284,14 +2288,17 @@ async fn dc_add_or_lookup_contacts_by_address_list(
) -> Result<Vec<u32>> {
let mut contact_ids = BTreeSet::new();
for info in address_list.iter() {
let addr = &info.addr;
if !may_be_valid_addr(addr) {
continue;
}
let display_name = if prevent_rename {
Some("")
} else {
info.display_name.as_deref()
};
contact_ids.insert(
add_or_lookup_contact_by_addr(context, display_name, &info.addr, origin).await?,
);
contact_ids
.insert(add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?);
}
Ok(contact_ids.into_iter().collect::<Vec<u32>>())
@@ -2340,12 +2347,10 @@ fn dc_create_incoming_rfc724_mid(mime: &MimeMessage) -> String {
#[cfg(test)]
mod tests {
use chat::get_chat_contacts;
use mailparse::MailHeaderMap;
use super::*;
use crate::chat::get_chat_contacts;
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
use crate::constants::{DC_CONTACT_ID_INFO, DC_GCL_NO_SPECIALS};
@@ -2500,7 +2505,7 @@ mod tests {
dc_receive_imf(&t, MSGRMSG, "INBOX", false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
assert!(!chat_id.is_special());
let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(chat.is_contact_request());
@@ -2534,7 +2539,7 @@ mod tests {
dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(chat.name, "group with Alice, Bob and Claire");
@@ -2550,7 +2555,7 @@ mod tests {
// adhoc-group with unknown contacts with show_emails=all will show up in a single chat
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(chat.is_contact_request());
chat_id.accept(&t).await.unwrap();
@@ -2954,7 +2959,7 @@ mod tests {
&headers,
"some-other-message-id",
std::iter::empty(),
ShowEmails::Off
ShowEmails::Off,
)
.await
.unwrap());
@@ -3073,7 +3078,7 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(&t).await.unwrap();
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
@@ -3141,7 +3146,7 @@ mod tests {
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(&t).await.unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert_eq!(chat.name, "delta-dev");
@@ -3167,10 +3172,7 @@ mod tests {
assert!(mime.contains(
"\r\n\
\r\n\
Hello mailinglist!\r\n\
\r\n\
-- \r\n\
Sent with my Delta Chat Messenger: https://delta.chat\r\n"
Hello mailinglist!\r\n"
));
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false).await?;
@@ -3247,7 +3249,7 @@ Sent with my Delta Chat Messenger: https://delta.chat\r\n"
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert!(chat.is_contact_request());
@@ -4278,76 +4280,6 @@ YEAAAAAA!.
assert_eq!(msg.text.unwrap(), "Reply");
}
#[async_std::test]
async fn test_dont_show_spam() {
async fn is_shown(t: &TestContext, raw: &[u8], server_folder: &str) -> bool {
let mail = mailparse::parse_mail(raw).unwrap();
dc_receive_imf(t, raw, server_folder, false).await.unwrap();
t.get_last_msg().await.rfc724_mid
== mail.get_headers().get_first_value("Message-Id").unwrap()
}
let t = TestContext::new_alice().await;
t.set_config(Config::ConfiguredSpamFolder, Some("Spam"))
.await
.unwrap();
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
assert!(
is_shown(
&t,
b"Message-Id: abcd1@exmaple.com\n\
From: bob@example.org\n\
Chat-Version: 1.0\n",
"Inbox",
)
.await,
);
assert!(
is_shown(
&t,
b"Message-Id: abcd2@exmaple.com\n\
From: bob@example.org\n",
"Inbox",
)
.await,
);
assert!(
is_shown(
&t,
b"Message-Id: abcd3@exmaple.com\n\
From: bob@example.org\n\
Chat-Version: 1.0\n",
"Spam",
)
.await,
);
assert!(
// Note the `!`:
!is_shown(
&t,
b"Message-Id: abcd4@exmaple.com\n\
From: bob@example.org\n",
"Spam",
)
.await,
);
Contact::create(&t, "", "bob@example.org").await.unwrap();
assert!(
is_shown(
&t,
b"Message-Id: abcd5@exmaple.com\n\
From: bob@example.org\n",
"Spam",
)
.await,
);
}
#[async_std::test]
async fn test_dont_show_all_outgoing_msgs_in_self_chat() {
// Regression test for <https://github.com/deltachat/deltachat-android/issues/1940>:
@@ -4846,7 +4778,7 @@ Second thread."#;
let mdn_body = rendered_mdn.message;
// Alice receives the read receipt.
dc_receive_imf(&alice, &mdn_body, "INBOX", false).await?;
dc_receive_imf(&alice, mdn_body.as_bytes(), "INBOX", false).await?;
// Chat should not pop up in the chatlist.
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
@@ -4989,4 +4921,16 @@ Message with references."#;
Ok(())
}
#[async_std::test]
async fn test_invalid_to_address() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime = include_bytes!("../test-data/message/invalid_email_to.eml");
// dc_receive_imf should not fail on this mail with invalid To: field
dc_receive_imf(&alice, mime, "Inbox", false).await?;
Ok(())
}
}

View File

@@ -13,7 +13,7 @@ use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{bail, Error};
use anyhow::Error;
use chrono::{Local, TimeZone};
use mailparse::dateparse;
use mailparse::headers::Headers;
@@ -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.
@@ -354,63 +336,6 @@ pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
}
}
pub(crate) async fn dc_copy_file(
context: &Context,
src_path: impl AsRef<Path>,
dest_path: impl AsRef<Path>,
) -> bool {
let src_abs = dc_get_abs_path(context, &src_path);
let mut src_file = match fs::File::open(&src_abs).await {
Ok(file) => file,
Err(err) => {
warn!(
context,
"failed to open for read '{}': {}",
src_abs.display(),
err
);
return false;
}
};
let dest_abs = dc_get_abs_path(context, &dest_path);
let mut dest_file = match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&dest_abs)
.await
{
Ok(file) => file,
Err(err) => {
warn!(
context,
"failed to open for write '{}': {}",
dest_abs.display(),
err
);
return false;
}
};
match io::copy(&mut src_file, &mut dest_file).await {
Ok(_) => true,
Err(err) => {
error!(
context,
"Cannot copy \"{}\" to \"{}\": {}",
src_abs.display(),
dest_abs.display(),
err
);
{
// Attempt to remove the failed file, swallow errors resulting from that.
fs::remove_file(dest_abs).await.ok();
}
false
}
}
}
pub(crate) async fn dc_create_folder(
context: &Context,
path: impl AsRef<Path>,
@@ -508,33 +433,6 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
}
}
/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
pub(crate) async fn get_next_backup_path(
folder: impl AsRef<Path>,
backup_time: i64,
) -> Result<(PathBuf, PathBuf), Error> {
let folder = PathBuf::from(folder.as_ref());
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
.to_string();
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut tempfile = folder.clone();
tempfile.push(format!("{}-{:02}.tar.part", stem, i));
let mut destfile = folder.clone();
destfile.push(format!("{}-{:02}.tar", stem, i));
if !tempfile.exists().await && !destfile.exists().await {
return Ok((tempfile, destfile));
}
}
bail!("could not create backup file, disk full?");
}
pub(crate) fn time() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@@ -864,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
@@ -1025,20 +903,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert!(dc_file_exist!(context, &abs_path).await);
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
// attempting to copy a second time should fail
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7);
let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap();
assert_eq!(buf.len(), 7);
assert_eq!(&buf, b"content");
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
assert!(dc_delete_file(context, "$BLOBDIR/dada").await);
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
.await
.is_ok());
@@ -1164,7 +1029,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let device_chat_id = chats.get_chat_id(0).unwrap();
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
@@ -1202,7 +1067,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap());
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
@@ -1234,7 +1099,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let device_chat_id = chats.get_chat_id(0).unwrap();
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
@@ -1256,7 +1121,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let device_chat_id = chats.get_chat_id(0).unwrap();
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
@@ -1273,7 +1138,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let device_chat_id = chats.get_chat_id(0).unwrap();
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();

View File

@@ -196,9 +196,13 @@ impl Imap {
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
let (last_uid, _received) = self
let (last_uid, _received) = match self
.fetch_many_msgs(context, folder, vec![uid], false, false)
.await;
.await
{
Ok(res) => res,
Err(_) => return ImapActionResult::Failed,
};
if last_uid.is_none() {
ImapActionResult::Failed
} else {

View File

@@ -2,7 +2,7 @@
use std::collections::HashSet;
use anyhow::{bail, format_err, Result};
use anyhow::{bail, format_err, Context as _, Result};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
@@ -121,9 +121,9 @@ impl EncryptHelper {
.into_iter()
.filter_map(|(state, addr)| state.map(|s| (s, addr)))
{
let key = peerstate.take_key(min_verified).ok_or_else(|| {
format_err!("proper enc-key for {} missing, cannot encrypt", addr)
})?;
let key = peerstate
.take_key(min_verified)
.with_context(|| format!("proper enc-key for {} missing, cannot encrypt", addr))?;
keyring.add(key);
}
keyring.add(self.public_key.clone());
@@ -390,12 +390,10 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",
"cannot ensure secret key if not configured."
))
})?;
.context(concat!(
"Failed to get self address, ",
"cannot ensure secret key if not configured."
))?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
}

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],
)

View File

@@ -9,6 +9,7 @@ use strum::EnumProperty;
use crate::chat::ChatId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateId;
#[derive(Debug)]
pub struct Events {
@@ -326,4 +327,10 @@ pub enum EventType {
#[strum(props(id = "2110"))]
SelfavatarChanged,
#[strum(props(id = "2120"))]
WebxdcStatusUpdate {
msg_id: MsgId,
status_update_id: StatusUpdateId,
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use super::Imap;
use anyhow::{bail, format_err, Result};
use anyhow::{bail, Context as _, Result};
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
@@ -31,7 +31,7 @@ impl Imap {
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if self.server_sent_unsolicited_exists(context) {
if self.server_sent_unsolicited_exists(context)? {
return Ok(info);
}
@@ -90,8 +90,8 @@ impl Imap {
let session = handle
.done()
.timeout(Duration::from_secs(15))
.await
.map_err(|err| format_err!("IMAP IDLE protocol timed out: {}", err))??;
.await?
.context("IMAP IDLE protocol timed out")?;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
@@ -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);

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");
@@ -71,7 +72,7 @@ impl Imap {
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
// Drain leftover unsolicited EXISTS messages
self.server_sent_unsolicited_exists(context);
self.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name())
@@ -79,7 +80,7 @@ impl Imap {
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !self.server_sent_unsolicited_exists(context) {
if !self.server_sent_unsolicited_exists(context)? {
break;
}
}
@@ -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)

View File

@@ -93,7 +93,11 @@ impl Imap {
// select new folder
if let Some(folder) = folder {
if let Some(ref mut session) = &mut self.session {
let res = session.select(folder).await;
let res = if self.config.can_condstore {
session.select_condstore(folder).await
} else {
session.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in

View File

@@ -19,8 +19,8 @@ use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::{
dc_copy_file, dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
dc_open_file_std, dc_read_file, dc_write_file, get_next_backup_path, time, EmailAddress,
dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
dc_open_file_std, dc_read_file, dc_write_file, time, EmailAddress,
};
use crate::e2ee;
use crate::events::EventType;
@@ -30,7 +30,7 @@ use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::sql;
use crate::stock_str;
// Name of the database file in the backup.
@@ -41,24 +41,24 @@ const BLOBS_BACKUP_NAME: &str = "blobs_backup";
#[repr(u32)]
pub enum ImexMode {
/// Export all private keys and all public keys of the user to the
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
/// directory given as `path`. The default key is written to the files `public-key-default.asc`
/// and `private-key-default.asc`, if there are more keys, they are written to files as
/// `public-key-<id>.asc` and `private-key-<id>.asc`
ExportSelfKeys = 1,
/// Import private keys found in the directory given as `param1`.
/// Import private keys found in the directory given as `path`.
/// The last imported key is made the default keys unless its name contains the string `legacy`.
/// Public keys are not imported.
ImportSelfKeys = 2,
/// Export a backup to the directory given as `param1`.
/// Export a backup to the directory given as `path` with the given `passphrase`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
/// the format is `delta-chat-<day>-<number>.tar`
ExportBackup = 11,
/// `param1` is the file (not: directory) to import. The file is normally
/// `path` is the file (not: directory) to import. The file is normally
/// created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
/// is only possible as long as the context is not configured or used in another way.
ImportBackup = 12,
@@ -78,11 +78,16 @@ pub enum ImexMode {
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
pub async fn imex(context: &Context, what: ImexMode, param1: &Path) -> Result<()> {
pub async fn imex(
context: &Context,
what: ImexMode,
path: &Path,
passphrase: Option<String>,
) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
let res = async {
let success = imex_inner(context, what, param1).await;
let success = imex_inner(context, what, path, passphrase).await;
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
@@ -115,15 +120,10 @@ async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
dc_delete_file(context, context.get_dbfile()).await;
dc_delete_files_in_dir(context, context.get_blobdir()).await;
}
if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup {
if let Err(e) = context.sql.open(context, context.get_dbfile(), false).await {
warn!(context, "Re-opening db after imex failed: {}", e);
}
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup(context: &Context, dir_name: &Path) -> Result<String> {
pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
@@ -145,59 +145,6 @@ pub async fn has_backup(context: &Context, dir_name: &Path) -> Result<String> {
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => has_backup_old(context, dir_name).await,
// When we decide to remove support for .bak backups, we can replace this with `None => bail!("no backup found in {}", dir_name.display()),`.
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String> {
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_time = 0;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
let path = dirent.path();
let name = dirent.file_name();
let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new();
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int("backup_time")
.await?
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
Err(e) => {
warn!(
context,
"Found backup file {} which could not be opened: {}", name, e
);
// On some Android devices we can't open sql files that are not in our private directory
// (see <https://github.com/deltachat/deltachat-android/issues/1768>). So, compare names
// to still find the newest backup.
let name: String = name.into();
if newest_backup_time == 0
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
newest_backup_path = Some(path);
newest_backup_name = name;
}
}
}
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => bail!("no backup found in {}", dir_name.display()),
@@ -449,7 +396,12 @@ fn normalize_setup_code(s: &str) -> String {
out
}
async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<()> {
async fn imex_inner(
context: &Context,
what: ImexMode,
path: &Path,
passphrase: Option<String>,
) -> Result<()> {
info!(context, "Import/export dir: {}", path.display());
ensure!(context.sql.is_open().await, "Database not opened.");
context.emit_event(EventType::ImexProgress(10));
@@ -467,19 +419,27 @@ async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<()
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
ImexMode::ExportBackup => export_backup(context, path).await,
// import_backup() will call import_backup_old() if this is an old backup.
ImexMode::ImportBackup => import_backup(context, path).await,
ImexMode::ExportBackup => {
export_backup(context, path, passphrase.unwrap_or_default()).await
}
ImexMode::ImportBackup => {
import_backup(context, path, passphrase.unwrap_or_default()).await?;
context.sql.run_migrations(context).await
}
}
}
/// Import Backup
async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> {
if backup_to_import.to_string_lossy().ends_with(".bak") {
// Backwards compability
return import_backup_old(context, backup_to_import).await;
}
/// Imports backup into the currently open database.
///
/// The contents of the currently open database will be lost.
///
/// `passphrase` is the passphrase used to open backup database. If backup is unencrypted, pass
/// empty string here.
async fn import_backup(
context: &Context,
backup_to_import: &Path,
passphrase: String,
) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
@@ -495,12 +455,6 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
!context.scheduler.read().await.is_running(),
"cannot import backup, IO already running"
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
!context.get_dbfile().exists().await,
"Cannot delete old database."
);
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
@@ -522,11 +476,15 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
f.unpack_in(context.get_blobdir()).await?;
fs::rename(
context.get_blobdir().join(DBFILE_BACKUP_NAME),
context.get_dbfile(),
)
.await?;
let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
context
.sql
.import(&unpacked_database, passphrase.clone())
.await
.context("cannot import unpacked database")?;
fs::remove_file(unpacked_database)
.await
.context("cannot remove unpacked database")?;
} else {
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
@@ -541,136 +499,52 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
}
}
context
.sql
.open(context, context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
}
async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
backup_to_import.display(),
context.get_dbfile().display()
);
ensure!(
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
ensure!(
!context.scheduler.read().await.is_running(),
"cannot import backup, IO already running"
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
!context.get_dbfile().exists().await,
"Cannot delete old database."
);
ensure!(
dc_copy_file(context, backup_to_import, context.get_dbfile()).await,
"could not copy file"
);
/* error already logged */
/* re-open copied database file */
context
.sql
.open(context, context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(context).await?;
let total_files_cnt = context
.sql
.count("SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.await?;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
// Load IDs only for now, without the file contents, to avoid
// consuming too much memory.
let file_ids = context
.sql
.query_map(
"SELECT id FROM backup_blobs ORDER BY id",
paramsv![],
|row| row.get(0),
|ids| {
ids.collect::<std::result::Result<Vec<i64>, _>>()
.map_err(Into::into)
},
)
.await?;
let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
// Load a single blob into memory
let (file_name, file_blob) = context
.sql
.query_row(
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
paramsv![file_id],
|row| {
let file_name: String = row.get(0)?;
let file_blob: Vec<u8> = row.get(1)?;
Ok((file_name, file_blob))
},
)
.await?;
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
}
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
}
if permille > 990 {
permille = 990
}
context.emit_event(EventType::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
}
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
context
.sql
.execute("DROP TABLE backup_blobs;", paramsv![])
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
Ok(())
} else {
bail!("received stop signal");
}
}
/*******************************************************************************
* Export backup
******************************************************************************/
#[allow(unused)]
async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
async fn get_next_backup_path(
folder: &Path,
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
.to_string();
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut tempdbfile = folder.clone();
tempdbfile.push(format!("{}-{:02}.db", stem, i));
let mut tempfile = folder.clone();
tempfile.push(format!("{}-{:02}.tar.part", stem, i));
let mut destfile = folder.clone();
destfile.push(format!("{}-{:02}.tar", stem, i));
if !tempdbfile.exists().await && !tempfile.exists().await && !destfile.exists().await {
return Ok((tempdbfile, tempfile, destfile));
}
}
bail!("could not create backup file, disk full?");
}
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let (temp_path, dest_path) = get_next_backup_path(dir, now).await?;
let _d = DeleteOnDrop(temp_path.clone());
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now).await?;
let _d1 = DeleteOnDrop(temp_db_path.clone());
let _d2 = DeleteOnDrop(temp_path.clone());
context
.sql
@@ -682,16 +556,14 @@ async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
.sql
.execute("VACUUM;", paramsv![])
.await
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e))
.ok();
ensure!(
!context.scheduler.read().await.is_running(),
"cannot export backup, IO already running"
);
// we close the database during the export
context.sql.close().await;
info!(
context,
"Backup '{}' to '{}'.",
@@ -699,10 +571,13 @@ async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
dest_path.display(),
);
let res = export_backup_inner(context, &temp_path).await;
context
.sql
.export(&temp_db_path, passphrase)
.await
.with_context(|| format!("failed to backup plaintext database to {:?}", temp_db_path))?;
// we re-open the database after export is finished
context.sql.open(context, context.get_dbfile(), false).await;
let res = export_backup_inner(context, &temp_db_path, &temp_path).await;
match &res {
Ok(_) => {
@@ -721,18 +596,21 @@ impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
async_std::task::block_on(async move { fs::remove_file(file).await.ok() });
async_std::task::block_on(fs::remove_file(file)).ok();
}
}
async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<()> {
async fn export_backup_inner(
context: &Context,
temp_db_path: &Path,
temp_path: &Path,
) -> Result<()> {
let file = File::create(temp_path).await?;
let mut builder = async_tar::Builder::new(file);
// append_path_with_name() wants the source path as the first argument, append_dir_all() wants it as the second argument.
builder
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
.await?;
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
@@ -1011,12 +889,12 @@ mod tests {
async fn test_export_and_import_key() {
let context = TestContext::new_alice().await;
let blobdir = context.ctx.get_blobdir();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
panic!("got error on export: {:?}", err);
}
let context2 = TestContext::new_alice().await;
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
panic!("got error on import: {:?}", err);
}
}

View File

@@ -3,28 +3,24 @@
//! This module implements a job queue maintained in the SQLite database
//! and job types.
use std::fmt;
use std::future::Future;
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
use async_smtp::smtp::response::{Category, Code, Detail};
use anyhow::{bail, format_err, Context as _, Error, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::contact::{normalize_name, Contact, Modifier, Origin};
use crate::context::Context;
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
use crate::dc_tools::time;
use crate::events::EventType;
use crate::imap::{Imap, ImapActionResult};
use crate::location;
use crate::log::LogExt;
use crate::message::{self, Message, MessageState, MsgId};
use crate::message::{Message, MsgId};
use crate::mimefactory::MimeFactory;
use crate::param::{Param, Params};
use crate::scheduler::InterruptInfo;
use crate::smtp::Smtp;
use crate::smtp::{smtp_send, Smtp};
use crate::sql;
// results in ~3 weeks for the last backoff timespan
@@ -109,7 +105,6 @@ pub enum Action {
MaybeSendLocations = 5005, // low priority ...
MaybeSendLocationsEnded = 5007,
SendMdn = 5010,
SendMsgToSmtp = 5901, // ... high priority
}
impl Default for Action {
@@ -135,7 +130,6 @@ impl From<Action> for Thread {
MaybeSendLocations => Thread::Smtp,
MaybeSendLocationsEnded => Thread::Smtp,
SendMdn => Thread::Smtp,
SendMsgToSmtp => Thread::Smtp,
}
}
}
@@ -203,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,
],
@@ -226,217 +220,6 @@ impl Job {
Ok(())
}
async fn smtp_send<F, Fut>(
&mut self,
context: &Context,
recipients: Vec<async_smtp::EmailAddress>,
message: Vec<u8>,
job_id: u32,
smtp: &mut Smtp,
success_cb: F,
) -> Status
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<()>>,
{
// hold the smtp lock during sending of a job and
// its ok/error response processing. Note that if a message
// was sent we need to mark it in the database ASAP as we
// otherwise might send it twice.
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "smtp-sending out mime message:");
println!("{}", String::from_utf8_lossy(&message));
}
smtp.connectivity.set_working(context).await;
let send_result = smtp.send(context, recipients, message, job_id).await;
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {:?}", &err);
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
// Workaround for incorrectly configured servers returning permanent errors
// instead of temporary ones.
let maybe_transient = match response.code {
// Sometimes servers send a permanent error when actually it is a temporary error
// For documentation see <https://tools.ietf.org/html/rfc3463>
Code {
category: Category::MailSystem,
detail: Detail::Zero,
..
} => {
// Ignore status code 5.5.0, see <https://support.delta.chat/t/every-other-message-gets-stuck/877/2>
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
//
// Other enhanced status codes, such as Postfix
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
// are not ignored.
response.first_word() == Some(&"5.5.0".to_string())
}
_ => false,
};
if maybe_transient {
Status::RetryLater
} else {
// If we do not retry, add an info message to the chat.
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
// should definitely go here, because user has to open the link to
// resume message sending.
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
}
async_smtp::smtp::error::Error::Transient(ref response) => {
// We got a transient 4xx response from SMTP server.
// Give some time until the server-side error maybe goes away.
if let Some(first_word) = response.first_word() {
if first_word.ends_with(".1.1")
|| first_word.ends_with(".1.2")
|| first_word.ends_with(".1.3")
{
// Sometimes we receive transient errors that should be permanent.
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
// receive as a transient error are misconfigurations of the smtp server.
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
info!(context, "Smtp-job #{} Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", self.job_id, first_word);
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
} else {
Status::RetryLater
}
} else {
Status::RetryLater
}
}
_ => {
if smtp.has_maybe_stale_connection().await {
info!(context, "stale connection? immediately reconnecting");
Status::RetryNow
} else {
Status::RetryLater
}
}
};
// this clears last_success info
smtp.disconnect().await;
res
}
Err(crate::smtp::send::Error::Envelope(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "SMTP job is invalid: {}", err);
Status::Finished(Err(err.into()))
}
Err(crate::smtp::send::Error::NoTransport) => {
// Should never happen.
// It does not even make sense to disconnect here.
error!(context, "SMTP job failed because SMTP has no transport");
Status::Finished(Err(format_err!("SMTP has not transport")))
}
Err(crate::smtp::send::Error::Other(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "unable to load job: {}", err);
Status::Finished(Err(err))
}
Ok(()) => {
job_try!(success_cb().await);
Status::Finished(Ok(()))
}
};
if let Status::Finished(Err(err)) = &status {
// We couldn't send the message, so mark it as failed
let msg_id = MsgId::new(self.foreign_id);
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
}
status
}
pub(crate) async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
// SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
smtp.last_send_error = Some(format!("SMTP connection failure: {:#}", err));
return Status::RetryLater;
}
let filename = job_try!(job_try!(self
.param
.get_path(Param::File, context)
.map_err(|_| format_err!("Can't get filename")))
.ok_or_else(|| format_err!("Can't get filename")));
let body = job_try!(dc_read_file(context, &filename).await);
let recipients = job_try!(self.param.get(Param::Recipients).ok_or_else(|| {
warn!(context, "Missing recipients for job {}", self.job_id);
format_err!("Missing recipients")
}));
let recipients_list = recipients
.split('\x1e')
.filter_map(
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
Ok(addr) => Some(addr),
Err(err) => {
warn!(context, "invalid recipient: {} {:?}", addr, err);
None
}
},
)
.collect::<Vec<_>>();
/* if there is a msg-id and it does not exist in the db, cancel sending.
this happends if dc_delete_msgs() was called
before the generated mime was sent out */
if 0 != self.foreign_id {
match message::exists(context, MsgId::new(self.foreign_id)).await {
Ok(exists) => {
if !exists {
return Status::Finished(Err(format_err!(
"Not sending Message {} as it was deleted",
self.foreign_id
)));
}
}
Err(err) => {
warn!(context, "failed to check message existence: {:?}", err);
smtp.last_send_error =
Some(format!("failed to check message existence: {:#}", err));
return Status::RetryLater;
}
}
};
let foreign_id = self.foreign_id;
self.smtp_send(context, recipients_list, body, self.job_id, smtp, || {
async move {
// smtp success, update db ASAP, then delete smtp file
if 0 != foreign_id {
set_delivered(context, MsgId::new(foreign_id)).await?;
}
// now also delete the generated file
dc_delete_file(context, filename).await;
// finally, create another send-job if there are items to be synced.
// triggering sync-job after msg-send-job guarantees, the recipient has grpid etc.
// once the sync message arrives.
// if there are no items to sync, this function returns fast.
context.send_sync_msg().await?;
Ok(())
}
})
.await
}
/// Get `SendMdn` jobs with foreign_id equal to `contact_id` excluding the `job_id` job.
async fn get_additional_mdn_jobs(
&self,
@@ -535,14 +318,13 @@ impl Job {
return Status::RetryLater;
}
self.smtp_send(context, recipients, body, self.job_id, smtp, || {
async move {
// Remove additional SendMdn jobs we have aggregated into this one.
kill_ids(context, &additional_job_ids).await?;
Ok(())
}
})
.await
let status = smtp_send(context, &recipients, &body, smtp, msg_id, 0).await;
if matches!(status, Status::Finished(Ok(_))) {
// Remove additional SendMdn jobs we have aggregated into this one.
job_try!(kill_ids(context, &additional_job_ids).await);
}
status
}
/// Read the recipients from old emails sent by the user and add them as contacts.
@@ -583,15 +365,7 @@ impl Job {
Status::Finished(Ok(()))
}
/// Synchronizes UIDs for sentbox, inbox and mvbox, in this order.
///
/// If a copy of the message is present in multiple folders, mvbox
/// is preferred to inbox, which is in turn preferred to
/// sentbox. This is because in the database it is impossible to
/// store multiple UIDs for one message, so we prefer to
/// automatically delete messages in the folders managed by Delta
/// Chat in contrast to the Sent folder, which is normally managed
/// by the user via webmail or another email client.
/// Synchronizes UIDs for sentbox, inbox and mvbox.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:?}", err);
@@ -711,17 +485,6 @@ pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
Ok(exists)
}
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
let chat_id: ChatId = context
.sql
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![msg_id])
.await?
.unwrap_or_default();
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
Ok(())
}
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
m
@@ -767,132 +530,6 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
};
}
/// Constructs a job for sending a message.
///
/// Returns `None` if no messages need to be sent out.
///
/// In order to be processed, must be `add`ded.
pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job>> {
let mut msg = Message::load_from_db(context, msg_id).await?;
msg.try_calc_and_set_dimensions(context).await.ok();
/* create message */
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let attach_selfavatar = match chat::shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "job: cannot get selfavatar-state: {}", err);
false
}
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?;
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately.
if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await? != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id).await?;
return Ok(None);
}
let rendered_msg = match mimefactory.render(context).await {
Ok(res) => Ok(res),
Err(err) => {
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
Err(err)
}
}?;
if needs_encryption && !rendered_msg.is_encrypted {
/* unrecoverable */
message::set_msg_failed(
context,
msg_id,
Some("End-to-end-encryption unavailable unexpectedly."),
)
.await;
bail!(
"e2e encryption unavailable {} - {:?}",
msg_id,
needs_encryption
);
}
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
}
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
{
error!(context, "Failed to set msg_location_id: {:?}", err);
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!(context, "Failed to delete sync ids: {:?}", err);
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
}
}
if rendered_msg.is_encrypted && !needs_encryption {
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.update_param(context).await;
}
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
let mut param = Params::new();
let bytes = &rendered_msg.message;
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes).await?;
let recipients = recipients.join("\x1e");
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
msg.subject = rendered_msg.subject.clone();
msg.update_subject(context).await;
let job = create(Action::SendMsgToSmtp, msg_id.to_u32(), param, 0)?;
Ok(Some(job))
}
pub(crate) enum Connection<'a> {
Inbox(&'a mut Imap),
Smtp(&'a mut Smtp),
@@ -1000,7 +637,6 @@ async fn perform_job_action(
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMsgToSmtp => job.send_msg_to_smtp(context, connection.smtp()).await,
Action::SendMdn => job.send_mdn(context, connection.smtp()).await,
Action::MaybeSendLocations => location::job_maybe_send_locations(context, job).await,
Action::MaybeSendLocationsEnded => {
@@ -1040,7 +676,7 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
if seconds < 1 {
seconds = 1;
}
seconds as i64
i64::from(seconds)
}
}
}
@@ -1064,16 +700,6 @@ pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
Ok(())
}
/// Creates a job.
pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Result<Job> {
ensure!(
action != Action::Unknown,
"Invalid action passed to job_add"
);
Ok(Job::new(action, foreign_id, param, delay_seconds))
}
/// Adds a job to the database, scheduling it.
pub async fn add(context: &Context, job: Job) -> Result<()> {
let action = job.action;
@@ -1092,10 +718,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
info!(context, "interrupt: imap");
context.interrupt_inbox(InterruptInfo::new(false)).await;
}
Action::MaybeSendLocations
| Action::MaybeSendLocationsEnded
| Action::SendMdn
| Action::SendMsgToSmtp => {
Action::MaybeSendLocations | Action::MaybeSendLocationsEnded | Action::SendMdn => {
info!(context, "interrupt: smtp");
context.interrupt_smtp(InterruptInfo::new(false)).await;
}

View File

@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::io::Cursor;
use anyhow::{format_err, Result};
use anyhow::{format_err, Context as _, Result};
use async_trait::async_trait;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
@@ -50,8 +50,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// the ASCII-armored representation.
fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
Self::KeyType::from_armor_single(Cursor::new(bytes))
.map_err(|err| format_err!("rPGP error: {}", err))
Self::KeyType::from_armor_single(Cursor::new(bytes)).context("rPGP error")
}
/// Load the users' default key from the database.
@@ -202,7 +201,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.ok_or_else(|| format_err!("No address configured"))?;
.context("no address configured")?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
@@ -289,13 +288,13 @@ pub async fn store_self_keypair(
paramsv![public_key, secret_key],
)
.await
.map_err(|err| err.context("failed to remove old use of key"))?;
.context("failed to remove old use of key")?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.await
.map_err(|err| err.context("failed to clear default"))?;
.context("failed to clear default")?;
}
let is_default = match default {
KeyPairUse::Default => true as i32,
@@ -313,7 +312,7 @@ pub async fn store_self_keypair(
paramsv![addr, is_default, public_key, secret_key, t],
)
.await
.map_err(|err| err.context("failed to insert keypair"))?;
.context("failed to insert keypair")?;
Ok(())
}

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,
@@ -87,6 +88,7 @@ pub mod stock_str;
mod sync;
mod token;
mod update_helper;
pub mod webxdc;
#[macro_use]
mod dehtml;
mod color;

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?;

View File

@@ -1,6 +1,6 @@
//! # Messages and their identifiers.
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::convert::TryInto;
use anyhow::{ensure, format_err, Context as _, Result};
@@ -113,14 +113,25 @@ WHERE id=?;
Ok(())
}
/// Deletes a message and corresponding MDNs from the database.
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
pub async fn delete_from_db(self, context: &Context) -> Result<()> {
// We don't use transactions yet, so remove MDNs first to make
// sure they are not left while the message is deleted.
context
.sql
.execute("DELETE FROM smtp WHERE msg_id=?", paramsv![self])
.await?;
context
.sql
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self])
.await?;
context
.sql
.execute(
"DELETE FROM msgs_status_updates WHERE msg_id=?;",
paramsv![self],
)
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
@@ -128,6 +139,20 @@ WHERE id=?;
Ok(())
}
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
update_msg_state(context, self, MessageState::OutDelivered).await?;
let chat_id: ChatId = context
.sql
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![self])
.await?
.unwrap_or_default();
context.emit_event(EventType::MsgDelivered {
chat_id,
msg_id: self,
});
Ok(())
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -155,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)
}
@@ -169,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))
@@ -756,36 +781,41 @@ impl Message {
///
/// The message itself is not required to exist in the database,
/// it may even be deleted from the database by the time the message is prepared.
pub async fn set_quote(&mut self, context: &Context, quote: &Message) -> Result<()> {
ensure!(
!quote.rfc724_mid.is_empty(),
"Message without Message-Id cannot be quoted"
);
self.in_reply_to = Some(quote.rfc724_mid.clone());
pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
if let Some(quote) = quote {
ensure!(
!quote.rfc724_mid.is_empty(),
"Message without Message-Id cannot be quoted"
);
self.in_reply_to = Some(quote.rfc724_mid.clone());
if quote
.param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or_default()
{
self.param.set(Param::GuaranteeE2ee, "1");
if quote
.param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or_default()
{
self.param.set(Param::GuaranteeE2ee, "1");
}
let text = quote.get_text().unwrap_or_default();
self.param.set(
Param::Quote,
if text.is_empty() {
// Use summary, similar to "Image" to avoid sending empty quote.
quote
.get_summary(context, None)
.await?
.truncated_text(500)
.to_string()
} else {
text
},
);
} else {
self.in_reply_to = None;
self.param.remove(Param::Quote);
}
let text = quote.get_text().unwrap_or_default();
self.param.set(
Param::Quote,
if text.is_empty() {
// Use summary, similar to "Image" to avoid sending empty quote.
quote
.get_summary(context, None)
.await?
.truncated_text(500)
.to_string()
} else {
text
},
);
Ok(())
}
@@ -795,16 +825,21 @@ impl Message {
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
let msg = Message::load_from_db(context, msg_id).await?;
return if msg.chat_id.is_trash() {
// If message is already moved to trash chat, pretend it does not exist.
Ok(None)
} else {
Ok(Some(msg))
};
}
return self.parent(context).await;
}
Ok(None)
}
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
let msg = Message::load_from_db(context, msg_id).await?;
return if msg.chat_id.is_trash() {
// If message is already moved to trash chat, pretend it does not exist.
Ok(None)
} else {
Ok(Some(msg))
};
}
}
Ok(None)
@@ -1156,6 +1191,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
"webm" => (Viewtype::Video, "video/webm"),
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
"wmv" => (Viewtype::Video, "video/x-ms-wmv"),
"xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
"xhtml" => (Viewtype::File, "application/xhtml+xml"),
"xlsx" => (
Viewtype::File,
@@ -1283,7 +1319,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
})
.await?;
let mut updated_chat_ids = BTreeMap::new();
let mut updated_chat_ids = BTreeSet::new();
for (id, curr_chat_id, curr_state, curr_blocked) in msgs.into_iter() {
if let Err(err) = id.start_ephemeral_timer(context).await {
@@ -1297,7 +1333,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
if curr_blocked == Blocked::Not
&& (curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed)
{
update_msg_state(context, id, MessageState::InSeen).await;
update_msg_state(context, id, MessageState::InSeen).await?;
info!(context, "Seen message {}.", id);
job::add(
@@ -1305,26 +1341,30 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await?;
updated_chat_ids.insert(curr_chat_id, true);
updated_chat_ids.insert(curr_chat_id);
}
}
for updated_chat_id in updated_chat_ids.keys() {
context.emit_event(EventType::MsgsNoticed(*updated_chat_id));
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
}
Ok(())
}
pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageState) -> bool {
pub(crate) async fn update_msg_state(
context: &Context,
msg_id: MsgId,
state: MessageState,
) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE id=?;",
paramsv![state, msg_id],
)
.await
.is_ok()
.await?;
Ok(())
}
// as we do not cut inside words, this results in about 32-42 characters.
@@ -1456,7 +1496,7 @@ pub async fn handle_mdn(
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
Ok(Some((chat_id, msg_id)))
} else {
Ok(None)
@@ -1525,9 +1565,7 @@ async fn ndn_maybe_add_info_msg(
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
.await?
.ok_or_else(|| {
format_err!("ndn_maybe_add_info_msg: Contact ID not found")
})?;
.context("contact ID not found")?;
let contact = Contact::load_from_db(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
@@ -1679,6 +1717,14 @@ mod tests {
guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
Some((Viewtype::Audio, "audio/mpeg"))
);
assert_eq!(
guess_msgtype_from_suffix(Path::new("foo/file.html")),
Some((Viewtype::File, "text/html"))
);
assert_eq!(
guess_msgtype_from_suffix(Path::new("foo/file.xdc")),
Some((Viewtype::Webxdc, "application/webxdc+zip"))
);
}
#[async_std::test]
@@ -1846,7 +1892,9 @@ mod tests {
assert!(!msg.rfc724_mid.is_empty());
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_quote(ctx, &msg).await.expect("can't set quote");
msg2.set_quote(ctx, Some(&msg))
.await
.expect("can't set quote");
assert!(msg2.quoted_text() == msg.get_text());
let quoted_msg = msg2
@@ -1977,9 +2025,9 @@ mod tests {
let msg2 = alice.get_last_msg().await;
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), alice_chat.id);
assert_eq!(chats.get_chat_id(0), msg1.chat_id);
assert_eq!(chats.get_chat_id(0), msg2.chat_id);
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
assert_eq!(chats.get_chat_id(0)?, msg1.chat_id);
assert_eq!(chats.get_chat_id(0)?, msg2.chat_id);
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
@@ -2043,7 +2091,7 @@ mod tests {
let payload = alice.pop_sent_msg().await;
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
set_msg_failed(&alice, alice_msg.id, Some("badly failed")).await;

View File

@@ -2,7 +2,7 @@
use std::convert::TryInto;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
@@ -81,7 +81,7 @@ pub struct MimeFactory<'a> {
/// Result of rendering a message, ready to be submitted to a send job.
#[derive(Debug, Clone)]
pub struct RenderedEmail {
pub message: Vec<u8>,
pub message: String,
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
@@ -207,7 +207,6 @@ impl<'a> MimeFactory<'a> {
)
.await?;
let default_str = stock_str::status_line(context).await;
let factory = MimeFactory {
from_addr,
from_displayname,
@@ -215,7 +214,7 @@ impl<'a> MimeFactory<'a> {
selfstatus: context
.get_config(Config::Selfstatus)
.await?
.unwrap_or(default_str),
.unwrap_or_default(),
recipients,
timestamp: msg.timestamp_sort,
loaded: Loaded::Message { chat },
@@ -246,11 +245,10 @@ impl<'a> MimeFactory<'a> {
.get_config(Config::Displayname)
.await?
.unwrap_or_default();
let default_str = stock_str::status_line(context).await;
let selfstatus = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or(default_str);
.unwrap_or_default();
let timestamp = dc_create_smeared_timestamp(context).await;
let res = MimeFactory::<'a> {
@@ -283,7 +281,7 @@ impl<'a> MimeFactory<'a> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.ok_or_else(|| format_err!("Not configured"))?;
.context("not configured")?;
let mut res = Vec::new();
for (_, addr) in self
@@ -643,6 +641,11 @@ impl<'a> MimeFactory<'a> {
"Content-Type".to_string(),
"multipart/report; report-type=multi-device-sync".to_string(),
))
} else if self.msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
PartBuilder::new().header((
"Content-Type".to_string(),
"multipart/report; report-type=status-update".to_string(),
))
} else {
PartBuilder::new().message_type(MimeMultipartType::Mixed)
};
@@ -767,7 +770,7 @@ impl<'a> MimeFactory<'a> {
} = self;
Ok(RenderedEmail {
message: outer_message.build().as_string().into_bytes(),
message: outer_message.build().as_string(),
// envelope: Envelope::new,
is_encrypted,
is_gossiped,
@@ -917,7 +920,9 @@ impl<'a> MimeFactory<'a> {
"ephemeral-timer-changed".to_string(),
));
}
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
SystemMessage::LocationOnly
| SystemMessage::MultiDeviceSync
| SystemMessage::WebxdcStatusUpdate => {
// This should prevent automatic replies,
// such as non-delivery reports.
//
@@ -1154,6 +1159,16 @@ impl<'a> MimeFactory<'a> {
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
parts.push(context.build_sync_part(json.to_string()).await);
self.sync_ids_to_delete = Some(ids.to_string());
} else if command == SystemMessage::WebxdcStatusUpdate {
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
parts.push(context.build_status_update_part(json).await);
} else if self.msg.viewtype == Viewtype::Webxdc {
if let Some(json) = context
.render_webxdc_status_update_object(self.msg.id, None)
.await?
{
parts.push(context.build_status_update_part(&json).await);
}
}
if self.attach_selfavatar {
@@ -1285,7 +1300,7 @@ async fn build_body_file(
.param
.get_blob(Param::File, context, true)
.await?
.ok_or_else(|| format_err!("msg has no filename"))?;
.context("msg has no filename")?;
let suffix = blob.suffix().unwrap_or("dat");
// Get file name to use for sending. For privacy purposes, we do
@@ -1313,8 +1328,7 @@ async fn build_body_file(
"video_{}.{}",
chrono::Utc
.timestamp(msg.timestamp_sort, 0)
.format("%Y-%m-%d_%H-%M-%S")
.to_string(),
.format("%Y-%m-%d_%H-%M-%S"),
&suffix
),
_ => blob.as_file_name().to_string(),
@@ -1688,7 +1702,7 @@ mod tests {
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
if let Some(q) = quote {
new_msg.set_quote(t, q).await?;
new_msg.set_quote(t, Some(q)).await?;
}
let sent = t.send_msg(group_id, &mut new_msg).await;
get_subject(t, sent).await
@@ -1857,7 +1871,7 @@ mod tests {
}
if reply {
new_msg.set_quote(&t, &incoming_msg).await.unwrap();
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
}
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
@@ -1877,7 +1891,7 @@ mod tests {
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let chat_id = chats.get_chat_id(0);
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(context).await.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
@@ -1917,7 +1931,7 @@ mod tests {
let rendered_msg = mimefactory.render(context).await.unwrap();
let mail = mailparse::parse_mail(&rendered_msg.message).unwrap();
let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap();
assert_eq!(
mail.headers
.iter()
@@ -1927,7 +1941,7 @@ mod tests {
"1.0"
);
let _mime_msg = MimeMessage::from_bytes(context, &rendered_msg.message)
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes())
.await
.unwrap();
}
@@ -2001,8 +2015,9 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
let payload = t.send_msg(chat.id, &mut msg).await.payload();
let mut payload = payload.splitn(3, "\r\n\r\n");
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
let outer = payload.next().unwrap();
let inner = payload.next().unwrap();
let body = payload.next().unwrap();
@@ -2020,8 +2035,8 @@ mod tests {
// if another message is sent, that one must not contain the avatar
// and no artificial multipart/mixed nesting
let payload = t.send_msg(chat.id, &mut msg).await.payload();
let mut payload = payload.splitn(2, "\r\n\r\n");
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(2, "\r\n\r\n");
let outer = payload.next().unwrap();
let body = payload.next().unwrap();
@@ -2060,11 +2075,11 @@ mod tests {
let to = parsed
.headers
.get_first_header("To")
.ok_or_else(|| format_err!("No To: header parsed"))?;
.context("no To: header parsed")?;
let to = addrparse_header(to)?;
let mailbox = to
.extract_single_info()
.ok_or_else(|| format_err!("To: field does not contain exactly one address"))?;
.context("to: field does not contain exactly one address")?;
assert_eq!(mailbox.addr, "bob@example.net");
Ok(())

View File

@@ -66,6 +66,7 @@ pub struct MimeMessage {
pub location_kml: Option<location::Kml>,
pub message_kml: Option<location::Kml>,
pub(crate) sync_items: Option<SyncItems>,
pub(crate) webxdc_status_update: Option<String>,
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub(crate) mdn_reports: Vec<Report>,
@@ -135,6 +136,10 @@ pub enum SystemMessage {
/// Self-sent-message that contains only json used for multi-device-sync;
/// if possible, we attach that to other messages as for locations.
MultiDeviceSync = 20,
// Sync message that contains a json payload
// sent to the other webxdc instances
WebxdcStatusUpdate = 30,
}
impl Default for SystemMessage {
@@ -296,6 +301,7 @@ impl MimeMessage {
location_kml: None,
message_kml: None,
sync_items: None,
webxdc_status_update: None,
user_avatar: None,
group_avatar: None,
failure_report: None,
@@ -397,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);
@@ -538,7 +546,7 @@ impl MimeMessage {
};
if let Some(ref subject) = self.get_subject() {
if !self.has_chat_version() {
if !self.has_chat_version() && self.webxdc_status_update.is_none() {
part.msg = subject.to_string();
}
}
@@ -837,6 +845,12 @@ impl MimeMessage {
.await?;
}
}
Some("status-update") => {
if let Some(second) = mail.subparts.get(1) {
self.add_single_part_if_known(context, second, is_related)
.await?;
}
}
Some(_) => {
if let Some(first) = mail.subparts.get(0) {
any_part_added = self
@@ -1006,8 +1020,13 @@ impl MimeMessage {
if decoded_data.is_empty() {
return;
}
// treat location/message kml file attachments specially
if filename.ends_with(".kml") {
let msg_type = if context
.is_webxdc_file(filename, decoded_data)
.await
.unwrap_or(false)
{
Viewtype::Webxdc
} else if filename.ends_with(".kml") {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
@@ -1023,6 +1042,7 @@ impl MimeMessage {
}
return;
}
msg_type
} else if filename == "multi-device-sync.json" {
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
@@ -1035,7 +1055,15 @@ impl MimeMessage {
})
.ok();
return;
}
} else if filename == "status-update.json" {
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
self.webxdc_status_update = Some(serialized);
return;
} else {
msg_type
};
/* we have a regular file attachment,
write decoded data to new blob object */
@@ -1175,6 +1203,9 @@ impl MimeMessage {
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
@@ -1452,8 +1483,8 @@ async fn update_gossip_peerstates(
pub(crate) struct Report {
/// Original-Message-ID header
///
/// It MUST be present if the original message has a Message-ID according to RFC 8098, but MS
/// Exchange does not add it nevertheless, in which case it is `None`.
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
/// In case we can't find it (shouldn't happen), this is None.
original_message_id: Option<String>,
/// Additional-Message-IDs
additional_message_ids: Vec<String>,
@@ -3155,10 +3186,32 @@ Message.
#[async_std::test]
async fn test_ms_exchange_mdn() -> Result<()> {
let t = TestContext::new_alice().await;
let raw =
t.set_config(Config::ShowEmails, Some("2")).await?;
let original =
include_bytes!("../test-data/message/ms_exchange_report_original_message.eml");
dc_receive_imf(&t, original, "INBOX", false).await?;
let original_msg_id = t.get_last_msg().await.id;
// 1. Test mimeparser directly
let mdn =
include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await?;
assert!(!mimeparser.mdn_reports.is_empty());
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
assert_eq!(mimeparser.mdn_reports.len(), 1);
assert_eq!(
mimeparser.mdn_reports[0].original_message_id.as_deref(),
Some("d5904dc344eeb5deaf9bb44603f0c716@posteo.de")
);
assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty());
// 2. Test that marking the original msg as read works
dc_receive_imf(&t, mdn, "INBOX", false).await?;
assert_eq!(
original_msg_id.get_state(&t).await?,
MessageState::OutMdnRcvd
);
Ok(())
}
}

View File

@@ -110,9 +110,6 @@ pub enum Param {
/// For Jobs
AlsoMove = b'M',
/// For Jobs: space-separated list of message recipients
Recipients = b'R',
/// For MDN-sending job
MsgId = b'I',
@@ -171,6 +168,12 @@ pub enum Param {
/// For Chats: timestamp of protection settings update.
ProtectionSettingsTimestamp = b'L',
/// For Webxdc Message Instances: Current summary
WebxdcSummary = b'N',
/// For Webxdc Message Instances: timestamp of summary update.
WebxdcSummaryTimestamp = b'Q',
}
/// An object for handling key=value parameter lists.

View File

@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashSet};
use std::io;
use std::io::Cursor;
use anyhow::{bail, ensure, format_err, Result};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use pgp::armor::BlockType;
use pgp::composed::{
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
@@ -346,7 +346,7 @@ pub async fn pk_validate(
// OpenPGP signature calculation.
let content = content
.get(..content.len().saturating_sub(2))
.ok_or_else(|| format_err!("index is out of range"))?;
.context("index is out of range")?;
for pkey in pkeys {
if standalone_signature.verify(pkey, content).is_ok() {
@@ -520,7 +520,6 @@ mod tests {
&sig_check_keyring,
)
.await
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
@@ -536,7 +535,6 @@ mod tests {
&sig_check_keyring,
)
.await
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);

View File

@@ -282,57 +282,23 @@ static P_DISROOT: Lazy<Provider> = Lazy::new(|| Provider {
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/disroot",
server: vec![],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// dubby.org.md: dubby.org
static P_DUBBY_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "dubby.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/dubby-org",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "dubby.org",
hostname: "disroot.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "dubby.org",
hostname: "disroot.org",
port: 587,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "dubby.org",
port: 465,
username_pattern: Email,
},
],
config_defaults: Some(vec![
ConfigDefault {
key: Config::BccSelf,
value: "1",
},
ConfigDefault {
key: Config::SentboxWatch,
value: "0",
},
ConfigDefault {
key: Config::MvboxMove,
value: "0",
},
]),
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
@@ -400,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,
@@ -423,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,
@@ -660,6 +619,35 @@ static P_ICLOUD: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// infomaniak.com.md: ik.me
static P_INFOMANIAK_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "infomaniak.com",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/infomaniak-com",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "mail.infomaniak.com",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.infomaniak.com",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: Some(10),
oauth2_authorizer: None,
});
// kolst.com.md: kolst.com
static P_KOLST_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "kolst.com",
@@ -688,12 +676,41 @@ static P_KONTENT_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// mail.de.md: mail.de
static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
id: "mail.de",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-de",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.mail.de",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "smtp.mail.de",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// mail.ru.md: mail.ru, inbox.ru, internet.ru, bk.ru, list.ru
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![
@@ -743,7 +760,22 @@ static P_MAILBOX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mailbox-org",
server: vec![],
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.mailbox.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "smtp.mailbox.org",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
@@ -866,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,
@@ -895,7 +927,7 @@ static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
id: "posteo",
status: Status::Ok,
@@ -924,7 +956,7 @@ static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// protonmail.md: protonmail.com, protonmail.ch
// protonmail.md: protonmail.com, protonmail.ch, pm.me
static P_PROTONMAIL: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "protonmail",
@@ -967,7 +999,22 @@ static P_RISEUP_NET: Lazy<Provider> = Lazy::new(|| Provider {
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/riseup-net",
server: vec![],
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "mail.riseup.net",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.riseup.net",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
@@ -1024,7 +1071,22 @@ static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/systemli-org",
server: vec![],
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "mail.systemli.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.systemli.org",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
@@ -1421,13 +1483,128 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("comcast.net", &*P_COMCAST),
("dismail.de", &*P_DISMAIL_DE),
("disroot.org", &*P_DISROOT),
("dubby.org", &*P_DUBBY_ORG),
("e.email", &*P_E_EMAIL),
("espiv.net", &*P_ESPIV_NET),
("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),
@@ -1451,8 +1628,10 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("icloud.com", &*P_ICLOUD),
("me.com", &*P_ICLOUD),
("mac.com", &*P_ICLOUD),
("ik.me", &*P_INFOMANIAK_COM),
("kolst.com", &*P_KOLST_COM),
("kontent.com", &*P_KONTENT_COM),
("mail.de", &*P_MAIL_DE),
("mail.ru", &*P_MAIL_RU),
("inbox.ru", &*P_MAIL_RU),
("internet.ru", &*P_MAIL_RU),
@@ -1469,10 +1648,12 @@ 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),
("posteo.be", &*P_POSTEO),
("posteo.ca", &*P_POSTEO),
("posteo.ch", &*P_POSTEO),
("posteo.cl", &*P_POSTEO),
("posteo.co", &*P_POSTEO),
@@ -1521,6 +1702,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("posteo.us", &*P_POSTEO),
("protonmail.com", &*P_PROTONMAIL),
("protonmail.ch", &*P_PROTONMAIL),
("pm.me", &*P_PROTONMAIL),
("qq.com", &*P_QQ),
("foxmail.com", &*P_QQ),
("riseup.net", &*P_RISEUP_NET),
@@ -1617,7 +1799,6 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("comcast", &*P_COMCAST),
("dismail.de", &*P_DISMAIL_DE),
("disroot", &*P_DISROOT),
("dubby.org", &*P_DUBBY_ORG),
("e.email", &*P_E_EMAIL),
("espiv.net", &*P_ESPIV_NET),
("example.com", &*P_EXAMPLE_COM),
@@ -1632,8 +1813,10 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("i.ua", &*P_I_UA),
("i3.net", &*P_I3_NET),
("icloud", &*P_ICLOUD),
("infomaniak.com", &*P_INFOMANIAK_COM),
("kolst.com", &*P_KOLST_COM),
("kontent.com", &*P_KONTENT_COM),
("mail.de", &*P_MAIL_DE),
("mail.ru", &*P_MAIL_RU),
("mail2tor", &*P_MAIL2TOR),
("mailbox.org", &*P_MAILBOX_ORG),
@@ -1670,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(2021, 9, 29));
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));

View File

@@ -304,14 +304,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.ok_or_else(|| format_err!("Invalid DCACCOUNT payload"))?;
.context("invalid DCACCOUNT payload")?;
let url =
url::Url::parse(payload).with_context(|| format!("Invalid account URL: {:?}", payload))?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::Account {
domain: url
.host_str()
.ok_or_else(|| format_err!("Can't extract WebRTC instance domain"))?
.context("can't extract WebRTC instance domain")?
.to_string(),
})
} else {
@@ -323,7 +323,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(DCWEBRTC_SCHEME.len()..)
.ok_or_else(|| format_err!("Invalid DCWEBRTC payload"))?;
.context("invalid DCWEBRTC payload")?;
let (_type, url) = Message::parse_webrtc_instance(payload);
let url =
@@ -333,7 +333,7 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
Ok(Qr::WebrtcInstance {
domain: url
.host_str()
.ok_or_else(|| format_err!("Can't extract WebRTC instance domain"))?
.context("can't extract WebRTC instance domain")?
.to_string(),
instance_pattern: payload.to_string(),
})

View File

@@ -1,6 +1,6 @@
//! # Support for IMAP QUOTA extension.
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use async_imap::types::{Quota, QuotaResource};
use std::collections::BTreeMap;
@@ -64,7 +64,7 @@ async fn get_unique_quota_roots_and_usage(
.iter()
.find(|q| &q.root_name == quota_root_name)
.cloned()
.ok_or_else(|| anyhow!("quota_root should have a quota"))?;
.context("quota_root should have a quota")?;
// replace old quotas, because between fetching quotaroots for folders,
// messages could be recieved and so the usage could have been changed
*unique_quota_roots
@@ -96,7 +96,7 @@ fn get_highest_usage<'t>(
}
}
highest.ok_or_else(|| anyhow!("no quota_resource found, this is unexpected"))
highest.context("no quota_resource found, this is unexpected")
}
/// Checks if a quota warning is needed.

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,9 +8,11 @@ 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::smtp::Smtp;
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
@@ -273,17 +307,25 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
let mut interrupt_info = Default::default();
loop {
match job::load_next(&ctx, Thread::Smtp, &interrupt_info)
.await
.ok()
.flatten()
{
let job = match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
Err(err) => {
error!(ctx, "Failed loading job from the database: {:#}.", err);
None
}
Ok(job) => job,
};
match job {
Some(job) => {
info!(ctx, "executing smtp job");
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;
interrupt_info = Default::default();
}
None => {
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
warn!(ctx, "send_smtp_messages failed: {:#}", err);
}
// Fake Idle
info!(ctx, "smtp fake idle - started");
match &connection.last_send_error {
@@ -333,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(

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 {

View File

@@ -4,14 +4,18 @@ pub mod send;
use std::time::{Duration, SystemTime};
use anyhow::{bail, format_err, Context as _, Result};
use async_smtp::smtp::client::net::ClientTlsParameters;
use async_smtp::{error, smtp, EmailAddress, ServerAddress};
use async_smtp::smtp::response::{Category, Code, Detail};
use async_smtp::{smtp, EmailAddress, ServerAddress};
use crate::constants::DC_LP_AUTH_OAUTH2;
use crate::events::EventType;
use crate::job::Status;
use crate::login_param::{
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
};
use crate::message::{self, MsgId};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::provider::Socket;
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
@@ -19,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>,
@@ -131,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);
@@ -159,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;
(
@@ -205,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());
@@ -220,3 +195,303 @@ impl Smtp {
Ok(())
}
}
/// Tries to send a message.
///
/// Returns Status::Finished if sending the message should not be retried anymore,
/// Status::RetryLater if sending should be postponed and Status::RetryNow if it is suspected that
/// temporary failure is caused by stale connection, in which case a second attempt to send the
/// same message may be done immediately.
pub(crate) async fn smtp_send(
context: &Context,
recipients: &[async_smtp::EmailAddress],
message: &str,
smtp: &mut Smtp,
msg_id: MsgId,
rowid: i64,
) -> Status {
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "smtp-sending out mime message:");
println!("{}", message);
}
smtp.connectivity.set_working(context).await;
let send_result = smtp
.send(context, recipients, message.as_bytes(), rowid)
.await;
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {:?}", &err);
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
// Workaround for incorrectly configured servers returning permanent errors
// instead of temporary ones.
let maybe_transient = match response.code {
// Sometimes servers send a permanent error when actually it is a temporary error
// For documentation see <https://tools.ietf.org/html/rfc3463>
Code {
category: Category::MailSystem,
detail: Detail::Zero,
..
} => {
// Ignore status code 5.5.0, see <https://support.delta.chat/t/every-other-message-gets-stuck/877/2>
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
//
// Other enhanced status codes, such as Postfix
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
// are not ignored.
response.first_word() == Some(&"5.5.0".to_string())
}
_ => false,
};
if maybe_transient {
Status::RetryLater
} else {
// If we do not retry, add an info message to the chat.
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
// should definitely go here, because user has to open the link to
// resume message sending.
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
}
async_smtp::smtp::error::Error::Transient(ref response) => {
// We got a transient 4xx response from SMTP server.
// Give some time until the server-side error maybe goes away.
if let Some(first_word) = response.first_word() {
if first_word.ends_with(".1.1")
|| first_word.ends_with(".1.2")
|| first_word.ends_with(".1.3")
{
// Sometimes we receive transient errors that should be permanent.
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
// receive as a transient error are misconfigurations of the smtp server.
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
info!(context, "Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", first_word);
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
} else {
Status::RetryLater
}
} else {
Status::RetryLater
}
}
_ => {
if smtp.has_maybe_stale_connection().await {
info!(context, "stale connection? immediately reconnecting");
Status::RetryNow
} else {
Status::RetryLater
}
}
};
// this clears last_success info
smtp.disconnect().await;
res
}
Err(crate::smtp::send::Error::Envelope(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "SMTP job is invalid: {}", err);
Status::Finished(Err(err.into()))
}
Err(crate::smtp::send::Error::NoTransport) => {
// Should never happen.
// It does not even make sense to disconnect here.
error!(context, "SMTP job failed because SMTP has no transport");
Status::Finished(Err(format_err!("SMTP has not transport")))
}
Err(crate::smtp::send::Error::Other(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "unable to load job: {}", err);
Status::Finished(Err(err))
}
Ok(()) => Status::Finished(Ok(())),
};
if let Status::Finished(Err(err)) = &status {
// We couldn't send the message, so mark it as failed
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
}
status
}
/// Sends message identified by `smtp` table rowid over SMTP connection.
///
/// Removes row if the message should not be retried, otherwise increments retry count.
pub(crate) async fn send_msg_to_smtp(
context: &Context,
smtp: &mut Smtp,
rowid: i64,
) -> anyhow::Result<()> {
if let Err(err) = smtp
.connect_configured(context)
.await
.context("SMTP connection failure")
{
smtp.last_send_error = Some(format!("{:#}", err));
return Err(err);
}
// Increase retry count as soon as we have an SMTP connection. This ensures that the message is
// eventually removed from the queue by exceeding retry limit even in case of an error that
// keeps happening early in the message sending code, e.g. failure to read the message from the
// database.
context
.sql
.execute(
"UPDATE smtp SET retries=retries+1 WHERE id=?",
paramsv![rowid],
)
.await
.context("failed to update retries count")?;
let (body, recipients, msg_id, retries) = context
.sql
.query_row(
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
paramsv![rowid],
|row| {
let mime: String = row.get(0)?;
let recipients: String = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
let retries: i64 = row.get(3)?;
Ok((mime, recipients, msg_id, retries))
},
)
.await?;
if retries > 6 {
message::set_msg_failed(
context,
msg_id,
Some("Number of retries exceeded the limit."),
)
.await;
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await
.context("failed to remove message with exceeded retry limit from smtp table")?;
bail!("Number of retries exceeded the limit");
}
let recipients_list = recipients
.split(' ')
.filter_map(
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
Ok(addr) => Some(addr),
Err(err) => {
warn!(context, "invalid recipient: {} {:?}", addr, err);
None
}
},
)
.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,
body.as_str(),
smtp,
msg_id,
rowid,
)
.await
{
Status::RetryNow => {
// Do a single retry immediately without increasing retry counter in case of stale
// connection.
info!(context, "Doing immediate retry to send message.");
// smtp_send just closed stale SMTP connection, reconnect and try again.
if let Err(err) = smtp
.connect_configured(context)
.await
.context("failed to reopen stale SMTP connection")
{
smtp.last_send_error = Some(format!("{:#}", err));
return Err(err);
}
smtp_send(
context,
&recipients_list,
body.as_str(),
smtp,
msg_id,
rowid,
)
.await
}
status => status,
};
match status {
Status::Finished(res) => {
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await?;
if res.is_ok() {
msg_id.set_delivered(context).await?;
}
res
}
Status::RetryNow | Status::RetryLater => Err(format_err!("Retry")),
}
}
/// Tries to send all messages currently in `smtp` table.
///
/// Logs and ignores SMTP errors to ensure that a single SMTP message constantly failing to be sent
/// does not block other messages in the queue from being sent.
pub(crate) async fn send_smtp_messages(
context: &Context,
connection: &mut Smtp,
) -> anyhow::Result<()> {
context.send_sync_msg().await?; // Add sync message to the end of the queue if needed.
let rowids = context
.sql
.query_map(
"SELECT id FROM smtp ORDER BY id ASC",
paramsv![],
|row| {
let rowid: i64 = row.get(0)?;
Ok(rowid)
},
|rowids| {
rowids
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for rowid in rowids {
if let Err(err) = send_msg_to_smtp(context, connection, rowid).await {
info!(context, "Failed to send message over SMTP: {:#}.", err);
}
}
Ok(())
}

View File

@@ -28,9 +28,9 @@ impl Smtp {
pub async fn send(
&mut self,
context: &Context,
recipients: Vec<EmailAddress>,
message: Vec<u8>,
job_id: u32,
recipients: &[EmailAddress],
message: &[u8],
rowid: i64,
) -> Result<()> {
let message_len_bytes = message.len();
@@ -41,7 +41,7 @@ impl Smtp {
}
}
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_display = recipients_chunk
.iter()
.map(|x| x.as_ref())
@@ -52,8 +52,8 @@ impl Smtp {
.map_err(Error::Envelope)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
&message,
rowid.to_string(), // only used for internal logging
message,
);
if let Some(ref mut transport) = self.transport {

View File

@@ -7,9 +7,10 @@ use std::collections::HashSet;
use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{bail, format_err, Context as _, Result};
use anyhow::{bail, Context as _, Result};
use async_std::path::PathBuf;
use async_std::prelude::*;
use rusqlite::OpenFlags;
use rusqlite::{config::DbConfig, Connection, OpenFlags};
use crate::blob::BlobObject;
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
@@ -38,20 +39,50 @@ mod migrations;
/// A wrapper around the underlying Sqlite3 object.
#[derive(Debug)]
pub struct Sql {
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
}
/// Database file path
pub(crate) dbfile: PathBuf,
impl Default for Sql {
fn default() -> Self {
Self {
pool: RwLock::new(None),
}
}
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
/// None if the database is not open, true if it is open with passphrase and false if it is
/// open without a passphrase.
is_encrypted: RwLock<Option<bool>>,
}
impl Sql {
pub fn new() -> Sql {
Self::default()
pub fn new(dbfile: PathBuf) -> Sql {
Self {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
}
}
/// Tests SQLCipher passphrase.
///
/// 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.
///
/// 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.");
}
// 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)
}
/// Checks if there is currently a connection to the underlying Sqlite database.
@@ -59,37 +90,98 @@ impl Sql {
self.pool.read().await.is_some()
}
/// Returns true if the database is encrypted.
///
/// If database is not open, returns `None`.
pub(crate) async fn is_encrypted(&self) -> Option<bool> {
*self.is_encrypted.read().await
}
/// Closes all underlying Sqlite connections.
pub async fn close(&self) {
async fn close(&self) {
let _ = self.pool.write().await.take();
// drop closes the connection
}
pub fn new_pool(
/// Exports the database to a separate file with the given passphrase.
///
/// Set passphrase to empty string to export the database unencrypted.
pub(crate) async fn export(&self, path: &Path, passphrase: String) -> Result<()> {
let path_str = path
.to_str()
.with_context(|| format!("path {:?} is not valid unicode", path))?;
let conn = self.get_conn().await?;
conn.execute(
"ATTACH DATABASE ? AS backup KEY ?",
paramsv![path_str, passphrase],
)
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
.context("failed to export to attached backup database");
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;
Ok(())
}
/// Imports the database from a separate file with the given passphrase.
pub(crate) async fn import(&self, path: &Path, passphrase: String) -> Result<()> {
let path_str = path
.to_str()
.with_context(|| format!("path {:?} is not valid unicode", path))?;
let conn = self.get_conn().await?;
// Reset the database without reopening it. We don't want to reopen the database because we
// don't have main database passphrase at this point.
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
// Without resetting import may fail due to existing tables.
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")?;
conn.execute("VACUUM", [])
.context("failed to vacuum the database")?;
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE")?;
conn.execute(
"ATTACH DATABASE ? AS backup KEY ?",
paramsv![path_str, passphrase],
)
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
Ok(())
})
.context("failed to import from attached backup database");
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;
Ok(())
}
fn new_pool(
dbfile: &Path,
readonly: bool,
) -> anyhow::Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>> {
passphrase: String,
) -> Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>> {
let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
if readonly {
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY);
} else {
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
}
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
// this actually creates min_idle database handles just now.
// therefore, with_init() must not try to modify the database as otherwise
// we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle)
let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile)
.with_flags(open_flags)
.with_init(|c| {
.with_init(move |c| {
c.execute_batch(&format!(
"PRAGMA secure_delete=on;
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
PRAGMA secure_delete=on;
PRAGMA busy_timeout = {};
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
",
Duration::from_secs(10).as_millis()
))?;
c.pragma_update(None, "key", passphrase.clone())?;
Ok(())
});
@@ -102,114 +194,125 @@ 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())?);
{
let conn = self.get_conn().await?;
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
// VACUUM, which usually happens before backup export.
// 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())?;
// journal_mode is persisted, it is sufficient to change it only for one handle.
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
}
self.run_migrations(context).await?;
Ok(())
}
pub 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.
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
migrations::run(context, self)
.await
.context("failed to run migrations")?;
// (2) updates that require high-level objects
// the structure is complete now and all objects are usable
if recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
let addrs = self
.query_map(
"SELECT addr FROM acpeerstates;",
paramsv![],
|row| row.get::<_, String>(0),
|addrs| {
addrs
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for addr in &addrs {
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
peerstate.recalc_fingerprint();
peerstate.save_to_db(self, false).await?;
}
}
}
if update_icons {
update_saved_messages_icon(context).await?;
update_device_icon(context).await?;
}
if disable_server_delete {
// We now always watch all folders and delete messages there if delete_server is enabled.
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
if context.get_config_delete_server_after().await?.is_some() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::delete_server_turned_off(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
context
.set_config(Config::DeleteServerAfter, Some("0"))
.await?;
}
}
if recode_avatar {
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?;
match blob.recode_to_avatar_size(context).await {
Ok(()) => {
context
.set_config(Config::Selfavatar, Some(&avatar))
.await?
}
Err(e) => {
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
context.set_config(Config::Selfavatar, None).await?
}
}
}
}
Ok(())
}
/// Opens the provided database and runs any necessary migrations.
/// If a database is already open, this will return an error.
pub async fn open(
&self,
context: &Context,
dbfile: &Path,
readonly: bool,
) -> anyhow::Result<()> {
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
if self.is_open().await {
error!(
context,
"Cannot open, database \"{:?}\" already opened.", dbfile,
"Cannot open, database \"{:?}\" already opened.", self.dbfile,
);
bail!("SQL database is already opened.");
}
*self.pool.write().await = Some(Self::new_pool(dbfile, readonly)?);
if !readonly {
{
let conn = self.get_conn().await?;
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
// VACUUM, which usually happens before backup export.
// 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())?;
// journal_mode is persisted, it is sufficient to change it only for one handle.
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
}
// (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.
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
migrations::run(context, self).await?;
// (2) updates that require high-level objects
// the structure is complete now and all objects are usable
if recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
let addrs = self
.query_map(
"SELECT addr FROM acpeerstates;",
paramsv![],
|row| row.get::<_, String>(0),
|addrs| {
addrs
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for addr in &addrs {
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
peerstate.recalc_fingerprint();
peerstate.save_to_db(self, false).await?;
}
}
}
if update_icons {
update_saved_messages_icon(context).await?;
update_device_icon(context).await?;
}
if disable_server_delete {
// We now always watch all folders and delete messages there if delete_server is enabled.
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
if context.get_config_delete_server_after().await?.is_some() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::delete_server_turned_off(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
context
.set_config(Config::DeleteServerAfter, Some("0"))
.await?;
}
}
if recode_avatar {
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?;
match blob.recode_to_avatar_size(context).await {
Ok(()) => {
context
.set_config(Config::Selfavatar, Some(&avatar))
.await?
}
Err(e) => {
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
context.set_config(Config::Selfavatar, None).await?
}
}
}
}
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(())
}
info!(context, "Opened database {:?}.", dbfile);
Ok(())
}
/// Execute the given query, returning the number of affected rows.
@@ -228,10 +331,10 @@ impl Sql {
&self,
query: impl AsRef<str>,
params: impl rusqlite::Params,
) -> anyhow::Result<usize> {
) -> Result<i64> {
let conn = self.get_conn().await?;
conn.execute(query.as_ref(), params)?;
Ok(usize::try_from(conn.last_insert_rowid())?)
Ok(conn.last_insert_rowid())
}
/// Prepares and executes the statement and maps a function over the resulting rows.
@@ -260,9 +363,7 @@ impl Sql {
&self,
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
let lock = self.pool.read().await;
let pool = lock
.as_ref()
.ok_or_else(|| format_err!("No SQL connection"))?;
let pool = lock.as_ref().context("no SQL connection")?;
let conn = pool.get()?;
Ok(conn)
@@ -686,7 +787,7 @@ async fn maybe_add_from_param(
async fn prune_tombstones(sql: &Sql) -> Result<()> {
sql.execute(
"DELETE FROM msgs
WHERE (chat_id=? OR hidden)
WHERE chat_id=?
AND NOT EXISTS (
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
)",
@@ -787,7 +888,7 @@ mod tests {
t.sql.close().await;
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
t.sql.open(&t, t.get_dbfile(), false).await.unwrap();
t.sql.open(&t, "".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()[..]);
@@ -805,6 +906,23 @@ mod tests {
}
}
/// Regression test for a bug where housekeeping deleted drafts since their
/// `hidden` flag is set.
#[async_std::test]
async fn test_housekeeping_dont_delete_drafts() {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
let mut new_draft = Message::new(Viewtype::Text);
new_draft.set_text(Some("This is my draft".to_string()));
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
housekeeping(&t).await.unwrap();
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
assert_eq!(loaded_draft.unwrap().text.unwrap(), "This is my draft");
}
/// Regression test.
///
/// Previously the code checking for existence of `config` table
@@ -827,14 +945,14 @@ mod tests {
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new();
let sql = Sql::new(dbfile.into());
// Create database with all the tables.
sql.open(&t, dbfile.as_ref(), false).await.unwrap();
sql.open(&t, "".to_string()).await.unwrap();
sql.close().await;
// Reopen the database
sql.open(&t, dbfile.as_ref(), false).await?;
sql.open(&t, "".to_string()).await?;
sql.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?);",
paramsv!("foo", "bar"),
@@ -887,4 +1005,36 @@ 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(())
}
}

View File

@@ -1,6 +1,6 @@
//! Migrations module.
use anyhow::Result;
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::constants::ShowEmails;
@@ -19,7 +19,11 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
let mut exists_before_update = false;
let mut dbversion_before_update = DBVERSION;
if !sql.table_exists("config").await? {
if !sql
.table_exists("config")
.await
.context("failed to check if config table exists")?
{
info!(context, "First time init: creating tables",);
sql.transaction(move |transaction| {
transaction.execute_batch(TABLES)?;
@@ -535,6 +539,46 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
)
.await?;
}
if dbversion < 83 {
info!(context, "[migration] v83");
sql.execute_migration(
"ALTER TABLE imap_sync
ADD COLUMN modseq -- Highest modification sequence
INTEGER DEFAULT 0",
83,
)
.await?;
}
if dbversion < 84 {
info!(context, "[migration] v84");
sql.execute_migration(
r#"CREATE TABLE msgs_status_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
msg_id INTEGER,
update_item TEXT DEFAULT '',
update_item_read INTEGER DEFAULT 0);
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
84,
)
.await?;
}
if dbversion < 85 {
info!(context, "[migration] v85");
sql.execute_migration(
r#"CREATE TABLE smtp (
id INTEGER PRIMARY KEY,
rfc724_mid TEXT NOT NULL, -- Message-ID
mime TEXT NOT NULL, -- SMTP payload
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
recipients TEXT NOT NULL, -- List of recipients separated by space
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the messsage
);
CREATE INDEX smtp_messageid ON imap(rfc724_mid);
"#,
85,
)
.await?;
}
Ok((
recalc_fingerprints,
@@ -562,7 +606,8 @@ impl Sql {
Ok(())
})
.await?;
.await
.with_context(|| format!("execute_migration failed for version {}", version))?;
Ok(())
}

View File

@@ -53,9 +53,6 @@ pub enum StockMessage {
#[strum(props(fallback = "File"))]
File = 12,
#[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))]
StatusLine = 13,
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))]
MsgGrpName = 15,
@@ -455,11 +452,6 @@ pub(crate) async fn file(context: &Context) -> String {
translated(context, StockMessage::File).await
}
/// Stock string: `Sent with my Delta Chat Messenger: https://delta.chat`.
pub(crate) async fn status_line(context: &Context) -> String {
translated(context, StockMessage::StatusLine).await
}
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
pub(crate) async fn msg_grp_name(
context: &Context,
@@ -1288,11 +1280,13 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0)).await.unwrap();
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap();
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
(chats.get_chat_id(0), chats.get_chat_id(1))
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
} else {
(chats.get_chat_id(1), chats.get_chat_id(0))
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
};
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored

View File

@@ -137,7 +137,14 @@ impl Message {
append_text = false;
stock_str::videochat_invitation(context).await
}
_ => {
Viewtype::Webxdc => {
append_text = true;
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 {
"".to_string()
} else {

View File

@@ -5,13 +5,11 @@
use std::collections::BTreeMap;
use std::ops::Deref;
use std::panic;
use std::str::FromStr;
use std::thread;
use std::time::{Duration, Instant};
use ansi_term::Color;
use async_std::channel::{self, Receiver, Sender};
use async_std::path::PathBuf;
use async_std::prelude::*;
use async_std::sync::{Arc, RwLock};
use async_std::task;
@@ -30,11 +28,9 @@ use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::EmailAddress;
use crate::events::{Event, EventType};
use crate::job::Action;
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId};
use crate::mimeparser::MimeMessage;
use crate::param::{Param, Params};
#[allow(non_upper_case_globals)]
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
@@ -275,27 +271,27 @@ impl TestContext {
/// Panics if there is no message or on any error.
pub async fn pop_sent_msg(&self) -> SentMessage {
let start = Instant::now();
let (rowid, foreign_id, raw_params) = loop {
let (rowid, msg_id, payload, recipients) = loop {
let row = self
.ctx
.sql
.query_row(
.query_row_optional(
r#"
SELECT id, foreign_id, param
FROM jobs
WHERE action=?
ORDER BY desired_timestamp DESC;
"#,
paramsv![Action::SendMsgToSmtp],
SELECT id, msg_id, mime, recipients
FROM smtp
ORDER BY id DESC"#,
paramsv![],
|row| {
let id: u32 = row.get(0)?;
let foreign_id: u32 = row.get(1)?;
let param: String = row.get(2)?;
Ok((id, foreign_id, param))
let rowid: i64 = row.get(0)?;
let msg_id: MsgId = row.get(1)?;
let mime: String = row.get(2)?;
let recipients: String = row.get(3)?;
Ok((rowid, msg_id, mime, recipients))
},
)
.await;
if let Ok(row) = row {
.await
.expect("query_row_optional failed");
if let Some(row) = row {
break row;
}
if start.elapsed() < Duration::from_secs(3) {
@@ -304,24 +300,18 @@ impl TestContext {
panic!("no sent message found in jobs table");
}
};
let id = MsgId::new(foreign_id);
let params = Params::from_str(&raw_params).unwrap();
let blob_path = params
.get_blob(Param::File, &self.ctx, false)
.await
.expect("failed to parse blob from param")
.expect("no Param::File found in Params")
.to_abs_path();
self.ctx
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
.await
.expect("failed to remove job");
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
SentMessage {
params,
blob_path,
sender_msg_id: id,
payload,
sender_msg_id: msg_id,
recipients,
}
}
@@ -345,7 +335,7 @@ impl TestContext {
let received_msg =
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
.to_owned()
+ &msg.payload();
+ msg.payload();
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", false)
.await
.unwrap();
@@ -590,8 +580,8 @@ impl Drop for LogSink {
/// passed through a SMTP-IMAP pipeline.
#[derive(Debug, Clone)]
pub struct SentMessage {
params: Params,
blob_path: PathBuf,
payload: String,
recipients: String,
pub sender_msg_id: MsgId,
}
@@ -600,17 +590,17 @@ impl SentMessage {
///
/// If there are multiple recipients this is just a random one, so is not very useful.
pub fn recipient(&self) -> EmailAddress {
let raw = self
.params
.get(Param::Recipients)
.expect("no recipients in params");
let rcpt = raw.split(' ').next().expect("no recipient found");
let rcpt = self
.recipients
.split(' ')
.next()
.expect("no recipient found");
rcpt.parse().expect("failed to parse email address")
}
/// The raw message payload.
pub fn payload(&self) -> String {
std::fs::read_to_string(&self.blob_path).unwrap()
pub fn payload(&self) -> &str {
&self.payload
}
}

1577
src/webxdc.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,10 @@ Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
Quota | IMAP QUOTA extension ([RFC 2087](https://tools.ietf.org/html/rfc2087))
Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162](https://tools.ietf.org/html/rfc7162))
Authorization | OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749))
End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)), Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
Configuration assistance | [Autoconfigure](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)
Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-mail-specification)
Detect mailing list | List-Id ([RFC 2919](https://tools.ietf.org/html/rfc2919)) and Precedence ([RFC 3834](https://tools.ietf.org/html/rfc3834))

View File

@@ -0,0 +1,15 @@
Subject: Some subject
Date: Sat, 01 Jan 2022 21:14:26 +0000
Chat-Version: 1.0
MIME-Version: 1.0
Message-ID: <foo@example.org>
To: <alice@example.org>, <bob@example.net>,
<xxxxxxxx.xxxx@example.orgã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â°ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢âŸã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¦ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã
£â¢ã<C2A2>,
<20>â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â<C2A2><EFBFBD>@abcdef.example.net,
<tmp.xxxxx@testrun.org>
From: Claire <claire@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
X-Spam: Yes
Some message.

View File

@@ -1,43 +1,43 @@
Return-Path: <anonymous@example.org>
Return-Path: <bob@example.net>
Delivered-To: anonymous@posteo.de
Received: from proxy02.posteo.name ([127.0.0.1])
by dovecot16.posteo.name (Dovecot) with LMTP id Cp2uFxP1sWHbCQEAchYRkQ
for <anonymous@posteo.de>; Thu, 09 Dec 2021 13:25:38 +0100
by dovecot16.posteo.name (Dovecot) with LMTP id GaxcARout2HxiwMAchYRkQ
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100
Received: from proxy02.posteo.de ([127.0.0.1])
by proxy02.posteo.name (Dovecot) with LMTP id MWsaCwrvsWG0wgEAGFAyLg
; Thu, 09 Dec 2021 13:25:38 +0100
Received: from mailin06.posteo.de (unknown [10.0.1.6])
by proxy02.posteo.de (Postfix) with ESMTPS id 4J8tXy0KkMz120l
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:38 +0100 (CET)
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
by mailin06.posteo.de (Postfix) with ESMTPS id F24DE215B8
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
by proxy02.posteo.name (Dovecot) with LMTP id q/LiCqwqt2FMTQEAGFAyLg
; Mon, 13 Dec 2021 12:35:32 +0100
Received: from mailin05.posteo.de (unknown [10.0.1.5])
by proxy02.posteo.de (Postfix) with ESMTPS id 4JCKFJ1LCLz1214
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
Received: from mx03.posteo.de (mailin05.posteo.de [127.0.0.1])
by mailin05.posteo.de (Postfix) with ESMTPS id 1B26420012
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
X-Virus-Scanned: amavisd-new at posteo.de
X-Spam-Flag: NO
X-Spam-Score: 0.011
X-Spam-Level:
X-Spam-Status: No, score=0.011 tagged_above=-1000 required=7
tests=[HTML_MESSAGE=0.001, T_POSTEO_TLSINY=0.01] autolearn=disabled
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=27yedFdXeAzOobR4x685XJ/5e6WQmX8PP5pSnOlGU2a9Ismhk38wb5AS44xh1yeL5PUxla78UEsHwGkPR0IyPRlHWaLMFLd5CJZN3GzFfrj/2CuB+cd1hOLpp9hRmCebc3rchuDr
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=RzB41PpqvrD+cuxf3UAqQLiXQL4MHazHZcKeOYJw75deIl7zxtrLXqfAZCeq2IPKt/njRRONUbfuvNdvLxg4mBJ0Rnb53wFOKOqtEpTzxYoQff3yqBpGSohr0DBG26PyBHi7ba/7
Authentication-Results: posteo.de; dmarc=none (p=none dis=none) header.from=example.org
X-Posteo-TLS-Received-Status: TLSv1.2
Received: from mail.example.org (mail.example.org [0.0.0.0])
by mx04.posteo.de (Postfix) with ESMTPS id 4J8tXx38vRz10yw
for <anonymous@posteo.at>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
Received: from [192.168.1.11] (port=22105 helo=mail.example.org)
by mx03.posteo.de (Postfix) with ESMTPS id 4JCKFH2ZM7zyx0
for <alice@example.org>; Mon, 13 Dec 2021 12:35:31 +0100 (CET)
Received: from [192.168.1.11] (port=27040 helo=mail.example.org)
by mail.example.org with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
(Exim 4.94.2)
(envelope-from <anonymous@example.org>)
id 1mvIUG-0007VC-2U
for anonymous@posteo.at; Thu, 09 Dec 2021 13:25:24 +0100
From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@posteo.at>
(envelope-from <bob@example.net>)
id 1mwjc6-0003hM-2K
for alice@example.org; Mon, 13 Dec 2021 12:35:26 +0100
From: Anonymous_2 <bob@example.net>
To: Anonymous_1 <alice@example.org>
Subject: Gelesen: Test message
Thread-Topic: Test message
Thread-Index: AQHX7Dt/+5f88Aokk0KrqG0hbF8dN6wqFvxh
Date: Thu, 9 Dec 2021 12:25:24 +0000
Message-ID: <1711fc3548cd4b2699ccd4fffac17713@anonymous>
In-Reply-To: <75dd051097b02468183707ad0dd62ebd@posteo.de>
Thread-Index: AQHX8BVZ9B3+kB6CNUCtJ9eQIONSNawwSqpi
Date: Mon, 13 Dec 2021 11:35:26 +0000
Message-ID: <59b1d0c94a8d4834b7ab779a76647d44@mail.example.org>
In-Reply-To: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
Accept-Language: de-AT, de-DE, en-US
Content-Language: de-DE
X-MS-Has-Attach:
@@ -45,29 +45,29 @@ X-MS-TNEF-Correlator:
x-ms-exchange-transport-fromentityheader: Hosted
x-originating-ip: [192.168.120.215]
Content-Type: multipart/report;
boundary="_000_1711fc3548cd4b2699ccd4fffac17713anonymous_";
boundary="_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_";
report-type=disposition-notification
MIME-Version: 1.0
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: multipart/alternative;
boundary="_002_1711fc3548cd4b2699ccd4fffac17713anonymous_"
boundary="_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_"
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Ihre Nachricht
An: Anonymous
An: Anonymous_2
Betreff: Test message
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) Amsterdam, Ber=
lin, Bern, Rom, Stockholm, Wien
Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC+01:00) Amsterdam, Berl=
in, Bern, Rom, Stockholm, Wien
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amsterdam, Berl=
in, Bern, Rom, Stockholm, Wien gelesen.
wurde am Montag, 13. Dezember 2021 12:34:40 (UTC+01:00) Amsterdam, Berlin,=
Bern, Rom, Stockholm, Wien gelesen.
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: text/html; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
@@ -83,26 +83,26 @@ ding-left: 4pt; border-left: #800000 2px solid; } --></style>
<font size=3D"2"><span style=3D"font-size:10pt;">
<div class=3D"PlainText">Ihre Nachricht <br>
<br>
&nbsp;&nbsp; An: Anonymous<br>
&nbsp;&nbsp; An: Anonymous_2<br>
&nbsp;&nbsp; Betreff: Test message<br>
&nbsp;&nbsp; Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC&#43;01:00) =
Amsterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
&nbsp;&nbsp; Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC&#43;01:00) A=
msterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
<br>
&nbsp;wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC&#43;01:00) Amster=
dam, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
&nbsp;wurde am Montag, 13. Dezember 2021 12:34:40 (UTC&#43;01:00) Amsterdam=
, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
</span></font>
</body>
</html>
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_--
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: message/disposition-notification
Final-recipient: RFC822; anonymous@example.org
Final-recipient: RFC822; bob@example.net
Disposition: automatic-action/MDN-sent-automatically; displayed
X-MSExch-Correlation-Key: coNC5vaCQkiAOjek1v1Uew==
X-Display-Name: Anonymous
X-MSExch-Correlation-Key: nf7/jgN6Qk+WzsrkY5s9WA==
X-Display-Name: Anonymous_2
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_--
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--

View File

@@ -0,0 +1,34 @@
Received: We have to put a Received header here. Otherwise, the message would be ignored
because DC thinks it's a draft, and the test fails.
Alternatively, we could configure the Sentobox folder in the test.
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="=_3293d145f71bf71c4bb97415536759d6"
Date: Mon, 13 Dec 2021 12:33:58 +0100
From: Anonymous_1 <alice@example.org>
To: Anonymous_2 <bob@example.net>
Subject: Test message
Return-Receipt-To: Anonymous_1 <alice@example.org>
Disposition-Notification-To: Anonymous_1 <alice@example.org>
Message-ID: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
X-Sender: alice@example.org
User-Agent: Posteo Webmail
--=_3293d145f71bf71c4bb97415536759d6
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset=US-ASCII
This is a test!
Best regards
--=_3293d145f71bf71c4bb97415536759d6
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html><body style=3D'font-size: 10pt; font-family: Verdana,Geneva,sans-seri=
f'>
This is a test!<br /><br />Best regards
</body></html>
--=_3293d145f71bf71c4bb97415536759d6--

View File

@@ -0,0 +1,21 @@
Subject: webxdc object attached
Message-ID: 67890@example.org
Date: Fri, 03 Dec 2021 10:00:27 +0000
To: alice@example.org
From: bob@example.org
Chat-Version: 1.0
Content-Type: multipart/mixed; boundary="==BREAK=="
--==BREAK==
Content-Type: text/plain; charset=utf-8
webxdc with bad extension and bad content.
--==BREAK==
Content-Type: application/webxdc+zip
Content-Disposition: attachment; filename=index.html
<html>hey!<html>
--==BREAK==--

View File

@@ -0,0 +1,29 @@
Subject: webxdc object attached
Message-ID: 12345@example.org
Date: Fri, 03 Dec 2021 10:00:27 +0000
To: alice@example.org
From: bob@example.org
Chat-Version: 1.0
Content-Type: multipart/mixed; boundary="==BREAK=="
--==BREAK==
Content-Type: text/plain; charset=utf-8
webxdc with good extension;
the mimetype is ignored then,
content is checked.
--==BREAK==
Content-Type: text/html
Content-Disposition: attachment; filename=minimal.xdc
Content-Transfer-Encoding: base64
UEsDBBQAAgAIAFJqnVOItjSofAAAALwAAAAKABwAaW5kZXguaHRtbFVUCQADG1LMYV1SzGF1eAsAAQ
T1AQAABBQAAACzUXTxdw6JDHBVyCjJzbHjsoFQCgo2SfkplSCGgkJJanGJIphlU5xclFlQAhFWUEjJ
Ty7NTc0r0SsvyixJ1VAqyU8EqlTStIYo1kdWbZOXj6o5uiQjsxhodkWJQnFGfmlOikJefolCUiqIV5
4XCzUCWZeNPsRNNvoQRwIAUEsBAh4DFAACAAgAUmqdU4i2NKh8AAAAvAAAAAoAGAAAAAAAAQAAAKSB
AAAAAGluZGV4Lmh0bWxVVAUAAxtSzGF1eAsAAQT1AQAABBQAAABQSwUGAAAAAAEAAQBQAAAAwAAAAA
AA
--==BREAK==--

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

62
webxdc.md Normal file
View File

@@ -0,0 +1,62 @@
# webxdc: at least 200 times more interesting than Web3 :)
webxdc combines secure chat-messaging and web tech to provide fertile ground for
a decentralized secure web app eco-system without servers, coins or chains.
## How webxdc works
webxdc apps use existing chat groups (instead of web servers) to communicate with each other, with roughly this initial technical work flow:
1. Send a message to a chat with an webxdc app (archive or bundled html) as attachment.
2. On tapping chat members open a sandboxed system web view from the unpacked webxdc app.
Sandboxed webxdc apps are barred from making any network requests, reloading code or resources.
3. webxdc apps can not reach the internet themselves and rather can only send "app-state" update message to other app users in the chat group.
While this describes an already working "proof of concept" usage there are already a lot of further reaching discussions. However, we prefer to avoid "early over-generalizing" and rather evolve further mechanisms and features from real-world needs, especially from people living in precarious contexts with bad networks and affected by internet outages.
## Benefits to users of webxdc apps
- full & fast web interactivity available in an offline first manner
- no need for logins, accounts or discovery mechanisms because apps start and run privately
from an existing social chat context
- privacy by protocol: leaking of content or metadata from app usage
virtually impossible. No need for annoying GDPR or cookie consent dialogues.
- webxdc app messaging is e2e-encrypted by default and there is no
way to read or collect app state information, not even by webxdc developers
because their apps can not contact anyone outside the chat.
- no need to think about creating or representing yourself as a wallet :)
## Benefits to developers of webxdc apps
webxdc empowers FOSS developments in unprecedented ways:
- well-known paradim: use all the existing JS/html5 libraries and designs of your choice
- quick onboarding: only a handful API methods to learn
- serverless (but really): no worrying about hosting a server or configuring DNS, AWS etc
- permissionless: no worrying about registering at app stores for distribution
- unbuerocratic: no worrying about login/password/expiry procedures or leaks
- secure: no worrying about cryptographic algos or e2e-encryption complexity
On the flip side, you need to learn how to do state updates between instances of your webxdc apps. This is a classic P2P problem and there are simple (send full state update) and advanced ways (use CRDTs or similar) to arrange decentralized state. In any case, there is no DHT let alone blockchain needed and thus no Crypto or coin needed, either.
## Other important Benefits
Apps can become small again. 640KB should be enough for everybody, right? :)
Network usage is minimal. Webxdc is designed to run and to be tested with end-users in low-resourced precarious contexts that suffer internet and power outages regularly.
As webxdc does not require browsers to make remote network requests, a lot of the related attack surfaces and code complexity is cut out.
## Is webxdc tied to Delta Chat?
Webxdc apps could be used by other chat messengers. However, most of those are busy with introducing advertising or blockchains so we are not holding our breath right now. Rest assured, when the time comes and interest rises we'll be there and open to collaboration :)