Compare commits

..

122 Commits

Author SHA1 Message Date
holger krekel
dac320a810 fix speed issues with ORDER BY clause 2022-03-31 10:40:41 +02:00
dependabot[bot]
dce7b90fc2 cargo: bump native-tls from 0.2.8 to 0.2.10
Bumps [native-tls](https://github.com/sfackler/rust-native-tls) from 0.2.8 to 0.2.10.
- [Release notes](https://github.com/sfackler/rust-native-tls/releases)
- [Changelog](https://github.com/sfackler/rust-native-tls/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sfackler/rust-native-tls/compare/v0.2.8...v0.2.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-30 20:15:21 +02:00
dependabot[bot]
4f94bdff3f Merge pull request #3160 from deltachat/dependabot/cargo/async-trait-0.1.53 2022-03-29 13:44:16 +00:00
dependabot[bot]
ce1f2a6fd4 Merge pull request #3162 from deltachat/dependabot/cargo/quote-1.0.17 2022-03-29 13:43:22 +00:00
dependabot[bot]
da292bb9b2 cargo: bump quote from 1.0.16 to 1.0.17
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.16 to 1.0.17.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.16...1.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-29 08:07:43 +00:00
dependabot[bot]
326a75d0e8 Merge pull request #3161 from deltachat/dependabot/cargo/syn-1.0.90 2022-03-29 08:05:58 +00:00
dependabot[bot]
e47860bc2e cargo: bump syn from 1.0.89 to 1.0.90
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.89 to 1.0.90.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.89...1.0.90)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 21:14:00 +00:00
dependabot[bot]
6212151562 cargo: bump async-trait from 0.1.52 to 0.1.53
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.52 to 0.1.53.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.52...0.1.53)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 21:13:49 +00:00
link2xt
8c2b9f9901 Do not overwrite better_msg if apply_group_changes returns None 2022-03-27 11:23:45 +00:00
link2xt
e9a733a789 Pass better message around instead of mutating mimeparser
This change is aimed at decoupling parsing and
add_parts() stages to eventually separate parsing
from database changes and pipeline message parsing and
decryption.
2022-03-27 11:23:45 +00:00
Floris Bruynooghe
b2fe723570 Do not read whole webxdc file into memory
This seems not only wasteful but genuinly has the risk someone makes
their device useless by accidentally adding a huge file.

This also re-structures the checks a little: The if-conditions are
flattened out and cheap checks are done before more expensive ones.
2022-03-28 14:48:55 +02:00
link2xt
33ba8dabe0 Increase python test timeout 2022-03-27 08:48:43 +00:00
link2xt
0842e54f52 Add ephemeral_timestamp index for msgs table
This reduced get_chat_msgs() benchmark time from 400ms to 170ms.
2022-03-26 20:19:49 +00:00
link2xt
08d34e41c6 Return results from add_parts() via structure
Replaced mutable out parameters with explicit return of structure.
Also moved all decisions about emitted events out of add_parts(). Chat
ID is removed from created_db_entries as it is the same for all parts.
2022-03-26 16:38:08 +00:00
Hocuri
e93c9f74c9 Add get_chat_msgs benchmark (#3151) 2022-03-26 15:18:27 +01:00
bjoern
1ab81256e9 remove usued repl command 'event' (#3153)
no need to re-implement that unless there is actually some need.
2022-03-25 15:53:51 +01:00
dependabot[bot]
cb19de57bb Merge pull request #3144 from deltachat/dependabot/cargo/zip-0.6.0 2022-03-23 10:26:36 +00:00
dependabot[bot]
e678e7df8f Merge pull request #3146 from deltachat/dependabot/cargo/log-0.4.16 2022-03-23 10:22:21 +00:00
bjoern
8487eefe46 config_cache fixes (#3145)
* add simple backup export/import test

this test fails on current master
until the context is recrated.

* avoid config_cache races

adds needed SQL-statements to config_cache locking.

otherwise, another thread may alter the database
eg. between SELECT and the config_cache update -
resulting in the wrong value being written to config_cache.

* also update config_cache on initializing tables

VERSION_CFG is also set later, however,
not doing it here will result in bugs when we change DBVERSION at some point.

as this alters only VERSION_CFG and that is executed sequentially anyway,
race conditions between SQL and config_cache
seems not to be an issue in this case.

* clear config_cache after backup import

import replaces the whole database,
so config_cache needs to be invalidated as well.

we do that before import,
so in case a backup is imported only partly,
the cache does not add additional problems.

* update CHANGELOG
2022-03-22 22:46:29 +01:00
dependabot[bot]
86da1aa429 Merge pull request #3147 from deltachat/dependabot/cargo/async-std-1.11.0 2022-03-22 21:37:45 +00:00
dependabot[bot]
48b580b59e cargo: bump async-std from 1.10.0 to 1.11.0
Bumps [async-std](https://github.com/async-rs/async-std) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/async-rs/async-std/releases)
- [Changelog](https://github.com/async-rs/async-std/blob/master/CHANGELOG.md)
- [Commits](https://github.com/async-rs/async-std/compare/v1.10.0...v1.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 21:15:44 +00:00
dependabot[bot]
8b568d796e cargo: bump log from 0.4.14 to 0.4.16
Bumps [log](https://github.com/rust-lang/log) from 0.4.14 to 0.4.16.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 21:15:36 +00:00
dependabot[bot]
4b5af85094 cargo: bump zip from 0.5.13 to 0.6.0
Bumps [zip](https://github.com/zip-rs/zip) from 0.5.13 to 0.6.0.
- [Release notes](https://github.com/zip-rs/zip/releases)
- [Commits](https://github.com/zip-rs/zip/commits/v0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 21:16:26 +00:00
B. Petersen
8d0be06f45 log file size on backup import
due to an bug from Apple copying files from/to iPhones
(cmp. https://support.delta.chat/t/import-backup-to-ios/1628/7 )
it may easily happen that one gets corrupted/partly backups.

such imports usually fail with some error,
however, for debugging it is nice to have the concrete file size in the log.
2022-03-21 22:09:22 +01:00
link2xt
26ae8accd4 Automatically unblock chats with outgoing messages 2022-03-20 18:03:10 +00:00
Hocuri
321e3e27de Introduce config caching (#3131)
* Introduce config caching

* Changelog

* Update CHANGELOG.md

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

* Cache a value after reading it

Co-authored-by: bjoern <r10s@b44t.com>
2022-03-21 10:13:43 +00:00
link2xt
7d26968bb3 Try to start ephemeral timers only if some message has nonzero timer 2022-03-20 18:12:01 +00:00
link2xt
83464a882e Optimize markseen_msgs
Use a single SELECT statement for all messages
and start ephemeral timers for all messages at once.
2022-03-20 14:57:14 +00:00
Hocuri
1e94ad25e1 Use repeat_vars() more (#3133) 2022-03-20 15:23:11 +01:00
link2xt
a3ba19db96 Resultify delete_poi_location() 2022-03-19 17:29:54 +00:00
link2xt
d9e9c849e1 imap: do not delete duplicates
Currently if user moves the message into some other folder and then
moves the message back, the message is considered duplicate even
though previous copy was already deleted. This is a common problem
reported by users at least twice.

Keeping duplicates does no harm except for additional storage usage.
If the message is later deleted by the user, all the copies on the
server will be deleted. anyway.
2022-03-19 15:51:17 +00:00
dependabot[bot]
c162c23d9e Merge pull request #3135 from deltachat/dependabot/cargo/tagger-4.3.3 2022-03-18 23:22:23 +00:00
dependabot[bot]
90fd1c300f Merge pull request #3136 from deltachat/dependabot/cargo/quote-1.0.16 2022-03-18 23:21:09 +00:00
dependabot[bot]
902a9cc812 Merge pull request #3137 from deltachat/dependabot/cargo/libc-0.2.121 2022-03-18 23:20:24 +00:00
dependabot[bot]
c51e1805fa cargo: bump libc from 0.2.120 to 0.2.121
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.120 to 0.2.121.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.120...0.2.121)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-18 21:10:46 +00:00
dependabot[bot]
7a2b9e85e7 cargo: bump quote from 1.0.15 to 1.0.16
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.15 to 1.0.16.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.15...1.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-18 21:10:40 +00:00
dependabot[bot]
547c40cd52 cargo: bump tagger from 4.3.1 to 4.3.3
Bumps [tagger](https://github.com/tiby312/tagger) from 4.3.1 to 4.3.3.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-18 21:10:30 +00:00
Floris Bruynooghe
e2d631097d Fix master by reverting ContactId Display impl (#3134)
Actual fix needs more investigation, it's not obvious.
2022-03-17 19:29:18 +01:00
Floris Bruynooghe
cc55be0b0a Customise Display impl of ContactId
This brings the Display of ContactId in line with those of ChatId etc,
which is a bit clearer is logs and such places.

It also updates an SQL query to rely on the ToSql impl of ContactId
rather than it's Display when building the query.
2022-03-16 22:41:14 +01:00
dependabot[bot]
64927190bd Merge pull request #3132 from deltachat/dependabot/cargo/syn-1.0.89 2022-03-16 21:39:21 +00:00
dependabot[bot]
24515126fe cargo: bump syn from 1.0.88 to 1.0.89
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.88 to 1.0.89.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.88...1.0.89)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 21:14:45 +00:00
Hocuri
7a56a93028 Fix long filenames containing dots (#3098) 2022-03-16 20:41:24 +01:00
Hocuri
ea7fc3a171 Benchmark dc_receive_imf() (#3128)
Don't count the account creation in the receive emails benchmark

Use Criterion's async support

See https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_async.html
2022-03-16 20:30:33 +01:00
dependabot[bot]
ae36a26045 cargo: bump image from 0.23.14 to 0.24.1
Bumps [image](https://github.com/image-rs/image) from 0.23.14 to 0.24.1.
- [Release notes](https://github.com/image-rs/image/releases)
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 13:10:24 +01:00
dependabot[bot]
d6c9f5c64b cargo: bump textwrap from 0.14.2 to 0.15.0
Bumps [textwrap](https://github.com/mgeisler/textwrap) from 0.14.2 to 0.15.0.
- [Release notes](https://github.com/mgeisler/textwrap/releases)
- [Changelog](https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgeisler/textwrap/compare/0.14.2...0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 11:23:12 +01:00
dependabot[bot]
c4f4f4295b cargo: bump async-std-resolver from 0.20.4 to 0.21.1
Bumps [async-std-resolver](https://github.com/bluejekyll/trust-dns) from 0.20.4 to 0.21.1.
- [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.4...v0.21.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 10:59:56 +01:00
link2xt
a997322efb Update MSRV to 1.56 and current version to 1.59
This is needed to support Rust 2021 edition required by the latest versions of `ed25519` and `image` crates.
2022-03-16 10:56:16 +01:00
dependabot[bot]
799688af76 cargo: bump libc from 0.2.119 to 0.2.120
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.119 to 0.2.120.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.119...0.2.120)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 10:55:58 +01:00
dependabot[bot]
260e95d027 cargo: bump syn from 1.0.86 to 1.0.88
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.86 to 1.0.88.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.86...1.0.88)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 10:55:20 +01:00
Floris Bruynooghe
f9ee70aa2e Minor cleanup of Viewtype
Provide checking for attachment as a method and move it to the message
module.  The method is a lot easier to read and have correct
expectations about.
2022-03-16 10:46:58 +01:00
Hocuri
50f13cb84b Set X-Microsoft-Original-Message-ID on outgoing emails for amazonaws (#3077) 2022-03-13 14:39:49 +01:00
dependabot[bot]
fc7e08bb49 Merge pull request #3087 from deltachat/dependabot/cargo/strum-0.24.0 2022-03-13 11:44:29 +00:00
dependabot[bot]
06ed3e5dfd cargo: bump strum from 0.23.0 to 0.24.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-13 10:50:20 +00:00
dependabot[bot]
4d792ad57b Merge pull request #3089 from deltachat/dependabot/cargo/strum_macros-0.24.0 2022-03-13 10:48:56 +00:00
dependabot[bot]
4fa78bfca0 Merge pull request #3114 from deltachat/dependabot/cargo/once_cell-1.10.0 2022-03-13 10:29:47 +00:00
link2xt
2012833cb3 Fix lint 2022-03-12 20:07:00 +00:00
link2xt
e48eef7e32 Start ephemeral timer when seen status is synchronized via imap 2022-03-12 19:28:31 +00:00
bjoern
74ac9c3a92 fix docs: dc_markseen_msgs() is typically called when scrolling through message list, not chat list. (#3120) 2022-03-12 13:45:22 +01:00
Hocuri
a907d789d6 Assign replies from different address to two-member-groups (#3119)
Holger had a case where he wrote with someone using a classing MUA.

He opened a two-member-group with this person (which also allowed him to
set the subject).

At some point the other person replied from a different email address.

What he expected: This reply should be sorted into the two-member-group.
What happened: This reply was sorted into the 1:1 chat.

---

I had added the line && chat_contacts.contains(&from_id) months ago when I wrote
this code because it seemed vaguely sensible but without any real
reason. So, let's remove it and see if it creates other problems -
my gut feeling is no.
2022-03-12 10:47:58 +00:00
dependabot[bot]
fc46c0b49c Merge pull request #3121 from deltachat/dependabot/cargo/tagger-4.3.1 2022-03-12 10:36:51 +00:00
dependabot[bot]
fef7862045 cargo: bump tagger from 4.2.1 to 4.3.1
Bumps [tagger](https://github.com/tiby312/tagger) from 4.2.1 to 4.3.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-03-11 21:23:15 +00:00
Hocuri
d9441a6bdd Also resync UIDs in folders that are not configured (#2289) 2022-03-10 16:12:24 +01:00
Simon Laux
332cb0896b add note about perl requirement to readme
closes #3106
2022-03-10 12:53:34 +01:00
dependabot[bot]
d1b0c28924 Merge pull request #3084 from deltachat/dependabot/cargo/libc-0.2.119 2022-03-09 11:28:43 +00:00
dependabot[bot]
dce958aac4 Merge pull request #3115 from deltachat/dependabot/cargo/regex-1.5.5 2022-03-09 11:26:52 +00:00
Floris Bruynooghe
438940219e Introduce a ContactId newtype
This makes the contact ID its own newtype instead of being a plain
u32.  The change purposefully does not yet try and reap any benefits
from this yet, instead aiming for a boring change that's easy to
review.  Only exception is the ToSql/FromSql as not doing that yet
would also have created churn in the database code and it is easier to
go straight for the right solution here.
2022-03-08 22:57:51 +01:00
link2xt
f28fcec81d imap: do not treat messages without Message-ID as duplicates
Message-IDs are now retrieved only during fetching and saved into imap
table. dc_receive_imf_inner does not attempt to extract the Message-ID
anymore.

For messages without Message-ID the ID is now generated in
imap::fetch_new_messages rather than dc_receive_imf_inner,
so the same ID is used in the imap table (maintained by the imap
module) and msgs table (maintained by dc_receive_imf module).

Message-ID generation based on the Date, From and To field hashing has
been replaced with a simple dc_create_id() to avoid retrieving Date,
From, and To fields in the imap module, as it's hard to test that it
stays compatible between Delta Chat versions in this module. This
breaks jump-to-quote for quoted messages without Message-ID, which is
not critical.

Also prefetch X-Microsoft-Original-Message-ID, so retrieval of
duplicate messages with X-Microsoft-Original-Message-ID can be skipped
like it is done for messages with Message-ID header.
2022-03-08 15:23:22 +00:00
missytake
586d027f86 Merge pull request #3103 from deltachat/docs-gh-action
GitHub Action to build & upload the rust documentation to rs.delta.chat
2022-03-08 16:02:57 +01:00
gerryfrancis
bd4fb7486d Various corrections #1 (#2983)
* Various corrections

Monk business... ;)

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* use correct spelling for parameter name

Co-authored-by: B. Petersen <r10s@b44t.com>
2022-03-08 14:23:40 +00:00
dependabot[bot]
f9cd2b8f36 cargo: bump regex from 1.5.4 to 1.5.5
Bumps [regex](https://github.com/rust-lang/regex) from 1.5.4 to 1.5.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.5.4...1.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-08 14:08:48 +00:00
dependabot[bot]
62e22236b7 Merge pull request #3076 from deltachat/dependabot/cargo/sha2-0.10.2 2022-03-08 14:07:31 +00:00
dependabot[bot]
8b157f427a cargo: bump once_cell from 1.9.0 to 1.10.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.9.0...v1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-08 14:05:07 +00:00
dependabot[bot]
f165c1d9b0 Merge pull request #3110 from deltachat/dependabot/cargo/anyhow-1.0.56 2022-03-08 14:03:59 +00:00
bjoern
500e2d62a0 remove sentbox_move (#3111)
* remove SentboxMove

* adapt python test to removed sendbox_move option

* update CHANGELOG
2022-03-08 11:29:45 +01:00
bjoern
a06e8677ac Fix link to Mozilla (#3112)
it seems to be a bug on the Mozilla servers,
however, they take months to fix that, cmp.
https://bugzilla.mozilla.org/show_bug.cgi?id=1744432
so we just use archive.org for now.
2022-03-08 01:12:19 +01:00
dependabot[bot]
b4d5783928 cargo: bump anyhow from 1.0.53 to 1.0.56
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.53 to 1.0.56.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.53...1.0.56)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 21:24:16 +00:00
missytake
3ce7f45503 use rust toolchain of deltachat-core-rust 2022-03-06 13:28:06 +01:00
missytake
b436c2761a GitHub Action to build & upload the CFFI documentation 2022-03-06 09:59:32 +01:00
missytake
b586d3bb0e only build docs for deltachat crate 2022-03-06 09:43:39 +01:00
holger krekel
63688a2f95 remove getAllUpdates() and add a typical replicatio API for the update call (#3081)
* (r10s, adb, hpk) remove getAllUpdates() and add a typical replica-API that works with increasing serials.  Streamline docs a bit.

* adapt ffi to new api

* documentation: updates serials may have gaps

* get_webxdc_status_updates() return updates larger than a given serial

* remove status_update_id from status-update-event; it is not needed (ui should update from the last known serial) and easily gets confused with last_serial

* unify wording to 'StatusUpdateSerial'

* remove legacy payload format, all known webxdc should be adapted

* add serial and max_serial to status updates

* avoid races when getting max_serial by avoiding two SQL calls

* update changelog

Co-authored-by: B. Petersen <r10s@b44t.com>
2022-03-04 20:22:48 +01:00
missytake
379cb1b2e0 remove trailing slash, so it doesn't just copy the content of doc/ 2022-03-04 01:58:33 +01:00
missytake
78429492f1 fix: pass arguments to rsync github action 2022-03-04 01:45:05 +01:00
missytake
9875047674 docs github action: scp -> rsync 2022-03-04 01:31:00 +01:00
missytake
5014b0a9cb GitHub Action to build & upload the rust documentation 2022-03-03 18:11:55 +01:00
Floris Bruynooghe
ef841b1aa3 Securejoin: store bobstate in database instead of context
The state bob needs to maintain during a secure-join process when
exchanging messages used to be stored on the context.  This means if
the process was killed this state was lost and the securejoin process
would fail.  Moving this state into the database should help this.

This still only allows a single securejoin process at a time, this may
be relaxed in the future.  For now any previous securejoin process
that was running is killed if a new one is started (this was already
the case).

This can remove some of the complexity around BobState handling: since
the state is in the database we can already make state interactions
transactional and correct.  We no longer need the mutex around the
state handling.  This means the BobStateHandle construct that was
handling the interactions between always having a valid state and
handling the mutex is no longer needed, resulting in some nice
simplifications.

Part of #2777.
2022-03-01 23:02:40 +01:00
link2xt
368f27ffbc Update rusqlite to stable version 2022-02-27 20:00:35 +00:00
link2xt
0e50bc1443 Fix 1.59 clippy warnings 2022-02-27 13:29:02 +00:00
Hocuri
7c4a6ddcdf Add AcManager (#3073)
* Add AcManager

See https://github.com/deltachat/deltachat-core-rust/pull/2901#issuecomment-998285039

This reduces boilerplate code again therefore, improving the
signal-noise-ratio and reducing the mental barrier to start
writing a unit test.

Slightly off-topic:

I didn't add any advanced functions like `manager.get("alice");` because
they're not needed yet; however, once we have the AcManager we can
think about fancy things like:

```rust
acm.send_text(&alice, "Hi Bob, this is Alice!", &bob);
```
which automatically lets bob receive the message.

However, this may be less useful than it seems at first, since most of
the tests I looked at wouldn't benefit from it, so at least I won't do
it until I have a test that would benefit from it.

* Remove unnecessary RefCell

* Rename AcManager to TestContextManager

* Don't store TestContext's in a vec for now as we don't need this; we can re-add it later

* Rename acm -> tcm
2022-02-23 19:34:47 +01:00
link2xt
7ab6d95b6c mimefactory: place common IMF headers at the top of the message
This moves most common headers like From, To, Subject etc. defined in
the Internet Message Format standard at the top of the message
in the same order as used in RFC 5322.
2022-02-23 17:51:15 +00:00
link2xt
6c32b89906 smtp: add more logging 2022-02-23 17:04:30 +00:00
dependabot[bot]
056e659a20 cargo: bump strum_macros from 0.23.1 to 0.24.0
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.23.1 to 0.24.0.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-22 21:14:05 +00:00
dependabot[bot]
62baff665c cargo: bump libc from 0.2.117 to 0.2.119
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.117 to 0.2.119.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.117...0.2.119)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-21 21:14:06 +00:00
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
dependabot[bot]
65df02163d cargo: bump sha2 from 0.10.1 to 0.10.2
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.1 to 0.10.2.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.1...sha2-v0.10.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-17 21:14:06 +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
76 changed files with 4091 additions and 2897 deletions

View File

@@ -77,19 +77,19 @@ jobs:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.54.0
rust: 1.59.0
python: 3.9
- os: windows-latest
rust: 1.54.0
rust: 1.59.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.51.0
# Minimum Supported Rust Version = 1.56.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.51.0
rust: 1.56.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:

28
.github/workflows/upload-docs.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Build & Deploy Documentation on rs.delta.chat
on:
push:
branches:
- master
- docs-gh-action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps
- name: Upload to rs.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"

28
.github/workflows/upload-ffi-docs.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- master
- docs-gh-action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"

View File

@@ -2,20 +2,88 @@
## Unreleased
### API changes
- change semantics of `dc_get_webxdc_status_updates()` second parameter
and remove update-id from `DC_EVENT_WEBXDC_STATUS_UPDATE` #3081
### Fixes
- Hopefully fix a bug where outgoing messages appear twice with Amazon SES #3077
- do not delete messages without Message-IDs as duplicates #3095
- Assign replies from a different email address to the correct chat #3119
- start ephemeral timer when seen status is synchronized via IMAP #3122
- do not delete duplicate messages on IMAP immediately to accidentally deleting
the last copy #3138
### Changes
- add more SMTP logging #3093
- place common headers like `From:` before the large `Autocrypt:` header #3079
- keep track of securejoin joiner status in database to survive restarts #2920
- remove never used `SentboxMove` option #3111
- improve speed by caching config values #3131 #3145
- optimize `markseen_msgs` #3141
- automatically accept chats with outgoing messages #3143
- `dc_receive_imf` refactorings #3154 #3156
- add index to speedup deletion of expired ephemeral messages #3155
### Fixes
- Fix a bug where sometimes the file extension of a long filename containing a dot was cropped #3098
## 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
- Also resync UIDs in folders that are not configured #2289
- 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
- refactorings #3023
- remove direct dependency on `byteorder` crate #3031
- make it possible to cancel message sending by removing the message #3034,
this was previosuly removed in 1.71.0 #2939
- always skip Seen flag synchronization when there are no updates #3039
- 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

432
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package]
name = "deltachat"
version = "1.72.0"
version = "1.76.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
edition = "2021"
license = "MPL-2.0"
resolver = "2"
rust-version = "1.56"
[profile.dev]
debug = 0
@@ -12,9 +12,6 @@ debug = 0
[profile.release]
lto = true
[patch.crates-io]
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" }
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -23,8 +20,8 @@ anyhow = "1"
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-resolver = "0.21"
async-std = { version = "1" }
async-tar = { version = "0.4", default-features=false }
async-trait = "0.1"
backtrace = "0.3"
@@ -37,27 +34,26 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
imap-proto = "0.14.3"
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
log = {version = "0.4.8", optional = true }
log = {version = "0.4.16", optional = true }
mailparse = "0.13"
native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.9.0"
once_cell = "1.10.0"
percent-encoding = "2.0"
pgp = { version = "0.7", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.22"
r2d2 = "0.8"
r2d2_sqlite = "0.19"
r2d2_sqlite = "0.20"
rand = "0.7"
regex = "1.5"
rusqlite = { version = "0.26", features = ["sqlcipher"] }
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "9", optional = true }
sanitize-filename = "0.3"
@@ -67,8 +63,8 @@ sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
stop-token = "0.7"
strum = "0.23"
strum_macros = "0.23"
strum = "0.24"
strum_macros = "0.24"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
@@ -77,14 +73,14 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.2.1"
textwrap = "0.14.2"
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
tagger = "4.3.3"
textwrap = "0.15.0"
zip = { version = "0.6.0", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = "0.3"
criterion = { version = "0.3.4", features = ["async_std"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
@@ -120,6 +116,14 @@ harness = false
name = "search_msgs"
harness = false
[[bench]]
name = "receive_emails"
harness = false
[[bench]]
name = "get_chat_msgs"
harness = false
[features]
default = ["vendored"]
internals = []

View File

@@ -12,6 +12,8 @@ To download and install the official compiler for the Rust programming language,
$ curl https://sh.rustup.rs -sSf | sh
```
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
## Using the CLI client
Compile and run Delta Chat Core command line utility, using `cargo`:

38
benches/get_chat_msgs.rs Normal file
View File

@@ -0,0 +1,38 @@
use async_std::path::Path;
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;
let context = Context::new(dbfile.into(), id).await.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0, None).await.ok());
}
}
fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let chats: Vec<_> = async_std::task::block_on(async {
let context = Context::new((&path).into(), 100).await.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
let len = chatlist.len();
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
});
c.bench_function("Load all chats", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_msgs_benchmark(black_box(&path.as_ref()), black_box(&chats)))
});
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

84
benches/receive_emails.rs Normal file
View File

@@ -0,0 +1,84 @@
use async_std::{path::PathBuf, task::block_on};
use criterion::{
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
Criterion,
};
use deltachat::{
config::Config,
context::Context,
dc_receive_imf::dc_receive_imf,
imex::{imex, ImexMode},
};
use tempfile::tempdir;
async fn recv_all_emails(context: Context) -> Context {
for i in 0..100 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), "INBOX", false)
.await
.unwrap();
}
context
}
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(dbfile.into(), id).await.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()
.join("delta-chat-backup.tar")
.into();
if backup.exists().await {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup, None)
.await
.unwrap();
}
let addr = "alice@example.com";
context.set_config(Config::Addr, Some(addr)).await.unwrap();
context
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
context
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
context
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Receive messages");
group.bench_function("Receive 100 simple text msgs", |b| {
b.to_async(AsyncStdExecutor).iter_batched(
|| block_on(create_context()),
|context| recv_all_emails(black_box(context)),
BatchSize::LargeInput,
);
});
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -31,13 +31,13 @@ use rand::Rng;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin};
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
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::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -493,15 +493,17 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
let id = id.unwrap_or_default();
id as libc::c_int
id.to_u32() as libc::c_int
}
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
*progress as libc::c_int
}
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,
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcStatusUpdate(msg_id) => msg_id.to_u32() as libc::c_int,
}
}
@@ -534,6 +536,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate(_)
| EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
@@ -543,9 +546,6 @@ 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,
}
}
@@ -587,7 +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::WebxdcStatusUpdate(_)
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -717,7 +717,11 @@ pub unsafe extern "C" fn dc_get_chatlist(
let ctx = &*context;
let qs = to_opt_string_lossy(query_str);
let qi = if query_id == 0 { None } else { Some(query_id) };
let qi = if query_id == 0 {
None
} else {
Some(ContactId::new(query_id))
};
block_on(async move {
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
@@ -745,7 +749,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::create_for_contact(ctx, contact_id)
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
.await
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
@@ -765,7 +769,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::lookup_by_contact(ctx, contact_id)
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
.await
.log_err(ctx, "Failed to get chat for contact_id")
.unwrap_or_default() // unwraps the Result
@@ -903,7 +907,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
pub unsafe extern "C" fn dc_get_webxdc_status_updates(
context: *mut dc_context_t,
msg_id: u32,
status_update_id: u32,
last_known_serial: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_webxdc_status_updates()");
@@ -913,11 +917,7 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
block_on(ctx.get_webxdc_status_updates(
MsgId::new(msg_id),
if status_update_id == 0 {
None
} else {
Some(StatusUpdateId::new(status_update_id))
},
StatusUpdateSerial::new(last_known_serial),
))
.unwrap_or_else(|_| "".to_string())
.strdup()
@@ -1346,7 +1346,10 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let arr = dc_array_t::from(
chat::get_chat_contacts(ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
.unwrap_or_log_default(ctx, "Failed get_chat_contacts")
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
})
@@ -1456,7 +1459,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
block_on(chat::is_contact_in_chat(
ctx,
ChatId::new(chat_id),
contact_id,
ContactId::new(contact_id),
))
.log_err(ctx, "is_contact_in_chat failed")
.unwrap_or_default() as libc::c_int
@@ -1477,7 +1480,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
block_on(chat::add_contact_to_chat(
ctx,
ChatId::new(chat_id),
contact_id,
ContactId::new(contact_id),
))
.log_err(ctx, "Failed to add contact")
.is_ok() as libc::c_int
@@ -1498,7 +1501,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
block_on(chat::remove_contact_from_chat(
ctx,
ChatId::new(chat_id),
contact_id,
ContactId::new(contact_id),
))
.log_err(ctx, "Failed to remove contact")
.is_ok() as libc::c_int
@@ -1833,7 +1836,8 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
Contact::lookup_id_by_addr(ctx, &to_string_lossy(addr), Origin::IncomingReplyTo)
.await
.unwrap_or_log_default(ctx, "failed to lookup id")
.unwrap_or(0)
.map(|id| id.to_u32())
.unwrap_or_default()
})
}
@@ -1853,6 +1857,7 @@ pub unsafe extern "C" fn dc_create_contact(
block_on(async move {
Contact::create(ctx, &name, &to_string_lossy(addr))
.await
.map(|id| id.to_u32())
.unwrap_or(0)
})
}
@@ -1891,7 +1896,9 @@ pub unsafe extern "C" fn dc_get_contacts(
block_on(async move {
match Contact::get_all(ctx, flags, query).await {
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))),
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(
contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>(),
))),
Err(_) => ptr::null_mut(),
}
})
@@ -1928,7 +1935,10 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
Contact::get_all_blocked(ctx)
.await
.log_err(ctx, "Can't get blocked contacts")
.unwrap_or_default(),
.unwrap_or_default()
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
)))
})
}
@@ -1939,18 +1949,18 @@ pub unsafe extern "C" fn dc_block_contact(
contact_id: u32,
block: libc::c_int,
) {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL.to_u32() {
eprintln!("ignoring careless call to dc_block_contact()");
return;
}
let ctx = &*context;
block_on(async move {
if block == 0 {
Contact::unblock(ctx, contact_id)
Contact::unblock(ctx, ContactId::new(contact_id))
.await
.ok_or_log_msg(ctx, "Can't unblock contact");
} else {
Contact::block(ctx, contact_id)
Contact::block(ctx, ContactId::new(contact_id))
.await
.ok_or_log_msg(ctx, "Can't block contact");
}
@@ -1969,7 +1979,7 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
let ctx = &*context;
block_on(async move {
Contact::get_encrinfo(ctx, contact_id)
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
@@ -1984,14 +1994,14 @@ pub unsafe extern "C" fn dc_delete_contact(
context: *mut dc_context_t,
contact_id: u32,
) -> libc::c_int {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL.to_u32() {
eprintln!("ignoring careless call to dc_delete_contact()");
return 0;
}
let ctx = &*context;
block_on(async move {
match Contact::delete(ctx, contact_id).await {
match Contact::delete(ctx, ContactId::new(contact_id)).await {
Ok(_) => 1,
Err(_) => 0,
}
@@ -2010,7 +2020,7 @@ pub unsafe extern "C" fn dc_get_contact(
let ctx = &*context;
block_on(async move {
Contact::get_by_id(ctx, contact_id)
Contact::get_by_id(ctx, ContactId::new(contact_id))
.await
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
.unwrap_or_else(|_| ptr::null_mut())
@@ -2433,7 +2443,7 @@ pub unsafe extern "C" fn dc_array_get_contact_id(
return 0;
}
(*array).get_location(index).contact_id
(*array).get_location(index).contact_id.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_msg_id(
@@ -2949,7 +2959,7 @@ pub unsafe extern "C" fn dc_msg_get_from_id(msg: *mut dc_msg_t) -> u32 {
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_from_id()
ffi_msg.message.get_from_id().to_u32()
}
#[no_mangle]
@@ -3660,7 +3670,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.get_id()
ffi_contact.contact.get_id().to_u32()
}
#[no_mangle]

View File

@@ -111,19 +111,19 @@ impl Lot {
match self {
Self::Summary(_) => Default::default(),
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::FprOk { contact_id } => *contact_id,
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
Qr::FprOk { contact_id } => contact_id.to_u32(),
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id } => *contact_id,
Qr::Addr { contact_id } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::WithdrawVerifyGroup { .. } => Default::default(),
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::ReviveVerifyGroup { .. } => Default::default(),
},
Self::Error(_) => Default::default(),

View File

@@ -29,7 +29,7 @@ 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`: an object with the following properties:
- `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";
@@ -45,48 +45,37 @@ 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.
eg. if the webxdc instance comes as a contact request 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()`.
and you won't get the update by `setUpdateListener()`.
### setUpdateListener()
```js
window.webxdc.setUpdateListener((update) => {});
window.webxdc.setUpdateListener((update) => {}, serial);
```
With `setUpdateListener()` you define a callback that receives the updates
sent by `sendUpdate()`.
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
The `serial` specifies the last serial that you know about (defaults to 0).
- `update`: passed to the callback on updates with the following fields:
`update.payload`: equals the payload given to `sendUpdate()`
Each `update` which is passed to the callback comes with the following properties:
The callback is called for updates sent by you or other peers.
- `update.payload`: equals the payload given to `sendUpdate()`
- `update.serial`: the serial number of this update.
Serials are larger `0` and newer serials have higher numbers.
There may be gaps in the serials
and it is not guaranteed that the next serial is exactly incremented by one.
### getAllUpdates()
- `update.max_serial`: the maximum serial currently known.
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
```js
updates = await window.webxdc.getAllUpdates();
```
- `update.info`: optional, short, informational message (see `send_update`)
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 => {});
```
- `update.summary`: optional, short text, shown beside app icon (see `send_update`)
### selfAddr
@@ -162,9 +151,7 @@ The following example shows an input field and every input is show on all peers
document.getElementById('output').innerHTML += update.payload + "<br>";
}
window.webxdc.setUpdateListener(receiveUpdate);
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
window.webxdc.setUpdateListener(receiveUpdate, 0);
</script>
</body>
</html>
@@ -191,4 +178,4 @@ just clone and start adapting things to your need.
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.
allow to aggregate the state or add metadata.

View File

@@ -17,7 +17,7 @@ use deltachat::download::DownloadState;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
@@ -84,6 +84,7 @@ async fn reset_tables(context: &Context, bits: i32) {
)
.await
.unwrap();
context.sql().config_cache().write().await.clear();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
@@ -209,7 +210,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
contact_id,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == 1 {
if msg.get_from_id() == DC_CONTACT_ID_SELF {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -267,9 +268,8 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
Ok(())
}
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
for contact_id in contacts {
let line;
let mut line2 = "".to_string();
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
@@ -284,24 +284,20 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
} else {
""
};
line = format!(
let line = format!(
"{}{} <{}>",
if !name.is_empty() {
&name
name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() {
&addr
} else {
"addr unset"
}
if !addr.is_empty() { addr } else { "addr unset" }
);
let peerstate = Peerstate::from_addr(context, &addr)
let peerstate = Peerstate::from_addr(context, addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 {
if peerstate.is_some() && *contact_id != DC_CONTACT_ID_SELF {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
@@ -430,7 +426,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
joinqr <qr-content>\n\
setqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
clear -- clear screen\n\
@@ -719,7 +714,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: u32 = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
println!("Single#{} created successfully.", chat_id,);
@@ -747,7 +742,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: u32 = arg1.parse()?;
let contact_id_0 = ContactId::new(arg1.parse()?);
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
.await?;
println!("Contact added to chat.");
@@ -755,7 +750,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"removemember" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: u32 = arg1.parse()?;
let contact_id_1 = ContactId::new(arg1.parse()?);
chat::remove_contact_from_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
@@ -771,7 +766,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::set_chat_name(
&context,
sel_chat.as_ref().unwrap().get_id(),
&format!("{} {}", arg1, arg2).trim(),
format!("{} {}", arg1, arg2).trim(),
)
.await?;
@@ -1139,7 +1134,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: u32 = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr();
@@ -1174,16 +1169,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"delcontact" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(&context, arg1.parse()?).await?;
Contact::delete(&context, ContactId::new(arg1.parse()?)).await?;
}
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
Contact::block(&context, contact_id).await?;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
Contact::unblock(&context, contact_id).await?;
}
"listblocked" => {
@@ -1224,17 +1219,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
// event, event as usize, r,
// );
// }
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");

View File

@@ -227,13 +227,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 12] = [
const MISC_COMMANDS: [&str; 11] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"event",
"fileinfo",
"clear",
"exit",
@@ -416,7 +415,7 @@ async fn handle_cmd(
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
@@ -433,7 +432,7 @@ async fn handle_cmd(
}
"getqrsvg" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {

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

@@ -652,8 +652,6 @@ class TestOnlineAccount:
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
# rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(240)
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -892,11 +890,11 @@ class TestOnlineAccount:
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message2")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message3")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_forward_messages(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1101,6 +1099,30 @@ class TestOnlineAccount:
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
lp.sec("Send an ephemeral message from ac2 to ac1")
ac2_chat.set_ephemeral_timer(60)
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1._evtracker.wait_next_incoming_message()
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1_clone._evtracker.wait_next_incoming_message()
ac2_chat.send_text("Foobar")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
assert "Expires: " in ac1_message.get_message_info()
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_message_info()
def test_message_override_sender_name(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -1400,11 +1422,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()
@@ -1412,6 +1436,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:
@@ -1436,6 +1461,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")
@@ -1449,6 +1483,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")
@@ -1834,7 +1872,6 @@ class TestOnlineAccount:
lp.sec("trigger ac setup message and return setupcode")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
setup_code = ac1.initiate_key_transfer()
ac2._evtracker.set_timeout(30)
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.is_setup_message()
@@ -1851,7 +1888,6 @@ class TestOnlineAccount:
def test_ac_setup_message_twice(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1)
ac2._evtracker.set_timeout(30)
acfactory.wait_configure_and_start_io()
lp.sec("trigger ac setup message but ignore")
@@ -2025,7 +2061,7 @@ class TestOnlineAccount:
lp.sec("ac1: send a message to group chat to promote the group")
chat.send_text("afterwards promoted")
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == \
@@ -2035,29 +2071,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
@@ -2504,8 +2540,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")
@@ -2682,7 +2717,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 + ")")
@@ -2715,7 +2756,6 @@ class TestOnlineAccount:
assert ac.get_config("configured_mvbox_folder")
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
ac1.set_config("sentbox_move", "1")
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure(ac1)
@@ -2735,7 +2775,7 @@ class TestOnlineAccount:
if mvbox_move:
ac1.direct_imap.select_config_folder("mvbox")
else:
ac1.direct_imap.select_config_folder("sentbox")
ac1.direct_imap.select_folder("INBOX")
ac1.direct_imap.idle_start()
lp.sec("send out message with bcc to ourselves")

View File

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

View File

@@ -1 +1 @@
1.54.0
1.59.0

View File

@@ -8,7 +8,7 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
RUST_VERSION=1.59.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -8,7 +8,7 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
RUST_VERSION=1.59.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -3,29 +3,26 @@
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::format_err;
use anyhow::Context as _;
use anyhow::Error;
use image::DynamicImage;
use image::GenericImageView;
use image::ImageFormat;
use anyhow::{format_err, Context as _, Error};
use image::{DynamicImage, ImageFormat};
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::config::Config;
use crate::constants::{
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
WORSE_IMAGE_SIZE,
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message;
use crate::message::Viewtype;
/// Represents a file in the blob directory.
///
@@ -292,7 +289,7 @@ impl<'a> BlobObject<'a> {
/// Returns the filename of the blob.
pub fn as_file_name(&self) -> &str {
self.name.rsplitn(2, '/').next().unwrap()
self.name.rsplit('/').next().unwrap()
}
/// The path relative in the blob directory.
@@ -305,7 +302,7 @@ impl<'a> BlobObject<'a> {
/// If a blob's filename has an extension, it is always guaranteed
/// to be lowercase.
pub fn suffix(&self) -> Option<&str> {
let ext = self.name.rsplitn(2, '.').next();
let ext = self.name.rsplit('.').next();
if ext == Some(&self.name) {
None
} else {
@@ -348,13 +345,30 @@ impl<'a> BlobObject<'a> {
};
let clean = sanitize_filename::sanitize_with_options(name, opts);
// Let's take the tricky filename
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
let mut iter = clean.splitn(2, '.');
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
// stem == "file"
let ext_chars = iter.next().unwrap_or_default().chars();
let ext: String = ext_chars
.rev()
.take(32)
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
// ext == "d_point_and_double_ending.tar.gz"
if ext.is_empty() {
(stem, "".to_string())
} else {
(stem, format!(".{}", ext).to_lowercase())
// Return ("file", ".d_point_and_double_ending.tar.gz")
// which is not perfect but acceptable.
}
}
@@ -449,7 +463,8 @@ impl<'a> BlobObject<'a> {
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
img.write_to(encoded, image::ImageFormat::Jpeg)?;
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
@@ -619,16 +634,14 @@ pub enum BlobError {
mod tests {
use fs::File;
use super::*;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::{
chat,
message::Message,
test_utils::{self, TestContext},
};
use anyhow::Result;
use image::Pixel;
use image::{GenericImageView, Pixel};
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::Message;
use crate::test_utils::{self, TestContext};
use super::*;
#[async_std::test]
async fn test_create() {
@@ -960,10 +973,11 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
let mut bytes = vec![];
let mut buf = Cursor::new(vec![]);
img_rotated
.write_to(&mut bytes, image::ImageFormat::Jpeg)
.write_to(&mut buf, image::ImageFormat::Jpeg)
.unwrap();
let bytes = buf.into_inner();
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes,

View File

@@ -15,12 +15,11 @@ use crate::blob::{BlobError, BlobObject};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, Viewtype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCM_ADDDAYMARKER, DC_GCM_INFO_ONLY,
DC_RESEND_USER_AVATAR_DAYS,
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_INFO, DC_CONTACT_ID_LAST_SPECIAL,
DC_CONTACT_ID_SELF, DC_GCM_ADDDAYMARKER, DC_GCM_INFO_ONLY, DC_RESEND_USER_AVATAR_DAYS,
};
use crate::contact::{addr_cmp, Contact, Origin, VerifiedStatus};
use crate::contact::{addr_cmp, Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_receive_imf::ReceivedMsg;
use crate::dc_tools::{
@@ -32,8 +31,7 @@ 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::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
@@ -42,6 +40,7 @@ use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone)]
@@ -153,7 +152,10 @@ impl ChatId {
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
///
/// If it does not exist, `None` is returned.
pub async fn lookup_by_contact(context: &Context, contact_id: u32) -> Result<Option<Self>> {
pub async fn lookup_by_contact(
context: &Context,
contact_id: ContactId,
) -> Result<Option<Self>> {
ChatIdBlocked::lookup_by_contact(context, contact_id)
.await
.map(|lookup| lookup.map(|chat| chat.id))
@@ -166,7 +168,7 @@ impl ChatId {
/// This is an internal API, if **a user action** needs to get a chat
/// [`ChatId::create_for_contact`] should be used as this also scales up the
/// [`Contact`]'s origin.
pub async fn get_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
pub async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.map(|chat| chat.id)
@@ -175,8 +177,8 @@ impl ChatId {
/// Returns the unblocked 1:1 chat with `contact_id`.
///
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
/// exists and is unblocked and scales the [`Contact`]'s origin.
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
/// exists, is unblocked and scales the [`Contact`]'s origin.
pub async fn create_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
}
@@ -185,7 +187,7 @@ impl ChatId {
/// `create_blocked` won't block already unblocked chats again.
pub(crate) async fn create_for_contact_with_blocked(
context: &Context,
contact_id: u32,
contact_id: ContactId,
create_blocked: Blocked,
) -> Result<Self> {
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
@@ -420,7 +422,7 @@ impl ChatId {
context: &Context,
protect: ProtectionStatus,
promote: bool,
from_id: u32,
from_id: ContactId,
) -> Result<()> {
let msg_text = context.stock_protection_msg(protect, from_id).await;
let cmd = match protect {
@@ -474,22 +476,21 @@ impl ChatId {
self
);
if visibility == ChatVisibility::Archived {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)
.await?;
}
context
.sql
.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
)
.transaction(move |transaction| {
if visibility == ChatVisibility::Archived {
transaction.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)?;
}
transaction.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
)?;
Ok(())
})
.await?;
context.emit_event(EventType::MsgsChanged {
@@ -961,7 +962,7 @@ impl std::fmt::Display for ChatId {
/// well as query for a [ChatId].
impl rusqlite::types::ToSql for ChatId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -971,7 +972,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))
@@ -1161,7 +1162,7 @@ impl Chat {
/// deltachat, and the data returned is still subject to change.
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
let draft = match self.id.get_draft(context).await? {
Some(message) => message.text.unwrap_or_else(String::new),
Some(message) => message.text.unwrap_or_default(),
_ => String::new(),
};
Ok(ChatInfo {
@@ -1629,7 +1630,11 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
Ok(icon)
}
async fn update_special_chat_name(context: &Context, contact_id: u32, name: String) -> Result<()> {
async fn update_special_chat_name(
context: &Context,
contact_id: ContactId,
name: String,
) -> Result<()> {
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
// the `!= name` condition avoids unneeded writes
context
@@ -1676,9 +1681,15 @@ impl ChatIdBlocked {
/// Searches the database for the 1:1 chat with this contact.
///
/// If no chat is found `None` is returned.
pub async fn lookup_by_contact(context: &Context, contact_id: u32) -> Result<Option<Self>> {
pub async fn lookup_by_contact(
context: &Context,
contact_id: ContactId,
) -> Result<Option<Self>> {
ensure!(context.sql.is_open().await, "Database not available");
ensure!(contact_id > 0, "Invalid contact id requested");
ensure!(
contact_id > ContactId::new(0),
"Invalid contact id requested"
);
context
.sql
@@ -1707,11 +1718,14 @@ impl ChatIdBlocked {
/// state.
pub async fn get_for_contact(
context: &Context,
contact_id: u32,
contact_id: ContactId,
create_blocked: Blocked,
) -> Result<Self> {
ensure!(context.sql.is_open().await, "Database not available");
ensure!(contact_id > 0, "Invalid contact id requested");
ensure!(
contact_id > ContactId::new(0),
"Invalid contact id requested"
);
if let Some(res) = Self::lookup_by_contact(context, contact_id).await? {
// Already exists, no need to create.
@@ -1793,26 +1807,10 @@ pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message)
Ok(msg_id)
}
pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
match msgtype {
Viewtype::Unknown => false,
Viewtype::Text => false,
Viewtype::Image => true,
Viewtype::Gif => true,
Viewtype::Sticker => true,
Viewtype::Audio => true,
Viewtype::Voice => true,
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Webxdc => true,
}
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
// the caller should check if the message text is empty
} else if msgtype_has_file(msg.viewtype) {
} else if msg.viewtype.has_file() {
let blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())
@@ -1915,7 +1913,7 @@ async fn prepare_msg_common(
pub async fn is_contact_in_chat(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
) -> Result<bool> {
// this function works for group and for normal chats, however, it is more useful
// for group chats.
@@ -1926,7 +1924,7 @@ pub async fn is_contact_in_chat(
.sql
.exists(
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
paramsv![chat_id, contact_id as i32],
paramsv![chat_id, contact_id],
)
.await?;
Ok(exists)
@@ -2228,9 +2226,12 @@ pub async fn get_chat_msgs(
|row: &rusqlite::Row| {
// is_info logic taken from Message.is_info()
let params = row.get::<_, String>("param")?;
let (from_id, to_id) = (row.get::<_, u32>("from_id")?, row.get::<_, u32>("to_id")?);
let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO as u32
|| to_id == DC_CONTACT_ID_INFO as u32
let (from_id, to_id) = (
row.get::<_, ContactId>("from_id")?,
row.get::<_, ContactId>("to_id")?,
);
let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO
|| to_id == DC_CONTACT_ID_INFO
|| match Params::from_str(&params) {
Ok(p) => {
let cmd = p.get_cmd();
@@ -2306,11 +2307,11 @@ pub async fn get_chat_msgs(
context
.sql
.query_map(
"SELECT m.id AS id, m.timestamp AS timestamp
"SELECT (m.id+1) AS id, (m.timestamp+1) AS timestamp
FROM msgs m
WHERE m.chat_id=?
AND m.hidden=0
ORDER BY m.timestamp, m.id;",
ORDER BY timestamp, id;",
paramsv![chat_id],
process_row,
process_rows,
@@ -2531,7 +2532,7 @@ pub async fn get_next_media(
}
/// Returns a vector of contact IDs for given chat ID.
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<u32>> {
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
@@ -2545,7 +2546,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
WHERE cc.chat_id=?
ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;",
paramsv![chat_id],
|row| row.get::<_, u32>(0),
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
@@ -2652,13 +2653,13 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id as i32],
paramsv![chat_id, contact_id],
)
.await?;
Ok(())
@@ -2668,13 +2669,13 @@ pub(crate) async fn add_to_chat_contacts_table(
pub(crate) async fn remove_from_chat_contacts_table(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
) -> Result<()> {
context
.sql
.execute(
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
paramsv![chat_id, contact_id as i32],
paramsv![chat_id, contact_id],
)
.await?;
Ok(())
@@ -2684,7 +2685,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
pub async fn add_contact_to_chat(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
) -> Result<()> {
add_contact_to_chat_ex(context, chat_id, contact_id, false).await?;
Ok(())
@@ -2693,7 +2694,7 @@ pub async fn add_contact_to_chat(
pub(crate) async fn add_contact_to_chat_ex(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
from_handshake: bool,
) -> Result<bool> {
ensure!(!chat_id.is_special(), "can not add member to special chats");
@@ -2770,9 +2771,8 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
msg.text = Some(
stock_str::msg_add_member(context, contact.get_addr(), DC_CONTACT_ID_SELF as u32).await,
);
msg.text =
Some(stock_str::msg_add_member(context, contact.get_addr(), DC_CONTACT_ID_SELF).await);
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.param.set_int(Param::Arg2, from_handshake.into());
@@ -2875,7 +2875,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
pub async fn remove_contact_from_chat(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
) -> Result<()> {
ensure!(
!chat_id.is_special(),
@@ -2904,15 +2904,14 @@ pub async fn remove_contact_from_chat(
msg.viewtype = Viewtype::Text;
if contact.id == DC_CONTACT_ID_SELF {
set_group_explicitly_left(context, &chat.grpid).await?;
msg.text = Some(
stock_str::msg_group_left(context, DC_CONTACT_ID_SELF as u32).await,
);
msg.text =
Some(stock_str::msg_group_left(context, DC_CONTACT_ID_SELF).await);
} else {
msg.text = Some(
stock_str::msg_del_member(
context,
contact.get_addr(),
DC_CONTACT_ID_SELF as u32,
DC_CONTACT_ID_SELF,
)
.await,
);
@@ -3005,13 +3004,8 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast {
msg.viewtype = Viewtype::Text;
msg.text = Some(
stock_str::msg_grp_name(
context,
&chat.name,
&new_name,
DC_CONTACT_ID_SELF as u32,
)
.await,
stock_str::msg_grp_name(context, &chat.name, &new_name, DC_CONTACT_ID_SELF)
.await,
);
msg.param.set_cmd(SystemMessage::GroupNameChanged);
if !chat.name.is_empty() {
@@ -3064,7 +3058,7 @@ pub async fn set_chat_profile_image(
if new_image.as_ref().is_empty() {
chat.param.remove(Param::ProfileImage);
msg.param.remove(Param::Arg);
msg.text = Some(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF as u32).await);
msg.text = Some(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF).await);
} else {
let mut image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) {
Ok(blob) => Ok(blob),
@@ -3078,7 +3072,7 @@ pub async fn set_chat_profile_image(
image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF as u32).await);
msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF).await);
}
chat.update_param(context).await?;
if chat.is_promoted() && !chat.is_mailing_list() {
@@ -3109,7 +3103,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
.query_map(
format!(
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
msg_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
sql::repeat_vars(msg_ids.len())?
),
rusqlite::params_from_iter(msg_ids),
|row| row.get::<_, MsgId>(0),
@@ -3906,7 +3900,7 @@ mod tests {
async fn test_self_talk() -> Result<()> {
let t = TestContext::new_alice().await;
let chat = &t.get_self_chat().await;
assert_eq!(DC_CONTACT_ID_SELF, 1);
assert_eq!(DC_CONTACT_ID_SELF, ContactId::new(1));
assert!(!chat.id.is_special());
assert!(chat.is_self_talk());
assert!(chat.visibility == ChatVisibility::Normal);
@@ -4323,7 +4317,7 @@ mod tests {
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de")
.await
.unwrap();
assert_ne!(contact1, 0);
assert_ne!(contact1, ContactId::new(0));
let chat_id = ChatId::create_for_contact(&context.ctx, contact1)
.await
@@ -4528,7 +4522,7 @@ mod tests {
// create contact, then unblocked chat
let contact_id = Contact::create(&ctx, "", "bob@foo.de").await.unwrap();
assert_ne!(contact_id, 0);
assert_ne!(contact_id, ContactId::new(0));
let found = ChatId::lookup_by_contact(&ctx, contact_id).await.unwrap();
assert!(found.is_none());
@@ -4554,10 +4548,14 @@ mod tests {
assert_eq!(chat2.blocked, Blocked::Yes);
// test nonexistent contact
let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap();
let found = ChatId::lookup_by_contact(&ctx, ContactId::new(1234))
.await
.unwrap();
assert!(found.is_none());
let found = ChatIdBlocked::lookup_by_contact(&ctx, 1234).await.unwrap();
let found = ChatIdBlocked::lookup_by_contact(&ctx, ContactId::new(1234))
.await
.unwrap();
assert!(found.is_none());
}

View File

@@ -8,7 +8,7 @@ use crate::constants::{
DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT, DC_GCL_ARCHIVED_ONLY,
DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::Contact;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::ephemeral::delete_expired_messages;
use crate::message::{Message, MessageState, MsgId};
@@ -85,7 +85,7 @@ impl Chatlist {
context: &Context,
listflags: usize,
query: Option<&str>,
query_contact_id: Option<u32>,
query_contact_id: Option<ContactId>,
) -> Result<Self> {
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
@@ -147,7 +147,7 @@ impl Chatlist {
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
process_row,
process_rows,
).await?
@@ -375,8 +375,8 @@ mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Viewtype;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;

View File

@@ -71,8 +71,12 @@ pub enum Config {
#[strum(props(default = "1"))]
MvboxMove,
/// 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"))]
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
OnlyFetchMvbox,
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
@@ -225,6 +229,11 @@ impl Context {
Ok(self.get_config_int(key).await? != 0)
}
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -233,7 +242,7 @@ impl Context {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
x => Ok(Some(i64::from(x))),
}
}
@@ -255,7 +264,7 @@ impl Context {
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(x as i64)),
x => Ok(Some(i64::from(x))),
}
}
@@ -281,31 +290,25 @@ impl Context {
}
}
self.emit_event(EventType::SelfavatarChanged);
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
let ret = self.sql.set_raw_config(key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
ret?
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
Ok(())
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {

View File

@@ -11,21 +11,20 @@ use async_std::task;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::dc_tools::EmailAddress;
use crate::config::Config;
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress};
use crate::imap::Imap;
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::oauth2::dc_get_oauth2_addr;
use crate::param::Params;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock_str;
use crate::{chat, e2ee, provider};
use crate::{config::Config, dc_tools::time};
use crate::{
constants::{Viewtype, DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2},
job,
};
use crate::{context::Context, param::Params};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -443,7 +442,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?;

View File

@@ -4,6 +4,7 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
use crate::contact::ContactId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
@@ -179,11 +180,11 @@ pub const DC_ELLIPSIS: &str = "[...]";
/// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
pub const DC_CONTACT_ID_INFO: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
pub const DC_CONTACT_ID_UNDEFINED: ContactId = ContactId::new(0);
pub const DC_CONTACT_ID_SELF: ContactId = ContactId::new(1);
pub const DC_CONTACT_ID_INFO: ContactId = ContactId::new(2);
pub const DC_CONTACT_ID_DEVICE: ContactId = ContactId::new(5);
pub const DC_CONTACT_ID_LAST_SPECIAL: ContactId = ContactId::new(9);
// decorative address that is used for DC_CONTACT_ID_DEVICE
// when an api that returns an email is called.
@@ -230,82 +231,6 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
/// Text message.
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().
Text = 10,
/// Image message.
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
Image = 20,
/// Animated GIF message.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
Gif = 21,
/// Message containing a sticker, similar to image.
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
/// Message containing an Audio file.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
Audio = 40,
/// A voice message that was directly recorded by the user.
/// For all other audio messages, the type #DC_MSG_AUDIO should be used.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
Voice = 41,
/// Video messages.
/// File, width, height and durarion
/// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
/// and retrieved via
/// dc_msg_get_file(), dc_msg_get_width(),
/// dc_msg_get_height(), dc_msg_get_duration().
Video = 50,
/// Message containing any file, eg. a PDF.
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an webxdc instance.
Webxdc = 80,
}
impl Default for Viewtype {
fn default() -> Self {
Viewtype::Unknown
}
}
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -317,33 +242,9 @@ pub enum KeyType {
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
#[test]
fn test_derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
#[test]
fn test_viewtype_values() {
// values may be written to disk and must not change
assert_eq!(Viewtype::Unknown, Viewtype::default());
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
}
use super::*;
#[test]
fn test_chattype_values() {

View File

@@ -1,12 +1,14 @@
//! Contacts module
use std::convert::{TryFrom, TryInto};
use std::fmt;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -27,6 +29,66 @@ use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::{chat, stock_str};
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct ContactId(u32);
impl ContactId {
/// Creates a new [`ContactId`].
pub const fn new(id: u32) -> ContactId {
ContactId(id)
}
/// Bad evil escape hatch, do not use.
pub const fn to_u32(&self) -> u32 {
self.0
}
}
impl fmt::Display for ContactId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
// if *self == DC_CONTACT_ID_UNDEFINED {
// write!(f, "Contact#Undefined")
// } else if *self == DC_CONTACT_ID_SELF {
// write!(f, "Contact#Self")
// } else if *self == DC_CONTACT_ID_INFO {
// write!(f, "Contact#Info")
// } else if *self == DC_CONTACT_ID_DEVICE {
// write!(f, "Contact#Device")
// } else if *self <= DC_CONTACT_ID_LAST_SPECIAL {
// write!(f, "Contact#Special{}", self.0)
// } else {
// write!(f, "Contact#{}", self.0)
// }
}
}
/// Allow converting [`ContactId`] to an SQLite type.
impl rusqlite::types::ToSql for ContactId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Allow converting an SQLite integer directly into [`ContactId`].
impl rusqlite::types::FromSql for ContactId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
val.try_into()
.map(ContactId::new)
.map_err(|_| rusqlite::types::FromSqlError::OutOfRange(val))
})
}
}
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
@@ -48,7 +110,7 @@ pub struct Contact {
/// `dc_set_config` using "addr".
///
/// Normal contact IDs are larger than these special ones (larger than DC_CONTACT_ID_LAST_SPECIAL).
pub id: u32,
pub id: ContactId,
/// Contact name. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
@@ -183,7 +245,7 @@ impl Default for VerifiedStatus {
}
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
let mut contact = context
.sql
.query_row(
@@ -191,7 +253,7 @@ impl Contact {
c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id as i32],
paramsv![contact_id],
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
@@ -245,18 +307,18 @@ impl Contact {
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
Ok(blocked)
}
/// Block the given contact.
pub async fn block(context: &Context, id: u32) -> Result<()> {
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
set_block_contact(context, id, true).await
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
set_block_contact(context, id, false).await
}
@@ -269,7 +331,7 @@ impl Contact {
/// a bunch of addresses.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<u32> {
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
let name = improve_single_line_input(name);
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
@@ -292,12 +354,12 @@ impl Contact {
}
/// Mark messages from a contact as noticed.
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
pub async fn mark_noticed(context: &Context, id: ContactId) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
)
.await?;
Ok(())
@@ -313,7 +375,7 @@ impl Contact {
context: &Context,
addr: &str,
min_origin: Origin,
) -> Result<Option<u32>> {
) -> Result<Option<ContactId>> {
if addr.is_empty() {
bail!("lookup_id_by_addr: empty address");
}
@@ -333,7 +395,7 @@ impl Contact {
AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_CONTACT_ID_LAST_SPECIAL,
min_origin as u32,
],
)
@@ -371,7 +433,7 @@ impl Contact {
name: &str,
addr: &str,
mut origin: Origin,
) -> Result<(u32, Modifier)> {
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
@@ -500,7 +562,7 @@ impl Contact {
paramsv![Chattype::Single, isize::try_from(row_id)?]
).await?;
if let Some(chat_id) = chat_id {
let contact = Contact::get_by_id(context, row_id as u32).await?;
let contact = Contact::get_by_id(context, ContactId::new(row_id)).await?;
let chat_name = contact.get_display_name();
match context
.sql
@@ -557,7 +619,7 @@ impl Contact {
}
}
Ok((row_id, sth_modified))
Ok((ContactId::new(row_id), sth_modified))
}
/// Add a number of contacts.
@@ -617,7 +679,7 @@ impl Contact {
context: &Context,
listflags: u32,
query: Option<impl AsRef<str>>,
) -> Result<Vec<u32>> {
) -> Result<Vec<ContactId>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
@@ -644,16 +706,16 @@ impl Contact {
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_CONTACT_ID_LAST_SPECIAL,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
],
|row| row.get::<_, i32>(0),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
ret.push(id?);
}
Ok(())
},
@@ -690,13 +752,13 @@ impl Contact {
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_CONTACT_ID_LAST_SPECIAL,
Origin::IncomingReplyTo
],
|row| row.get::<_, i32>(0),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
ret.push(id?);
}
Ok(())
},
@@ -767,7 +829,7 @@ impl Contact {
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
Contact::update_blocked_mailinglist_contacts(context)
.await
.context("cannot update blocked mailinglist contacts")?;
@@ -776,8 +838,8 @@ impl Contact {
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
|row| row.get::<_, ContactId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
@@ -792,7 +854,7 @@ impl Contact {
/// This function returns a string explaining the encryption state
/// of the contact and if the connection is encrypted the
/// fingerprints of the keys involved.
pub async fn get_encrinfo(context: &Context, contact_id: u32) -> Result<String> {
pub async fn get_encrinfo(context: &Context, contact_id: ContactId) -> Result<String> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not provide encryption info for special contact"
@@ -861,7 +923,7 @@ impl Contact {
/// possible as the contact is in use. In this case, the contact can be blocked.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn delete(context: &Context, contact_id: u32) -> Result<()> {
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not delete special contact"
@@ -871,17 +933,14 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
paramsv![contact_id],
)
.await?;
if count_chats == 0 {
match context
.sql
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
.await
{
Ok(_) => {
@@ -907,7 +966,7 @@ impl Contact {
/// For contact DC_CONTACT_ID_SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by dc_set_config().
pub async fn get_by_id(context: &Context, contact_id: u32) -> Result<Contact> {
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
let contact = Contact::load_from_db(context, contact_id).await?;
Ok(contact)
@@ -919,7 +978,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id as i32],
paramsv![self.param.to_string(), self.id],
)
.await?;
Ok(())
@@ -931,14 +990,14 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
paramsv![self.status, self.id as i32],
paramsv![self.status, self.id],
)
.await?;
Ok(())
}
/// Get the ID of the contact.
pub fn get_id(&self) -> u32 {
pub fn get_id(&self) -> ContactId {
self.id
}
@@ -1065,7 +1124,7 @@ impl Contact {
pub async fn addr_equals_contact(
context: &Context,
addr: &str,
contact_id: u32,
contact_id: ContactId,
) -> Result<bool> {
if addr.is_empty() {
return Ok(false);
@@ -1091,13 +1150,13 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
)
.await?;
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> Result<bool> {
pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Ok(false);
}
@@ -1106,7 +1165,7 @@ impl Contact {
.sql
.exists(
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
paramsv![contact_id],
)
.await?;
Ok(exists)
@@ -1114,14 +1173,14 @@ impl Contact {
pub async fn scaleup_origin_by_id(
context: &Context,
contact_id: u32,
contact_id: ContactId,
origin: Origin,
) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
paramsv![origin, contact_id, origin],
)
.await?;
Ok(())
@@ -1165,7 +1224,11 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
}
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
async fn set_block_contact(
context: &Context,
contact_id: ContactId,
new_blocking: bool,
) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can't block special contact {}",
@@ -1179,7 +1242,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
paramsv![i32::from(new_blocking), contact_id],
)
.await?;
@@ -1230,7 +1293,7 @@ WHERE type=? AND id IN (
/// this typically happens if we see message with our own profile image, sent from another device.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: u32,
contact_id: ContactId,
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
@@ -1277,7 +1340,7 @@ pub(crate) async fn set_profile_image(
/// between Delta Chat devices.
pub(crate) async fn set_status(
context: &Context,
contact_id: u32,
contact_id: ContactId,
status: String,
encrypted: bool,
has_chat_version: bool,
@@ -1303,7 +1366,7 @@ pub(crate) async fn set_status(
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
pub(crate) async fn update_last_seen(
context: &Context,
contact_id: u32,
contact_id: ContactId,
timestamp: i64,
) -> Result<()> {
ensure!(
@@ -1474,7 +1537,7 @@ mod tests {
Origin::IncomingReplyTo,
)
.await?;
assert_ne!(id, 0);
assert_ne!(id, ContactId::new(0));
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
@@ -1629,7 +1692,7 @@ mod tests {
// check SELF
let contact = Contact::load_from_db(&t, DC_CONTACT_ID_SELF).await.unwrap();
assert_eq!(DC_CONTACT_ID_SELF, 1);
assert_eq!(DC_CONTACT_ID_SELF, ContactId::new(1));
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());

View File

@@ -24,7 +24,6 @@ use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
#[derive(Clone, Debug)]
@@ -45,7 +44,6 @@ pub struct InnerContext {
/// Blob directory path
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) bob: Bob,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
@@ -171,7 +169,6 @@ impl Context {
blobdir,
running_state: RwLock::new(Default::default()),
sql: Sql::new(dbfile),
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
@@ -357,7 +354,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")
@@ -421,7 +418,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);
@@ -673,10 +670,10 @@ mod tests {
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::DC_CONTACT_ID_SELF;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::test_utils::TestContext;
use anyhow::Context as _;
use std::time::Duration;

File diff suppressed because it is too large Load Diff

View File

@@ -21,10 +21,10 @@ use mailparse::MailHeaderMap;
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::events::EventType;
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::provider::get_provider_update_timestamp;
use crate::stock_str;
@@ -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
@@ -534,8 +534,8 @@ impl rusqlite::types::ToSql for EmailAddress {
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: &str) -> String {
input
.replace("\n", " ")
.replace("\r", " ")
.replace('\n', " ")
.replace('\r', " ")
.trim()
.to_string()
}

View File

@@ -3,14 +3,14 @@
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::{job_try, stock_str, EventType};
@@ -146,7 +146,7 @@ impl Job {
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid)
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
@@ -185,6 +185,7 @@ impl Imap {
context: &Context,
folder: &str,
uid: u32,
rfc724_mid: String,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
@@ -196,8 +197,10 @@ impl Imap {
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (last_uid, _received) = match self
.fetch_many_msgs(context, folder, vec![uid], false, false)
.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, false)
.await
{
Ok(res) => res,
@@ -251,13 +254,15 @@ impl MimeMessage {
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
use crate::chat::send_msg;
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf_inner;
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::test_utils::TestContext;
use num_traits::FromPrimitive;
use super::*;
#[test]
fn test_downloadstate_values() {
@@ -337,7 +342,16 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain";
dc_receive_imf_inner(&t, header.as_bytes(), "INBOX", false, Some(100000), false).await?;
dc_receive_imf_inner(
&t,
"Mr.12345678901@example.com",
header.as_bytes(),
"INBOX",
false,
Some(100000),
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Available);
assert_eq!(msg.get_subject(), "foo");
@@ -348,6 +362,7 @@ mod tests {
dc_receive_imf_inner(
&t,
"Mr.12345678901@example.com",
format!("{}\n\n100k text...", header).as_bytes(),
"INBOX",
false,
@@ -377,6 +392,7 @@ mod tests {
// download message from bob partially, this must not change the ephemeral timer
dc_receive_imf_inner(
&t,
"first@example.org",
b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\

View File

@@ -400,15 +400,14 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::constants::Viewtype;
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
mod ensure_secret_key_exists {
use super::*;

View File

@@ -67,14 +67,16 @@ use serde::{Deserialize, Serialize};
use crate::chat::{send_msg, ChatId};
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
use crate::contact::ContactId;
use crate::context::Context;
use crate::dc_tools::time;
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::message::{Message, MessageState, MsgId};
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock_str;
use std::cmp::max;
@@ -212,7 +214,7 @@ impl ChatId {
pub(crate) async fn stock_ephemeral_timer_changed(
context: &Context,
timer: Timer,
from_id: u32,
from_id: ContactId,
) -> String {
match timer {
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
@@ -297,6 +299,37 @@ impl MsgId {
}
}
pub(crate) async fn start_ephemeral_timers_msgids(
context: &Context,
msg_ids: &[MsgId],
) -> Result<()> {
let msg_ids: Vec<&dyn crate::ToSql> = msg_ids
.iter()
.map(|msg_id| msg_id as &dyn crate::ToSql)
.collect();
let now = time();
let count = context
.sql
.execute(
format!(
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
AND id IN ({})",
sql::repeat_vars(msg_ids.len())?
),
rusqlite::params_from_iter(
std::iter::once(&now as &dyn crate::ToSql)
.chain(std::iter::once(&now as &dyn crate::ToSql))
.chain(msg_ids),
),
)
.await?;
if count > 0 {
schedule_ephemeral_task(context).await;
}
Ok(())
}
/// Deletes messages which are expired according to
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
@@ -451,12 +484,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],
)
@@ -824,8 +856,8 @@ mod tests {
// Check that if there is a message left, the text and metadata are gone
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, 0);
assert_eq!(msg.to_id, 0);
assert_eq!(msg.from_id, ContactId::new(0));
assert_eq!(msg.to_id, ContactId::new(0));
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql

View File

@@ -7,9 +7,9 @@ use async_std::path::PathBuf;
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateId;
#[derive(Debug)]
pub struct Events {
@@ -253,7 +253,7 @@ pub enum EventType {
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[strum(props(id = "2030"))]
ContactsChanged(Option<u32>),
ContactsChanged(Option<ContactId>),
/// Location of one or more contact has changed.
///
@@ -261,7 +261,7 @@ pub enum EventType {
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
#[strum(props(id = "2035"))]
LocationChanged(Option<u32>),
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().
#[strum(props(id = "2041"))]
@@ -305,7 +305,10 @@ pub enum EventType {
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
#[strum(props(id = "2060"))]
SecurejoinInviterProgress { contact_id: u32, progress: usize },
SecurejoinInviterProgress {
contact_id: ContactId,
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
@@ -316,7 +319,10 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
SecurejoinJoinerProgress {
contact_id: ContactId,
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
@@ -329,8 +335,5 @@ pub enum EventType {
SelfavatarChanged,
#[strum(props(id = "2120"))]
WebxdcStatusUpdate {
msg_id: MsgId,
status_update_id: StatusUpdateId,
},
WebxdcStatusUpdate(MsgId),
}

View File

@@ -279,9 +279,9 @@ mod tests {
use crate::chat;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::DC_CONTACT_ID_SELF;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::MessengerMessage;
use crate::message::{MessengerMessage, Viewtype};
use crate::test_utils::TestContext;
#[async_std::test]
@@ -365,7 +365,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// however, rust multiline-strings use just `\n`;
// therefore, we just remove `\r` before comparison.
assert_eq!(
parser.html.replace("\r", ""),
parser.html.replace('\r', ""),
r##"
<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
@@ -379,7 +379,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
</html>
@@ -394,7 +394,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
<p>
this is <b>html</b>

View File

@@ -9,7 +9,7 @@ use std::{
collections::{BTreeMap, BTreeSet},
};
use anyhow::{anyhow, bail, format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_imap::types::{
Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse,
};
@@ -17,30 +17,32 @@ use async_std::channel::Receiver;
use async_std::prelude::*;
use num_traits::FromPrimitive;
use crate::chat;
use crate::chat::ChatId;
use crate::chat::ChatIdBlocked;
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, ShowEmails, Viewtype, DC_CONTACT_ID_SELF, DC_FETCH_EXISTING_MSGS_COUNT,
Blocked, Chattype, ShowEmails, DC_CONTACT_ID_SELF, DC_FETCH_EXISTING_MSGS_COUNT,
DC_FOLDERS_CONFIGURED_VERSION, DC_LP_AUTH_OAUTH2,
};
use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf_inner, from_field_to_contact_id, get_prefetch_parent_message, ReceivedMsg,
};
use crate::dc_tools::dc_create_id;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{ServerAddress, Socks5Config};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
use crate::login_param::{
CertificateChecks, LoginParam, ServerAddress, ServerLoginParam, Socks5Config,
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::provider::Socket;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
use crate::{config::Config, scheduler::connectivity::ConnectivityStore};
mod client;
mod idle;
@@ -69,6 +71,7 @@ pub enum ImapActionResult {
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
@@ -458,15 +461,9 @@ impl Imap {
.await
.context("fetch_new_messages")?;
self.move_messages(context, watch_folder)
self.move_delete_messages(context, watch_folder)
.await
.context("move_messages")?;
self.delete_messages(context, watch_folder)
.await
.context("delete_messages")?;
self.sync_seen_flags(context, watch_folder)
.await
.context("sync_seen_flags")?;
.context("move_delete_messages")?;
Ok(())
}
@@ -500,9 +497,8 @@ impl Imap {
let msg = fetch?;
// Get Message-ID
let message_id = get_fetch_headers(&msg)
.and_then(|headers| prefetch_get_message_id(&headers))
.ok();
let message_id =
get_fetch_headers(&msg).map_or(None, |headers| prefetch_get_message_id(&headers));
if let (Some(uid), Some(rfc724_mid)) = (msg.uid, message_id) {
msg_ids.insert(uid, rfc724_mid);
@@ -667,6 +663,11 @@ impl Imap {
folder: &str,
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder).await? {
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
let new_emails = self.select_with_uidvalidity(context, folder).await?;
if !new_emails && !fetch_existing_msgs {
@@ -689,6 +690,7 @@ impl Imap {
let download_limit = context.download_limit().await?;
let mut uids_fetch_fully = Vec::with_capacity(msgs.len());
let mut uids_fetch_partially = Vec::with_capacity(msgs.len());
let mut uid_message_ids = BTreeMap::new();
let mut largest_uid_skipped = None;
// Store the info about IMAP messages in the database.
@@ -701,7 +703,8 @@ impl Imap {
}
};
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
// Get the Message-ID or generate a fake one to identify the message in the database.
let message_id = prefetch_get_message_id(&headers).unwrap_or_else(dc_create_id);
let target = match target_folder(context, folder, &headers).await? {
Some(config) => match context.get_config(config).await? {
@@ -711,19 +714,6 @@ impl Imap {
None => folder.to_string(),
};
let duplicate = context
.sql
.count(
"SELECT COUNT(*)
FROM imap
WHERE rfc724_mid=?
AND folder=?
AND uid<?",
paramsv![message_id, &target, uid],
)
.await?
> 0;
context
.sql
.execute(
@@ -732,13 +722,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
paramsv![
message_id,
folder,
uid,
uid_validity,
if duplicate { "" } else { &target }
],
paramsv![message_id, folder, uid, uid_validity, &target],
)
.await?;
@@ -749,6 +733,11 @@ impl Imap {
// message, move it to the movebox and then download the second message before
// downloading the first one, if downloading from inbox before moving is allowed.
if folder == target
// Never download messages directly from the spam folder.
// If the sender is known, the message will be moved to the Inbox or Mvbox
// and then we download the message from there.
// Also see `spam_target_folder()`.
&& !context.is_spam_folder(folder).await?
&& prefetch_should_download(
context,
&headers,
@@ -768,6 +757,7 @@ impl Imap {
}
None => uids_fetch_fully.push(uid),
}
uid_message_ids.insert(uid, message_id);
} else {
largest_uid_skipped = Some(uid);
}
@@ -783,6 +773,7 @@ impl Imap {
context,
folder,
uids_fetch_fully,
&uid_message_ids,
false,
fetch_existing_msgs,
)
@@ -793,6 +784,7 @@ impl Imap {
context,
folder,
uids_fetch_partially,
&uid_message_ids,
true,
fetch_existing_msgs,
)
@@ -825,18 +817,128 @@ impl Imap {
Ok(read_cnt > 0)
}
/// Moves messages.
/// Deletes batch of messages identified by their UID from the currently
/// selected folder.
async fn delete_message_batch(
&mut self,
context: &Context,
uid_set: &str,
row_ids: Vec<i64>,
) -> Result<()> {
// mark the message for deletion
self.add_flag_finalized_with_set(uid_set, "\\Deleted")
.await?;
context
.sql
.execute(
format!(
"DELETE FROM imap WHERE id IN ({})",
sql::repeat_vars(row_ids.len())?
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot remove deleted messages from imap table")?;
context.emit_event(EventType::ImapMessageDeleted(format!(
"IMAP messages {} marked as deleted",
uid_set
)));
self.config.selected_folder_needs_expunge = true;
Ok(())
}
/// Moves batch of messages identified by their UID from the currently
/// selected folder to the target folder.
async fn move_message_batch(
&mut self,
context: &Context,
set: &str,
row_ids: Vec<i64>,
target: &str,
) -> Result<()> {
if self.config.can_move {
let session = self
.session
.as_mut()
.context("no session while attempting to MOVE messages")?;
match session.uid_mv(set, &target).await {
Ok(()) => {
// Messages are moved or don't exist, IMAP returns OK response in both cases.
context
.sql
.execute(
format!(
"DELETE FROM imap WHERE id IN ({})",
sql::repeat_vars(row_ids.len())?
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot delete moved messages from imap table")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {} moved to {}",
set, target
)));
return Ok(());
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {} to {}: {}",
set,
target,
err
);
}
}
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
);
}
// Server does not support MOVE or MOVE failed.
// Copy the message to the destination folder and mark the record for deletion.
let session = self
.session
.as_mut()
.context("no session while attempting to COPY messages")?;
match session.uid_copy(&set, &target).await {
Ok(()) => {
context
.sql
.execute(
format!(
"UPDATE imap SET target='' WHERE id IN ({})",
sql::repeat_vars(row_ids.len())?
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot plan deletion of copied messages")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {} copied to {}",
set, target
)));
Ok(())
}
Err(err) => Err(err.into()),
}
}
/// Moves and deletes messages as planned in the `imap` table.
///
/// This is the only place where messages are moved on the IMAP server.
async fn move_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let rows = context
/// This is the only place where messages are moved or deleted on the IMAP server.
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let mut rows = context
.sql
.query_map(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND target != folder
AND target != '' -- Not planned for deletion.
ORDER BY id",
ORDER BY target, uid",
paramsv![folder],
|row| {
let rowid: i64 = row.get(0)?;
@@ -846,147 +948,62 @@ impl Imap {
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
.await?
.into_iter()
.peekable();
self.prepare(context).await?;
self.select_folder(context, Some(folder)).await?;
for (rowid, uid, target) in rows {
// TODO: batch moves of messages with the same destination.
let set = uid.to_string();
while let Some((_, _, target)) = rows.peek().cloned() {
// Construct next request for the target folder.
let mut uid_set = String::new();
let mut rowid_set = Vec::new();
if self.config.can_move {
if let Some(session) = &mut self.session {
match session.uid_mv(&set, &target).await {
Ok(_) => {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP message {}/{} moved to {}",
folder, uid, target
)));
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(async_imap::error::Error::No(text)) => {
// "NO" response, probably the message is moved already.
info!(
context,
"IMAP message {}/{} cannot be moved: {}", folder, uid, text
);
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
target,
err
);
}
while uid_set.len() < 1000 {
// Construct a new range.
if let Some((start_rowid, start_uid, _)) =
rows.next_if(|(_, _, start_target)| start_target == &target)
{
rowid_set.push(start_rowid);
let mut end_uid = start_uid;
while let Some((next_rowid, next_uid, _)) =
rows.next_if(|(_, next_uid, next_target)| {
next_target == &target && *next_uid == end_uid + 1
})
{
end_uid = next_uid;
rowid_set.push(next_rowid);
}
let uid_range = UidRange {
start: start_uid,
end: end_uid,
};
if !uid_set.is_empty() {
uid_set.push(',');
}
uid_set.push_str(&uid_range.to_string());
} else {
bail!("No session while attempting to move the message");
}
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
folder,
uid,
target
);
}
// Server does not support MOVE or MOVE failed.
// Copy the message to the destination folder and mark the record for deletion.
if let Some(session) = &mut self.session {
match session.uid_copy(&set, &target).await {
Ok(_) => {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP message {}/{} copied to {}",
folder, uid, target
)));
// Plan deletion of the original message.
context
.sql
.execute("UPDATE imap SET target='' WHERE id=?", paramsv![rowid])
.await?;
}
Err(async_imap::error::Error::No(text)) => {
// "NO" response, probably the message is moved already.
info!(
context,
"IMAP message {}/{} cannot be copied: {}", folder, uid, text
);
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(err) => {
warn!(
context,
"Could not copy message {}/{}: {}", folder, uid, err
);
// Break the loop to avoid moving messages out of order.
// We can't proceed until this message is moved or copied.
break;
}
}
} else {
bail!("No session while attempting to copy the message");
}
}
Ok(())
}
/// Deletes messages that are marked as planned for deletion in `imap` table.
///
/// This is the only place where messages are deleted from the IMAP server.
async fn delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let rows = context
.sql
.query_map(
"SELECT id, uid FROM imap
WHERE folder=? AND target=''
ORDER BY uid ASC
LIMIT 50", // Do not try to delete too many messages at once.
paramsv![folder],
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
Ok((rowid, uid))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
if rows.is_empty() {
return Ok(());
}
for (rowid, uid) in rows {
match self.delete_msg(context, folder, uid).await {
ImapActionResult::Failed | ImapActionResult::RetryLater => {
warn!(context, "Deletion of message {}/{} failed", folder, uid);
break;
}
ImapActionResult::Success => {
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
}
}
// Empty target folder name means messages should be deleted.
if target.is_empty() {
self.delete_message_batch(context, &uid_set, rowid_set)
.await
.with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
} else {
self.move_message_batch(context, &uid_set, rowid_set, &target)
.await
.with_context(|| {
format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?;
}
}
@@ -1023,33 +1040,23 @@ impl Imap {
.as_ref()
.with_context(|| format!("No mailbox selected, folder: {}", folder))?;
let remote_highest_modseq = if let Some(remote_highest_modseq) = mailbox.highest_modseq {
remote_highest_modseq
} else {
// Check if the mailbox supports MODSEQ.
// We are not interested in actual value of HIGHESTMODSEQ.
if mailbox.highest_modseq.is_none() {
info!(
context,
"Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
);
return Ok(());
};
let mut highest_modseq = get_modseq(context, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {}", folder))?;
if highest_modseq >= remote_highest_modseq {
info!(
context,
"MODSEQ {} is already new, HIGHESTMODSEQ={}, skipping seen flag update",
highest_modseq,
remote_highest_modseq
);
return Ok(());
}
let mut updated_chat_ids = BTreeSet::new();
let uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get UID validity for folder {}", folder))?;
let mut highest_modseq = get_modseq(context, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {}", folder))?;
let mut list = session
.uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {})", highest_modseq))
.await
@@ -1084,10 +1091,6 @@ impl Imap {
}
}
if remote_highest_modseq > highest_modseq {
// We haven't seen the message with the highest MODSEQ, maybe it was deleted already.
highest_modseq = remote_highest_modseq;
}
set_modseq(context, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {}", folder))?;
@@ -1217,6 +1220,7 @@ impl Imap {
context: &Context,
folder: &str,
server_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
fetching_existing_messages: bool,
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
@@ -1295,11 +1299,23 @@ impl Imap {
let folder = folder.clone();
// safe, as we checked above that there is a body.
let body = body.unwrap();
let body = body
.context("we checked that message has body right above, but it has vanished")?;
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
let rfc724_mid = if let Some(rfc724_mid) = &uid_message_ids.get(&server_uid) {
rfc724_mid
} else {
warn!(
context,
"No Message-ID corresponding to UID {} passed in uid_messsage_ids",
server_uid
);
""
};
match dc_receive_imf_inner(
&context,
rfc724_mid,
body,
&folder,
is_seen,
@@ -1350,7 +1366,7 @@ impl Imap {
bail!("Can't set flag, should reconnect");
}
let session = self.session.as_mut().context("No session").unwrap();
let session = self.session.as_mut().context("No session")?;
let query = format!("+FLAGS ({})", flag);
let mut responses = session
.uid_store(uid_set, &query)
@@ -1428,39 +1444,6 @@ impl Imap {
}
}
pub async fn delete_msg(
&mut self,
context: &Context,
folder: &str,
uid: u32,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
.await
{
return imapresult;
}
// we are connected, and the folder is selected
let display_imap_id = format!("{}/{}", folder, uid);
// mark the message for deletion
if let Err(err) = self.add_flag_finalized(uid, "\\Deleted").await {
warn!(
context,
"Cannot mark message {} as \"Deleted\": {}.", display_imap_id, err
);
ImapActionResult::RetryLater
} else {
context.emit_event(EventType::ImapMessageDeleted(format!(
"IMAP Message {} marked as deleted",
display_imap_id
)));
self.config.selected_folder_needs_expunge = true;
ImapActionResult::Success
}
}
pub async fn ensure_configured_folders(
&mut self,
context: &Context,
@@ -1584,23 +1567,6 @@ impl Imap {
Ok(())
}
/// Update HIGHESTMODSEQ on selected mailbox.
///
/// Should be called when MODSEQ is seen on the response, such as IDLE response.
pub(crate) fn update_modseq(&mut self, modseq: u64) {
self.config.selected_mailbox =
self.config
.selected_mailbox
.as_ref()
.map(|mailbox| Mailbox {
highest_modseq: Some(std::cmp::max(
mailbox.highest_modseq.unwrap_or_default(),
modseq,
)),
..mailbox.clone()
});
}
/// Return whether the server sent an unsolicited EXISTS response.
/// Drains all responses from `session.unsolicited_responses` in the process.
/// If this returns `true`, this means that new emails arrived and you should
@@ -1627,29 +1593,38 @@ impl Imap {
self.config.can_check_quota
}
pub async fn get_quota_roots(
pub(crate) async fn get_quota_roots(
&mut self,
mailbox_name: &str,
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
if let Some(session) = self.session.as_mut() {
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
} else {
Err(anyhow!("Not connected to IMAP, no session"))
}
let session = self.session.as_mut().context("no session")?;
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
}
}
/// Returns target folder for a message found in the Spam folder.
async fn spam_target_folder(
async fn should_move_out_of_spam(
context: &Context,
folder: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> {
if let Some(chat) = prefetch_get_chat(context, headers).await? {
if chat.blocked != Blocked::Not {
) -> Result<bool> {
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
// If this is a chat message (i.e. has a ChatVersion header), then this might be
// a securejoin message. We can't find out at this point as we didn't prefetch
// the SecureJoin header. So, we always move chat messages out of Spam.
// Two possibilities to change this would be:
// 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
// `fetch_new_messages()`, and then let `dc_receive_imf()` check
// if it's a spam message and should be hidden.
// 2. Or add a flag to the ChatVersion header that this is a securejoin
// request, and return `true` here only if the message has this flag.
// `dc_receive_imf()` can then check if the securejoin request is valid.
return Ok(true);
}
if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
if msg.chat_blocked != Blocked::Not {
// Blocked or contact request message in the spam folder, leave it there.
return Ok(None);
return Ok(false);
}
} else {
// No chat found.
@@ -1657,23 +1632,40 @@ async fn spam_target_folder(
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
if blocked_contact {
// Contact is blocked, leave the message in spam.
return Ok(None);
return Ok(false);
}
if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
if chat_id_blocked.blocked != Blocked::Not {
return Ok(None);
return Ok(false);
}
} else if from_id != DC_CONTACT_ID_SELF {
// No chat with this contact found.
return Ok(None);
return Ok(false);
}
}
if needs_move_to_mvbox(context, headers).await? {
Ok(true)
}
/// Returns target folder for a message found in the Spam folder.
/// If this returns None, the message will not be moved out of the
/// Spam folder, and as `fetch_new_messages()` doesn't download
/// messages from the Spam folder, the message will be ignored.
async fn spam_target_folder(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> {
if !should_move_out_of_spam(context, headers).await? {
return Ok(None);
}
if needs_move_to_mvbox(context, headers).await?
// If OnlyFetchMvbox is set, we don't want to move the message to
// the inbox or sentbox where we wouldn't fetch it again:
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
} else if needs_move_to_sentbox(context, folder, headers).await? {
Ok(Some(Config::ConfiguredSentboxFolder))
} else {
Ok(Some(Config::ConfiguredInboxFolder))
}
@@ -1691,11 +1683,9 @@ pub async fn target_folder(
}
if context.is_spam_folder(folder).await? {
spam_target_folder(context, folder, headers).await
spam_target_folder(context, headers).await
} else if needs_move_to_mvbox(context, headers).await? {
Ok(Some(Config::ConfiguredMvboxFolder))
} else if needs_move_to_sentbox(context, folder, headers).await? {
Ok(Some(Config::ConfiguredSentboxFolder))
} else {
Ok(None)
}
@@ -1730,44 +1720,6 @@ async fn needs_move_to_mvbox(
}
}
async fn prefetch_is_outgoing(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let from_address_list = &mimeparser::get_from(headers);
// Only looking at the first address in the `From:` field.
if let Some(info) = from_address_list.first() {
if context.is_self_addr(&info.addr).await? {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
async fn needs_move_to_sentbox(
context: &Context,
folder: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let needs_move = context.get_config_bool(Config::SentboxMove).await?
&& context
.get_config(Config::ConfiguredSentboxFolder)
.await?
.is_some()
&& context.is_inbox(folder).await?
&& headers.get_header_value(HeaderDef::ChatVersion).is_some()
&& headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.is_none()
&& prefetch_is_outgoing(context, headers).await?;
Ok(needs_move)
}
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
// TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
// however, if we fail to find out the sent-folder,
@@ -1889,13 +1841,13 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
}
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::XMicrosoftOriginalMessageId) {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
crate::mimeparser::parse_message_id(&message_id).ok()
} else if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
crate::mimeparser::parse_message_id(&message_id).ok()
} else {
bail!("prefetch: No message ID found");
None
}
}
@@ -2005,7 +1957,7 @@ async fn mark_seen_by_uid(
.sql
.query_row_optional(
"SELECT id, chat_id FROM msgs
WHERE rfc724_mid IN (
WHERE id > 9 AND rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
@@ -2045,6 +1997,12 @@ async fn mark_seen_by_uid(
> 0;
if updated {
msg_id
.start_ephemeral_timer(context)
.await
.with_context(|| {
format!("failed to start ephemeral timer for message {}", msg_id)
})?;
Ok(Some(chat_id))
} else {
// Message state has not chnaged.
@@ -2152,6 +2110,21 @@ pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result
}
}
/// Whether to ignore fetching messages from a folder.
///
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
/// not explicitly watched should not be fetched.
async fn should_ignore_folder(context: &Context, folder: &str) -> Result<bool> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false);
}
if context.is_sentbox(folder).await? {
// Still respect the SentboxWatch setting.
return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
}
Ok(!(context.is_mvbox(folder).await? || context.is_spam_folder(folder).await?))
}
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
@@ -2320,7 +2293,6 @@ mod tests {
accepted_chat: bool,
outgoing: bool,
setupmessage: bool,
sentbox_move: bool,
) -> Result<()> {
println!("Testing: For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}",
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage);
@@ -2339,9 +2311,6 @@ mod tests {
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
.await?;
t.ctx.set_config(Config::ShowEmails, Some("2")).await?;
t.ctx
.set_config_bool(Config::SentboxMove, sentbox_move)
.await?;
if accepted_chat {
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
@@ -2406,7 +2375,7 @@ mod tests {
("Spam", true, true, "DeltaChat"),
];
// These are the same as above, but all messages in Spam stay in Spam
// These are the same as above, but non-chat messages in Spam stay in Spam
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
@@ -2417,9 +2386,9 @@ mod tests {
("Sent", true, false, "Sent"),
("Sent", true, true, "DeltaChat"),
("Spam", false, false, "Spam"),
("Spam", false, true, "Spam"),
("Spam", false, true, "INBOX"),
("Spam", true, false, "Spam"),
("Spam", true, true, "Spam"),
("Spam", true, true, "DeltaChat"),
];
#[async_std::test]
@@ -2433,7 +2402,6 @@ mod tests {
true,
false,
false,
false,
)
.await?;
}
@@ -2451,7 +2419,6 @@ mod tests {
false,
false,
false,
false,
)
.await?;
}
@@ -2460,26 +2427,18 @@ mod tests {
#[async_std::test]
async fn test_target_folder_outgoing() -> Result<()> {
for sentbox_move in &[true, false] {
// Test outgoing emails
for (folder, mvbox_move, chat_msg, mut expected_destination) in
COMBINATIONS_ACCEPTED_CHAT
{
if *folder == "INBOX" && !mvbox_move && *chat_msg && *sentbox_move {
expected_destination = "Sent"
}
check_target_folder_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
true,
true,
false,
*sentbox_move,
)
.await?;
}
// Test outgoing emails
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_target_folder_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
true,
true,
false,
)
.await?;
}
Ok(())
}
@@ -2496,7 +2455,6 @@ mod tests {
false,
true,
true,
false,
)
.await?;
}

View File

@@ -3,7 +3,6 @@ use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use imap_proto::types::{AttributeValue, Response};
use std::time::{Duration, SystemTime};
use crate::{context::Context, scheduler::InterruptInfo};
@@ -72,13 +71,6 @@ impl Imap {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
if let Response::Fetch(_message, attrs) = x.parsed() {
for attr in attrs {
if let AttributeValue::ModSeq(modseq) = attr {
self.update_modseq(*modseq);
}
}
}
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
@@ -165,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

@@ -2,15 +2,18 @@ use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::imap::Imap;
use crate::{config::Config, log::LogExt};
use crate::log::LogExt;
use crate::{context::Context, imap::FolderMeaning};
use async_std::prelude::*;
use async_std::stream::StreamExt;
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,28 +23,18 @@ impl Imap {
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
return Ok(false);
}
}
info!(context, "Starting full folder scan");
self.prepare(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let folders = self.list_folders(context).await?;
let watched_folders = get_watched_folders(context).await?;
let mut folder_configs = BTreeMap::new();
for folder in folders {
let folder = match folder {
Ok(f) => f,
Err(e) => {
warn!(context, "Can't get folder: {}", e);
continue;
}
};
// Gmail labels are not folders and should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is
// received. The code used to wrongly conclude that the email had
@@ -98,24 +91,40 @@ impl Imap {
}
last_scan.replace(Instant::now());
Ok(())
Ok(true)
}
/// Returns the names of all folders on the IMAP server.
pub async fn list_folders(
self: &mut Imap,
context: &Context,
) -> Result<Vec<async_imap::types::Name>> {
let session = self.session.as_mut();
let session = session.context("No IMAP connection")?;
let list = session
.list(Some(""), Some("*"))
.await?
.filter_map(|f| f.ok_or_log_msg(context, "list_folders() can't get folder"));
Ok(list.collect().await)
}
}
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

@@ -16,7 +16,7 @@ use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::DC_CONTACT_ID_SELF;
use crate::context::Context;
use crate::dc_tools::{
dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
@@ -26,7 +26,7 @@ use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
use crate::message::{Message, MsgId};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::pgp;
@@ -165,7 +165,6 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
}
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let mut msg: Message;
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
let setup_file_content = render_setup_file(context, &setup_code).await?;
@@ -178,8 +177,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
.await?;
let chat_id = ChatId::create_for_contact(context, DC_CONTACT_ID_SELF).await?;
msg = Message::default();
msg.viewtype = Viewtype::File;
let mut msg = Message {
viewtype: Viewtype::File,
..Default::default()
};
msg.param.set(Param::File, setup_file_blob.as_name());
msg.subject = stock_str::ac_setup_msg_subject(context).await;
msg.param
@@ -236,7 +237,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
Ok(format!(
concat!(
"<!DOCTYPE html>\r\n",
@@ -440,13 +441,6 @@ async fn import_backup(
backup_to_import: &Path,
passphrase: String,
) -> 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."
@@ -458,6 +452,16 @@ async fn import_backup(
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
info!(
context,
"Import \"{}\" ({} bytes) to \"{}\".",
backup_to_import.display(),
file_size,
context.get_dbfile().display()
);
context.sql.config_cache.write().await.clear();
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
@@ -899,6 +903,54 @@ mod tests {
}
}
#[async_std::test]
async fn test_export_and_import_backup() -> Result<()> {
let backup_dir = tempfile::tempdir().unwrap();
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path().as_ref())
.await
.is_err());
// export from context1
assert!(imex(
&context1,
ImexMode::ExportBackup,
backup_dir.path().as_ref(),
None,
)
.await
.is_ok());
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path().as_ref()).await?;
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
Ok(())
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

View File

@@ -9,7 +9,7 @@ use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::config::Config;
use crate::contact::{normalize_name, Contact, Modifier, Origin};
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
@@ -197,7 +197,7 @@ impl Job {
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
self.desired_timestamp,
self.tries as i64,
i64::from(self.tries),
self.param.to_string(),
self.job_id as i32,
],
@@ -224,7 +224,7 @@ impl Job {
async fn get_additional_mdn_jobs(
&self,
context: &Context,
contact_id: u32,
contact_id: ContactId,
) -> Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context
@@ -271,7 +271,7 @@ impl Job {
return Status::Finished(Err(format_err!("MDNs are disabled")));
}
let contact_id = self.foreign_id;
let contact_id = ContactId::new(self.foreign_id);
let contact = job_try!(Contact::load_from_db(context, contact_id).await);
if contact.is_blocked() {
return Status::Finished(Err(format_err!("Contact is blocked")));
@@ -365,29 +365,38 @@ impl Job {
Status::Finished(Ok(()))
}
/// Synchronizes UIDs for sentbox, inbox and mvbox.
/// Synchronizes UIDs for all folders.
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);
return Status::RetryLater;
}
let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
if let Some(sentbox_folder) = sentbox_folder {
job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
let all_folders = match imap.list_folders(context).await {
Ok(v) => v,
Err(e) => {
warn!(context, "Listing folders for resync failed: {:#}", e);
return Status::RetryLater;
}
};
let mut any_failed = false;
for folder in all_folders {
if let Err(e) = imap
.resync_folder_uids(context, folder.name().to_string())
.await
{
warn!(context, "{:#}", e);
any_failed = true;
}
}
let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
if let Some(inbox_folder) = inbox_folder {
job_try!(imap.resync_folder_uids(context, inbox_folder).await);
if any_failed {
Status::RetryLater
} else {
Status::Finished(Ok(()))
}
let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
if let Some(mvbox_folder) = mvbox_folder {
job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
}
Status::Finished(Ok(()))
}
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
@@ -463,9 +472,12 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
/// Remove jobs with specified IDs.
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
if job_ids.is_empty() {
return Ok(());
}
let q = format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
sql::repeat_vars(job_ids.len())?
);
context
.sql
@@ -676,7 +688,7 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
if seconds < 1 {
seconds = 1;
}
seconds as i64
i64::from(seconds)
}
}
}
@@ -685,7 +697,11 @@ async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
let mut param = Params::new();
param.set(Param::MsgId, msg.id.to_u32().to_string());
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await?;
add(
context,
Job::new(Action::SendMdn, msg.from_id.to_u32(), param, 0),
)
.await?;
Ok(())
}

View File

@@ -297,8 +297,8 @@ pub async fn store_self_keypair(
.context("failed to clear default")?;
}
let is_default = match default {
KeyPairUse::Default => true as i32,
KeyPairUse::ReadOnly => false as i32,
KeyPairUse::Default => i32::from(true),
KeyPairUse::ReadOnly => i32::from(false),
};
let addr = keypair.addr.to_string();
@@ -318,7 +318,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Clone, Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {

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,

View File

@@ -7,12 +7,13 @@ use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::DC_CONTACT_ID_SELF;
use crate::contact::ContactId;
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::job::{self, Job};
use crate::message::{Message, MsgId};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Params;
use crate::stock_str;
@@ -25,7 +26,7 @@ pub struct Location {
pub longitude: f64,
pub accuracy: f64,
pub timestamp: i64,
pub contact_id: u32,
pub contact_id: ContactId,
pub msg_id: u32,
pub chat_id: ChatId,
pub marker: Option<String>,
@@ -101,10 +102,10 @@ impl Kml {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let val = val
.replace("\n", "")
.replace("\r", "")
.replace("\t", "")
.replace(" ", "");
.replace('\n', "")
.replace('\r', "")
.replace('\t', "")
.replace(' ', "");
if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 {
// YYYY-MM-DDTHH:MM:SSZ
@@ -558,7 +559,7 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
pub(crate) async fn save(
context: &Context,
chat_id: ChatId,
contact_id: u32,
contact_id: ContactId,
locations: &[Location],
independent: bool,
) -> Result<Option<u32>> {
@@ -585,12 +586,12 @@ pub(crate) async fn save(
conn.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id])?;
if independent || !exists {
stmt_insert.execute(paramsv![
timestamp,
contact_id as i32,
contact_id,
chat_id,
latitude,
longitude,

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,7 +1,6 @@
//! # Messages and their identifiers.
use std::collections::BTreeSet;
use std::convert::TryInto;
use anyhow::{ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
@@ -11,17 +10,17 @@ use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::constants::{
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO, DC_CONTACT_ID_SELF,
DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{Contact, Origin};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::dc_tools::{
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
};
use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::job::{self, Action};
use crate::log::LogExt;
@@ -29,6 +28,7 @@ use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
use crate::summary::Summary;
@@ -183,7 +183,7 @@ impl rusqlite::types::ToSql for MsgId {
format_err!("Invalid MsgId {}", self.0).into(),
));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -194,7 +194,7 @@ impl rusqlite::types::FromSql for MsgId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Would be nice if we could use match here, but alas.
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= std::u32::MAX as i64 {
if 0 <= val && val <= i64::from(std::u32::MAX) {
Ok(MsgId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))
@@ -240,8 +240,8 @@ impl Default for MessengerMessage {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Message {
pub(crate) id: MsgId,
pub(crate) from_id: u32,
pub(crate) to_id: u32,
pub(crate) from_id: ContactId,
pub(crate) to_id: ContactId,
pub(crate) chat_id: ChatId,
pub(crate) viewtype: Viewtype,
pub(crate) state: MessageState,
@@ -387,7 +387,7 @@ impl Message {
}
pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
if chat::msgtype_has_file(self.viewtype) {
if self.viewtype.has_file() {
let file_param = self.param.get_path(Param::File, context)?;
if let Some(path_and_filename) = file_param {
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
@@ -456,7 +456,7 @@ impl Message {
self.id
}
pub fn get_from_id(&self) -> u32 {
pub fn get_from_id(&self) -> ContactId {
self.from_id
}
@@ -620,7 +620,7 @@ impl Message {
/// copied to the blobdir. Thus those attachments need to be
/// created immediately in the blobdir with a valid filename.
pub fn is_increation(&self) -> bool {
chat::msgtype_has_file(self.viewtype) && self.state == MessageState::OutPreparing
self.viewtype.has_file() && self.state == MessageState::OutPreparing
}
pub fn is_setupmessage(&self) -> bool {
@@ -1049,7 +1049,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
|row| {
let contact_id: i32 = row.get(0)?;
let contact_id: ContactId = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
@@ -1061,7 +1061,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let fts = dc_timestamp_to_str(ts);
ret += &format!("Read: {}", fts);
let name = Contact::load_from_db(context, contact_id.try_into()?)
let name = Contact::load_from_db(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
@@ -1237,7 +1237,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for msg_id in msg_ids.iter() {
let msg = Message::load_from_db(context, *msg_id).await?;
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await;
delete_poi_location(context, msg.location_id).await?;
}
msg_id
.trash(context)
@@ -1270,15 +1270,15 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
Ok(())
}
async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
context
.sql
.execute(
"DELETE FROM locations WHERE independent = 1 AND id=?;",
paramsv![location_id as i32],
)
.await
.is_ok()
.await?;
Ok(())
}
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
@@ -1286,50 +1286,52 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
return Ok(());
}
let conn = context.sql.get_conn().await?;
let msgs = async_std::task::spawn_blocking(move || -> Result<_> {
let mut stmt = conn.prepare_cached(concat!(
"SELECT",
" m.chat_id AS chat_id,",
" m.state AS state,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND m.chat_id>9"
))?;
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids.into_iter() {
let query_res = stmt.query_row(paramsv![id], |row| {
let msgs = context
.sql
.query_map(
format!(
"SELECT
m.id AS id,
m.chat_id AS chat_id,
m.state AS state,
m.ephemeral_timer AS ephemeral_timer,
c.blocked AS blocked
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE m.id IN ({}) AND m.chat_id>9",
sql::repeat_vars(msg_ids.len())?
),
rusqlite::params_from_iter(&msg_ids),
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
Ok((
row.get::<_, ChatId>("chat_id")?,
row.get::<_, MessageState>("state")?,
row.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
id,
chat_id,
state,
blocked.unwrap_or_default(),
ephemeral_timer,
))
});
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
continue;
}
let (chat_id, state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
msgs.push((id, chat_id, state, blocked));
}
drop(stmt);
drop(conn);
Ok(msgs)
})
.await?;
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
if msgs
.iter()
.any(|(_id, _chat_id, _state, _blocked, ephemeral_timer)| {
*ephemeral_timer != EphemeralTimer::Disabled
})
{
start_ephemeral_timers_msgids(context, &msg_ids)
.await
.context("failed to start ephemeral timers")?;
}
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 {
error!(
context,
"Failed to start ephemeral timer for message {}: {}", id, err
);
continue;
}
for (id, curr_chat_id, curr_state, curr_blocked, _curr_ephemeral_timer) in msgs.into_iter() {
if curr_blocked == Blocked::Not
&& (curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed)
{
@@ -1426,7 +1428,7 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl
/// returns Some if an event should be send
pub async fn handle_mdn(
context: &Context,
from_id: u32,
from_id: ContactId,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Result<Option<(ChatId, MsgId)>> {
@@ -1479,7 +1481,7 @@ pub async fn handle_mdn(
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id as i32,],
paramsv![msg_id, from_id],
)
.await?
{
@@ -1487,7 +1489,7 @@ pub async fn handle_mdn(
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
paramsv![msg_id, from_id as i32, timestamp_sent],
paramsv![msg_id, from_id, timestamp_sent],
)
.await?;
}
@@ -1701,9 +1703,106 @@ pub(crate) async fn rfc724_mid_exists(
Ok(res)
}
/// How a message is primarily displayed.
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
/// Text message.
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().
Text = 10,
/// Image message.
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
Image = 20,
/// Animated GIF message.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
Gif = 21,
/// Message containing a sticker, similar to image.
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
/// Message containing an Audio file.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
Audio = 40,
/// A voice message that was directly recorded by the user.
/// For all other audio messages, the type #DC_MSG_AUDIO should be used.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
Voice = 41,
/// Video messages.
/// File, width, height and durarion
/// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
/// and retrieved via
/// dc_msg_get_file(), dc_msg_get_width(),
/// dc_msg_get_height(), dc_msg_get_duration().
Video = 50,
/// Message containing any file, eg. a PDF.
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an webxdc instance.
Webxdc = 80,
}
impl Default for Viewtype {
fn default() -> Self {
Viewtype::Unknown
}
}
impl Viewtype {
/// Whether a message with this [`Viewtype`] should have a file attachment.
pub fn has_file(&self) -> bool {
match self {
Viewtype::Unknown => false,
Viewtype::Text => false,
Viewtype::Image => true,
Viewtype::Gif => true,
Viewtype::Sticker => true,
Viewtype::Audio => true,
Viewtype::Voice => true,
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Webxdc => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
use crate::chat::{marknoticed_chat, ChatItem};
use crate::chatlist::Chatlist;
use crate::constants::DC_CONTACT_ID_DEVICE;
@@ -1711,6 +1810,8 @@ mod tests {
use crate::test_utils as test;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_guess_msgtype_from_suffix() {
assert_eq!(
@@ -2155,4 +2256,29 @@ mod tests {
Ok(())
}
#[test]
fn test_viewtype_derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
#[test]
fn test_viewtype_values() {
// values may be written to disk and must not change
assert_eq!(Viewtype::Unknown, Viewtype::default());
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
}
}

View File

@@ -7,9 +7,9 @@ use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::chat::Chat;
use crate::config::Config;
use crate::constants::{Chattype, Viewtype, DC_FROM_HANDSHAKE};
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
use crate::context::{get_version_str, Context};
use crate::dc_tools::IsNoneOrEmpty;
@@ -22,7 +22,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
use crate::format_flowed::{format_flowed, format_flowed_quote};
use crate::html::new_html_mimepart;
use crate::location;
use crate::message::{self, Message, MsgId};
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
@@ -503,33 +503,80 @@ impl<'a> MimeFactory<'a> {
}
}
// Start with Internet Message Format headers in the order of the standard example
// <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
headers
.unprotected
.push(Header::new("MIME-Version".into(), "1.0".into()));
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers
.unprotected
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
let subject_str = self.subject_str(context).await?;
let encoded_subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str.clone()
} else {
encode_words(&subject_str)
};
headers
.protected
.push(Header::new("Subject".into(), encoded_subject));
let date = chrono::Utc
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
// and when downloading messages we look for this header in order to correctly identify
// messages.
// Amazon's servers do not add such a header, so we just add it ourselves.
if let Some(server) = context.get_config(Config::ConfiguredSendServer).await? {
if server.ends_with(".amazonaws.com") {
headers.unprotected.push(Header::new(
"X-Microsoft-Original-Message-ID".into(),
rfc724_mid_headervalue.clone(),
))
}
}
headers
.unprotected
.push(Header::new("Message-ID".into(), rfc724_mid_headervalue));
// Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
if !self.in_reply_to.is_empty() {
headers
.unprotected
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
}
if !self.references.is_empty() {
headers
.unprotected
.push(Header::new("References".into(), self.references.clone()));
}
if !self.in_reply_to.is_empty() {
headers
.unprotected
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
}
let date = chrono::Utc
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
headers
.unprotected
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
if let Loaded::Mdn { .. } = self.loaded {
headers.unprotected.push(Header::new(
"Auto-Submitted".to_string(),
@@ -542,6 +589,11 @@ impl<'a> MimeFactory<'a> {
));
}
// Non-standard headers.
headers
.unprotected
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
if self.req_mdn {
// we use "Chat-Disposition-Notification-To"
// because replies to "Disposition-Notification-To" are weird in many cases
@@ -556,21 +608,9 @@ impl<'a> MimeFactory<'a> {
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let skip_autocrypt = self.should_skip_autocrypt();
let subject_str = self.subject_str(context).await?;
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(context).await?;
let encoded_subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str.clone()
} else {
encode_words(&subject_str)
};
if !skip_autocrypt {
// unless determined otherwise we add the Autocrypt header
let aheader = encrypt_helper.get_aheader().to_string();
@@ -579,15 +619,6 @@ impl<'a> MimeFactory<'a> {
.push(Header::new("Autocrypt".into(), aheader));
}
headers
.protected
.push(Header::new("Subject".into(), encoded_subject));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
headers.protected.push(Header::new(
@@ -596,25 +627,11 @@ impl<'a> MimeFactory<'a> {
));
}
headers.unprotected.push(Header::new(
"Message-ID".into(),
render_rfc724_mid(&rfc724_mid),
));
// MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
// Content-Type
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers
.unprotected
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
.push(Header::new("MIME-Version".into(), "1.0".into()));
let mut is_gossiped = false;
@@ -1123,7 +1140,7 @@ impl<'a> MimeFactory<'a> {
}
// add attachment part
if chat::msgtype_has_file(self.msg.viewtype) {
if self.msg.viewtype.has_file() {
if !is_file_size_okay(context, self.msg).await? {
bail!(
"Message exceeds the recommended {} MB.",
@@ -1435,12 +1452,13 @@ fn maybe_encode_words(words: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use async_std::fs::File;
use async_std::prelude::*;
use mailparse::{addrparse_header, MailHeaderMap};
use crate::chat::ChatId;
use crate::chat::{
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
ProtectionStatus,
};
use crate::chatlist::Chatlist;
@@ -1449,9 +1467,7 @@ mod tests {
use crate::mimeparser::MimeMessage;
use crate::test_utils::{get_chat_msg, TestContext};
use async_std::fs::File;
use mailparse::{addrparse_header, MailHeaderMap};
use super::*;
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -2084,4 +2100,27 @@ mod tests {
Ok(())
}
/// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header.
#[async_std::test]
async fn test_from_before_autocrypt() -> Result<()> {
// create chat with bob
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let payload = sent_msg.payload();
assert_eq!(payload.match_indices("Autocrypt:").count(), 1);
assert_eq!(payload.match_indices("From:").count(), 1);
assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next());
Ok(())
}
}

View File

@@ -2,6 +2,7 @@
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::io::Cursor;
use std::pin::Pin;
use anyhow::{bail, Result};
@@ -12,8 +13,8 @@ use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::addr_normalize;
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::{addr_normalize, ContactId};
use crate::context::Context;
use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers};
use crate::dehtml::dehtml;
@@ -23,7 +24,7 @@ use crate::format_flowed::unformat_flowed;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::Fingerprint;
use crate::location;
use crate::message;
use crate::message::{self, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::simplify;
@@ -368,6 +369,16 @@ impl MimeMessage {
} else if value == "protection-disabled" {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
}
} else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
self.is_system_message = SystemMessage::MemberRemovedFromGroup;
} else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
self.is_system_message = SystemMessage::MemberAddedToGroup;
} else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
self.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
self.is_system_message = SystemMessage::GroupImageChanged;
}
}
}
@@ -1020,8 +1031,9 @@ impl MimeMessage {
if decoded_data.is_empty() {
return;
}
let reader = Cursor::new(decoded_data);
let msg_type = if context
.is_webxdc_file(filename, decoded_data)
.is_webxdc_file(filename, reader)
.await
.unwrap_or(false)
{
@@ -1138,7 +1150,7 @@ impl MimeMessage {
}
}
pub fn get_rfc724_mid(&self) -> Option<String> {
pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get_header(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
@@ -1203,6 +1215,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)
@@ -1366,7 +1381,7 @@ impl MimeMessage {
pub async fn handle_reports(
&self,
context: &Context,
from_id: u32,
from_id: ContactId,
sent_timestamp: i64,
parts: &[Part],
) {
@@ -1480,8 +1495,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>,
@@ -3183,10 +3198,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

@@ -379,10 +379,9 @@ impl Peerstate {
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&SignedPublicKey> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key
.as_ref()
.or_else(|| self.gossip_key.as_ref()),
PeerstateVerifiedStatus::Unverified => {
self.public_key.as_ref().or(self.gossip_key.as_ref())
}
}
}

View File

@@ -41,7 +41,7 @@ impl PlainText {
// as <http://example.org> cannot be handled correctly
// (they would become &lt;http://example.org&gt; where the trailing &gt; would become a valid url part).
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
let line = line.to_string().replace("\r", "");
let line = line.to_string().replace('\r', "");
let mut line = LINKIFY_MAIL_RE
.replace_all(&*line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")

View File

@@ -113,7 +113,7 @@ pub async fn get_provider_info(
domain: &str,
skip_mx: bool,
) -> Option<&'static Provider> {
let domain = domain.rsplitn(2, '@').next()?;
let domain = domain.rsplit('@').next()?;
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);

View File

@@ -366,7 +366,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
}
});
// fastmail.md: fastmail.com
// fastmail.md: 123mail.org, 150mail.com, 150ml.com, 16mail.com, 2-mail.com, 4email.net, 50mail.com, airpost.net, allmail.net, bestmail.us, cluemail.com, elitemail.org, emailcorner.net, emailengine.net, emailengine.org, emailgroups.net, emailplus.org, emailuser.net, eml.cc, f-m.fm, fast-email.com, fast-mail.org, fastem.com, fastemail.us, fastemailer.com, fastest.cc, fastimap.com, fastmail.cn, fastmail.co.uk, fastmail.com, fastmail.com.au, fastmail.de, fastmail.es, fastmail.fm, fastmail.fr, fastmail.im, fastmail.in, fastmail.jp, fastmail.mx, fastmail.net, fastmail.nl, fastmail.org, fastmail.se, fastmail.to, fastmail.tw, fastmail.uk, fastmail.us, fastmailbox.net, fastmessaging.com, fea.st, fmail.co.uk, fmailbox.com, fmgirl.com, fmguy.com, ftml.net, h-mail.us, hailmail.net, imap-mail.com, imap.cc, imapmail.org, inoutbox.com, internet-e-mail.com, internet-mail.org, internetemails.net, internetmailing.net, jetemail.net, justemail.net, letterboxes.org, mail-central.com, mail-page.com, mailandftp.com, mailas.com, mailbolt.com, mailc.net, mailcan.com, mailforce.net, mailftp.com, mailhaven.com, mailingaddress.org, mailite.com, mailmight.com, mailnew.com, mailsent.net, mailservice.ms, mailup.net, mailworks.org, ml1.net, mm.st, myfastmail.com, mymacmail.com, nospammail.net, ownmail.net, petml.com, postinbox.com, postpro.net, proinbox.com, promessage.com, realemail.net, reallyfast.biz, reallyfast.info, rushpost.com, sent.as, sent.at, sent.com, speedpost.net, speedymail.org, ssl-mail.com, swift-mail.com, the-fastest.net, the-quickest.com, theinternetemail.com, veryfast.biz, veryspeedy.net, warpmail.net, xsmail.com, yepmail.net, your-mail.com
static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
id: "fastmail",
status: Status::Preparation,
@@ -389,13 +389,6 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
port: 465,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.fastmail.com",
port: 587,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
@@ -716,8 +709,8 @@ static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "mail.ru",
status: Status::Ok,
before_login_hint: "Не рекомендуется использовать mail.ru, потому что он разряжает вашу батарею быстрее, чем другие провайдеры.",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
@@ -905,7 +898,7 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "outlook.com",
status: Status::Ok,
@@ -1495,7 +1488,123 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("example.net", &*P_EXAMPLE_COM),
("123mail.org", &*P_FASTMAIL),
("150mail.com", &*P_FASTMAIL),
("150ml.com", &*P_FASTMAIL),
("16mail.com", &*P_FASTMAIL),
("2-mail.com", &*P_FASTMAIL),
("4email.net", &*P_FASTMAIL),
("50mail.com", &*P_FASTMAIL),
("airpost.net", &*P_FASTMAIL),
("allmail.net", &*P_FASTMAIL),
("bestmail.us", &*P_FASTMAIL),
("cluemail.com", &*P_FASTMAIL),
("elitemail.org", &*P_FASTMAIL),
("emailcorner.net", &*P_FASTMAIL),
("emailengine.net", &*P_FASTMAIL),
("emailengine.org", &*P_FASTMAIL),
("emailgroups.net", &*P_FASTMAIL),
("emailplus.org", &*P_FASTMAIL),
("emailuser.net", &*P_FASTMAIL),
("eml.cc", &*P_FASTMAIL),
("f-m.fm", &*P_FASTMAIL),
("fast-email.com", &*P_FASTMAIL),
("fast-mail.org", &*P_FASTMAIL),
("fastem.com", &*P_FASTMAIL),
("fastemail.us", &*P_FASTMAIL),
("fastemailer.com", &*P_FASTMAIL),
("fastest.cc", &*P_FASTMAIL),
("fastimap.com", &*P_FASTMAIL),
("fastmail.cn", &*P_FASTMAIL),
("fastmail.co.uk", &*P_FASTMAIL),
("fastmail.com", &*P_FASTMAIL),
("fastmail.com.au", &*P_FASTMAIL),
("fastmail.de", &*P_FASTMAIL),
("fastmail.es", &*P_FASTMAIL),
("fastmail.fm", &*P_FASTMAIL),
("fastmail.fr", &*P_FASTMAIL),
("fastmail.im", &*P_FASTMAIL),
("fastmail.in", &*P_FASTMAIL),
("fastmail.jp", &*P_FASTMAIL),
("fastmail.mx", &*P_FASTMAIL),
("fastmail.net", &*P_FASTMAIL),
("fastmail.nl", &*P_FASTMAIL),
("fastmail.org", &*P_FASTMAIL),
("fastmail.se", &*P_FASTMAIL),
("fastmail.to", &*P_FASTMAIL),
("fastmail.tw", &*P_FASTMAIL),
("fastmail.uk", &*P_FASTMAIL),
("fastmail.us", &*P_FASTMAIL),
("fastmailbox.net", &*P_FASTMAIL),
("fastmessaging.com", &*P_FASTMAIL),
("fea.st", &*P_FASTMAIL),
("fmail.co.uk", &*P_FASTMAIL),
("fmailbox.com", &*P_FASTMAIL),
("fmgirl.com", &*P_FASTMAIL),
("fmguy.com", &*P_FASTMAIL),
("ftml.net", &*P_FASTMAIL),
("h-mail.us", &*P_FASTMAIL),
("hailmail.net", &*P_FASTMAIL),
("imap-mail.com", &*P_FASTMAIL),
("imap.cc", &*P_FASTMAIL),
("imapmail.org", &*P_FASTMAIL),
("inoutbox.com", &*P_FASTMAIL),
("internet-e-mail.com", &*P_FASTMAIL),
("internet-mail.org", &*P_FASTMAIL),
("internetemails.net", &*P_FASTMAIL),
("internetmailing.net", &*P_FASTMAIL),
("jetemail.net", &*P_FASTMAIL),
("justemail.net", &*P_FASTMAIL),
("letterboxes.org", &*P_FASTMAIL),
("mail-central.com", &*P_FASTMAIL),
("mail-page.com", &*P_FASTMAIL),
("mailandftp.com", &*P_FASTMAIL),
("mailas.com", &*P_FASTMAIL),
("mailbolt.com", &*P_FASTMAIL),
("mailc.net", &*P_FASTMAIL),
("mailcan.com", &*P_FASTMAIL),
("mailforce.net", &*P_FASTMAIL),
("mailftp.com", &*P_FASTMAIL),
("mailhaven.com", &*P_FASTMAIL),
("mailingaddress.org", &*P_FASTMAIL),
("mailite.com", &*P_FASTMAIL),
("mailmight.com", &*P_FASTMAIL),
("mailnew.com", &*P_FASTMAIL),
("mailsent.net", &*P_FASTMAIL),
("mailservice.ms", &*P_FASTMAIL),
("mailup.net", &*P_FASTMAIL),
("mailworks.org", &*P_FASTMAIL),
("ml1.net", &*P_FASTMAIL),
("mm.st", &*P_FASTMAIL),
("myfastmail.com", &*P_FASTMAIL),
("mymacmail.com", &*P_FASTMAIL),
("nospammail.net", &*P_FASTMAIL),
("ownmail.net", &*P_FASTMAIL),
("petml.com", &*P_FASTMAIL),
("postinbox.com", &*P_FASTMAIL),
("postpro.net", &*P_FASTMAIL),
("proinbox.com", &*P_FASTMAIL),
("promessage.com", &*P_FASTMAIL),
("realemail.net", &*P_FASTMAIL),
("reallyfast.biz", &*P_FASTMAIL),
("reallyfast.info", &*P_FASTMAIL),
("rushpost.com", &*P_FASTMAIL),
("sent.as", &*P_FASTMAIL),
("sent.at", &*P_FASTMAIL),
("sent.com", &*P_FASTMAIL),
("speedpost.net", &*P_FASTMAIL),
("speedymail.org", &*P_FASTMAIL),
("ssl-mail.com", &*P_FASTMAIL),
("swift-mail.com", &*P_FASTMAIL),
("the-fastest.net", &*P_FASTMAIL),
("the-quickest.com", &*P_FASTMAIL),
("theinternetemail.com", &*P_FASTMAIL),
("veryfast.biz", &*P_FASTMAIL),
("veryspeedy.net", &*P_FASTMAIL),
("warpmail.net", &*P_FASTMAIL),
("xsmail.com", &*P_FASTMAIL),
("yepmail.net", &*P_FASTMAIL),
("your-mail.com", &*P_FASTMAIL),
("firemail.at", &*P_FIREMAIL_DE),
("firemail.de", &*P_FIREMAIL_DE),
("five.chat", &*P_FIVE_CHAT),
@@ -1539,6 +1648,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("outlook.de", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("posteo.af", &*P_POSTEO),
("posteo.at", &*P_POSTEO),
@@ -1743,4 +1853,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 11));
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));

View File

@@ -9,7 +9,7 @@ use std::collections::BTreeMap;
use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, ContactId, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::key::Fingerprint;
@@ -30,7 +30,7 @@ const HTTPS_SCHEME: &str = "https://";
#[derive(Debug, Clone, PartialEq)]
pub enum Qr {
AskVerifyContact {
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -38,16 +38,16 @@ pub enum Qr {
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
contact_id: ContactId,
},
FprMismatch {
contact_id: Option<u32>,
contact_id: Option<ContactId>,
},
FprWithoutAddr {
fingerprint: String,
@@ -60,7 +60,7 @@ pub enum Qr {
instance_pattern: String,
},
Addr {
contact_id: u32,
contact_id: ContactId,
},
Url {
url: String,
@@ -69,7 +69,7 @@ pub enum Qr {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -77,13 +77,13 @@ pub enum Qr {
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -91,7 +91,7 @@ pub enum Qr {
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -173,7 +173,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
};
let name = if let Some(encoded_name) = param.get("n") {
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
match percent_decode_str(&encoded_name).decode_utf8() {
Ok(name) => name.to_string(),
Err(err) => bail!("Invalid name: {}", err),
@@ -188,7 +188,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let grpname = if grpid.is_some() {
if let Some(encoded_name) = param.get("g") {
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
match percent_decode_str(&encoded_name).decode_utf8() {
Ok(name) => Some(name.to_string()),
Err(err) => bail!("Invalid group name: {}", err),
@@ -711,7 +711,7 @@ mod tests {
..
} = qr
{
assert_ne!(contact_id, 0);
assert_ne!(contact_id, ContactId::new(0));
assert_eq!(grpname, "test ? test !");
} else {
bail!("Wrong QR code type");
@@ -729,7 +729,7 @@ mod tests {
..
} = qr
{
assert_ne!(contact_id, 0);
assert_ne!(contact_id, ContactId::new(0));
assert_eq!(grpname, "test ? test !");
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
@@ -751,7 +751,7 @@ mod tests {
).await?;
if let Qr::AskVerifyContact { contact_id, .. } = qr {
assert_ne!(contact_id, 0);
assert_ne!(contact_id, ContactId::new(0));
} else {
bail!("Wrong QR code type");
}

View File

@@ -6,13 +6,12 @@ use std::collections::BTreeMap;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::param::Params;
use crate::{job, stock_str, EventType};

View File

@@ -11,6 +11,7 @@ use crate::dc_tools::maybe_add_time_based_warnings;
use crate::ephemeral::delete_expired_imap_messages;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::log::LogExt;
use crate::smtp::{send_smtp_messages, Smtp};
use self::connectivity::ConnectivityStore;
@@ -169,25 +170,49 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
warn!(ctx, "{:#}", err);
}
// 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);
}
}
// 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
@@ -350,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

@@ -3,21 +3,20 @@
use std::convert::TryFrom;
use anyhow::{bail, Context as _, Error, Result};
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, Origin, VerifiedStatus};
use crate::constants::{Blocked, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_tools::time;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
@@ -25,28 +24,16 @@ use crate::qr::check_qr;
use crate::stock_str;
use crate::token;
mod bob;
mod bobstate;
mod qrinvite;
use crate::token::Namespace;
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
use bobstate::BobState;
use qrinvite::QrInvite;
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
macro_rules! joiner_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
$progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::SecurejoinJoinerProgress {
contact_id: $contact_id,
progress: $progress,
});
};
}
macro_rules! inviter_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
@@ -60,100 +47,6 @@ macro_rules! inviter_progress {
};
}
/// State for setup-contact/secure-join protocol joiner's side, aka Bob's side.
///
/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the
/// joiner/invitee (Bob). For Alice this state is minimal and in the `tokens` table in the
/// database. For Bob this state is only carried live on the [`Context`] in this struct.
#[derive(Debug, Default)]
pub(crate) struct Bob {
inner: Mutex<Option<BobState>>,
}
/// Return value for [`Bob::start_protocol`].
///
/// This indicates which protocol variant was started and provides the required information
/// about it.
enum StartedProtocolVariant {
/// The setup-contact protocol, to verify a contact.
SetupContact,
/// The secure-join protocol, to join a group.
SecureJoin {
group_id: String,
group_name: String,
},
}
impl Bob {
/// Starts the securejoin protocol with the QR `invite`.
///
/// This will try to start the securejoin protocol for the given QR `invite`. If it
/// succeeded the protocol state will be tracked in `self`.
///
/// This function takes care of starting the "ongoing" mechanism if required and
/// handling errors while starting the protocol.
///
/// # Returns
///
/// If the started protocol is joining a group the returned struct contains information
/// about the group and ongoing process.
async fn start_protocol(
&self,
context: &Context,
invite: QrInvite,
) -> Result<StartedProtocolVariant, JoinError> {
let mut guard = self.inner.lock().await;
if guard.is_some() {
warn!(context, "The new securejoin will replace the ongoing one.");
*guard = None;
}
let variant = match invite {
QrInvite::Group {
ref grpid,
ref name,
..
} => StartedProtocolVariant::SecureJoin {
group_id: grpid.clone(),
group_name: name.clone(),
},
_ => StartedProtocolVariant::SetupContact,
};
match BobState::start_protocol(context, invite).await {
Ok((state, stage)) => {
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
joiner_progress!(context, state.invite().contact_id(), 400);
}
*guard = Some(state);
Ok(variant)
}
Err(err) => {
if let StartedProtocolVariant::SecureJoin { .. } = variant {
context.free_ongoing().await;
}
Err(err)
}
}
}
/// Returns a handle to the [`BobState`] of the handshake.
///
/// If there currently isn't a handshake running this will return `None`. Otherwise
/// this will return a handle to the current [`BobState`]. This handle allows
/// processing an incoming message and allows terminating the handshake.
///
/// The handle contains an exclusive lock, which is held until the handle is dropped.
/// This guarantees all state and state changes are correct and allows safely
/// terminating the handshake without worrying about concurrency.
async fn state(&self, context: &Context) -> Option<BobStateHandle<'_>> {
let guard = self.inner.lock().await;
let ret = BobStateHandle::from_guard(guard);
if ret.is_none() {
info!(context, "No active BobState found for securejoin handshake");
}
ret
}
}
/// Generates a Secure Join QR code.
///
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
@@ -280,7 +173,7 @@ pub enum JoinError {
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
securejoin(context, qr).await.map_err(|err| {
warn!(context, "Fatal joiner error: {:#}", err);
// This is a modal operation, the user has context on what failed.
// The user just scanned this QR code so has context on what failed.
error!(context, "QR process failed");
err
})
@@ -297,47 +190,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let invite = QrInvite::try_from(qr_scan)?;
match context.bob.start_protocol(context, invite.clone()).await? {
StartedProtocolVariant::SetupContact => {
// for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
.await
.map_err(JoinError::UnknownContact)?;
Ok(chat_id)
}
StartedProtocolVariant::SecureJoin {
group_id,
group_name,
} => {
// for a group-join, also create the chat soon and let the verification run in background.
// however, the group will become usable only when the protocol has finished.
let contact_id = invite.contact_id();
let chat_id = if let Some((chat_id, _protected, _blocked)) =
chat::get_chat_id_by_grpid(context, &group_id).await?
{
chat_id.unblock(context).await?;
chat_id
} else {
ChatId::create_multiuser_record(
context,
Chattype::Group,
&group_id,
&group_name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
None,
)
.await?
};
if !is_contact_in_chat(context, chat_id, contact_id).await? {
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
}
let msg = stock_str::secure_join_started(context, contact_id).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
Ok(chat_id)
}
}
bob::start_protocol(context, invite).await
}
/// Error when failing to send a protocol handshake message.
@@ -352,7 +205,7 @@ pub struct SendMsgError(#[from] anyhow::Error);
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
async fn send_alice_handshake_msg(
context: &Context,
contact_id: u32,
contact_id: ContactId,
step: &str,
fingerprint: Option<Fingerprint>,
) -> Result<(), SendMsgError> {
@@ -380,7 +233,7 @@ async fn send_alice_handshake_msg(
}
/// Get an unblocked chat that can be used for info messages.
async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
Ok(chat_id_blocked.id)
}
@@ -388,7 +241,7 @@ async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
async fn fingerprint_equals_sender(
context: &Context,
fingerprint: &Fingerprint,
contact_id: u32,
contact_id: ContactId,
) -> Result<bool, Error> {
let contact = Contact::load_from_db(context, contact_id).await?;
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
@@ -442,9 +295,8 @@ pub(crate) enum HandshakeMessage {
/// Handle incoming secure-join handshake.
///
/// This function will update the securejoin state in [`InnerContext::bob`] and also
/// terminate the ongoing process using [`Context::stop_ongoing`] as required by the
/// protocol.
/// This function will update the securejoin state in the database as the protocol
/// progresses.
///
/// A message which results in [`Err`] will be hidden from the user but not deleted, it may
/// be a valid message for something else we are not aware off. E.g. it could be part of a
@@ -452,13 +304,11 @@ pub(crate) enum HandshakeMessage {
///
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
/// database; this is done by `receive_imf()` later on as needed.
///
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
#[allow(clippy::indexing_slicing)]
pub(crate) async fn handle_securejoin_handshake(
context: &Context,
mime_message: &MimeMessage,
contact_id: u32,
contact_id: ContactId,
) -> Result<HandshakeMessage> {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Err(Error::msg("Can not be called with special contact ID"));
@@ -521,38 +371,7 @@ pub(crate) async fn handle_securejoin_handshake(
==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol =====
========================================================*/
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
if join_vg {
// the message reads "Alice replied, waiting for being added to the group…";
// show it only on secure-join and not on setup-contact therefore.
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(
context,
bobstate.chat_id(context).await?,
&msg,
time(),
)
.await?;
}
joiner_progress!(context, bobstate.invite().contact_id(), 400);
Ok(HandshakeMessage::Done)
}
None => Ok(HandshakeMessage::Ignore),
},
None => Ok(HandshakeMessage::Ignore),
}
bob::handle_auth_required(context, mime_message).await
}
"vg-request-with-auth" | "vc-request-with-auth" => {
/*==========================================================
@@ -683,44 +502,14 @@ pub(crate) async fn handle_securejoin_handshake(
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
info!(context, "matched vc-contact-confirm step");
let retval = if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Ignore
};
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Can only be BobHandshakeStage::Completed
secure_connection_established(
context,
contact_id,
bobstate.chat_id(context).await?,
)
.await?;
Ok(retval)
}
Some(_) => {
warn!(
context,
"Impossible state returned from handling handshake message"
);
Ok(retval)
}
None => Ok(retval),
match BobState::from_db(&context.sql).await? {
Some(bobstate) => {
bob::handle_contact_confirm(context, bobstate, mime_message).await
}
None => match join_vg {
true => Ok(HandshakeMessage::Propagate),
false => Ok(HandshakeMessage::Ignore),
},
None => Ok(retval),
}
}
"vg-member-added-received" | "vc-contact-confirm-received" => {
@@ -782,7 +571,7 @@ pub(crate) async fn handle_securejoin_handshake(
pub(crate) async fn observe_securejoin_on_other_device(
context: &Context,
mime_message: &MimeMessage,
contact_id: u32,
contact_id: ContactId,
) -> Result<HandshakeMessage> {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Err(Error::msg("Can not be called with special contact ID"));
@@ -847,11 +636,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
async fn secure_connection_established(
context: &Context,
contact_id: u32,
contact_id: ContactId,
chat_id: ChatId,
) -> Result<(), Error> {
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
let msg = stock_str::contact_verified(context, &contact).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
@@ -859,20 +648,12 @@ async fn secure_connection_established(
async fn could_not_establish_secure_connection(
context: &Context,
contact_id: u32,
contact_id: ContactId,
chat_id: ChatId,
details: &str,
) -> Result<(), Error> {
let contact = Contact::get_by_id(context, contact_id).await;
let msg = stock_str::contact_not_verified(
context,
if let Ok(ref contact) = contact {
contact.get_addr()
} else {
"?"
},
)
.await;
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_not_verified(context, &contact).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
warn!(
context,
@@ -942,30 +723,34 @@ mod tests {
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::peerstate::Peerstate;
use crate::test_utils::{LogSink, TestContext};
use crate::test_utils::{TestContext, TestContextManager};
#[async_std::test]
async fn test_setup_contact() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
async fn test_setup_contact() {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
0
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
0
);
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap();
// Step 2: Bob scans QR-code, sends vc-request
dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
@@ -977,7 +762,13 @@ mod tests {
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -1039,21 +830,30 @@ mod tests {
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
// Check Alice got the verified message in her 1:1 chat.
{
@@ -1093,14 +893,14 @@ mod tests {
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
contact_bob.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
contact_alice.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
@@ -1131,7 +931,6 @@ mod tests {
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
Ok(())
}
#[async_std::test]
@@ -1143,17 +942,9 @@ mod tests {
#[async_std::test]
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
@@ -1276,17 +1067,9 @@ mod tests {
#[async_std::test]
async fn test_setup_contact_concurrent_calls() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// do a scan that is not working as claire is never responding
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
@@ -1315,25 +1098,19 @@ mod tests {
#[async_std::test]
async fn test_secure_join() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// We start with empty chatlists.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
let chatid =
let alice_chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = dc_get_securejoin_qr(&alice.ctx, Some(chatid))
let qr = dc_get_securejoin_qr(&alice.ctx, Some(alice_chatid))
.await
.unwrap();
@@ -1424,6 +1201,35 @@ mod tests {
"vg-member-added"
);
{
// Now Alice's chat with Bob should still be hidden, the verified message should
// appear in the group chat.
let chat = alice
.get_chat(&bob)
.await
.expect("Alice has no 1:1 chat with bob");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, 0x1, None)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
}
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
@@ -1438,10 +1244,53 @@ mod tests {
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
{
// Bob has Alice verified, message shows up in the group chat.
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
let chat = bob
.get_chat(&alice)
.await
.expect("Bob has no 1:1 chat with Alice");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Bob's 1:1 chat with Alice is not hidden"
);
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
.await
.unwrap()
{
if let chat::ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
println!("msg {} text: {}", msg_id, text);
}
}
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
.await
.unwrap()
.into_iter();
loop {
match msg_iter.next() {
Some(chat::ChatItem::Message { msg_id }) => {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
match text.contains("alice@example.org verified") {
true => {
assert!(msg.is_info());
break;
}
false => continue,
}
}
Some(_) => continue,
None => panic!("Verified message not found in Bob's group chat"),
}
}
}
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;

257
src/securejoin/bob.rs Normal file
View File

@@ -0,0 +1,257 @@
//! Bob's side of SecureJoin handling.
//!
//! This are some helper functions around [`BobState`] which augment the state changes with
//! the required user interactions.
use anyhow::Result;
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
use crate::constants::{Blocked, Chattype};
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::mimeparser::MimeMessage;
use crate::{chat, stock_str};
use super::bobstate::{BobHandshakeStage, BobState};
use super::qrinvite::QrInvite;
use super::{HandshakeMessage, JoinError};
/// Starts the securejoin protocol with the QR `invite`.
///
/// This will try to start the securejoin protocol for the given QR `invite`. If it
/// succeeded the protocol state will be tracked in `self`.
///
/// This function takes care of handling multiple concurrent joins and handling errors while
/// starting the protocol.
///
/// # Returns
///
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
/// chat with Alice, for a SecureJoin QR this is the group chat.
pub(super) async fn start_protocol(
context: &Context,
invite: QrInvite,
) -> Result<ChatId, JoinError> {
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
// hidden, if a user starts sending messages in it it will be unhidden in
// dc_receive_imf.
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,
};
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.map_err(JoinError::UnknownContact)?;
// Now start the protocol and initialise the state
let (state, stage, aborted_states) =
BobState::start_protocol(context, invite.clone(), chat_id).await?;
for state in aborted_states {
error!(context, "Aborting previously unfinished QR Join process.");
state.notify_aborted(context, "new QR scanned").await?;
state.emit_progress(context, JoinerProgress::Error);
}
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
state.emit_progress(context, JoinerProgress::RequestWithAuthSent);
}
match invite {
QrInvite::Group { .. } => {
// For a secure-join we need to create the group and add the contact. The group will
// only become usable once the protocol is finished.
// TODO: how does this group become usable?
let group_chat_id = state.joining_chat_id(context).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(context, group_chat_id, invite.contact_id())
.await?;
}
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
Ok(group_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
// uses it to send the handshake messages.
Ok(state.alice_chat())
}
}
}
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
///
/// # Bob - the joiner's side
/// ## Step 4 in the "Setup Contact protocol"
pub(super) async fn handle_auth_required(
context: &Context,
message: &MimeMessage,
) -> Result<HandshakeMessage> {
match BobState::from_db(&context.sql).await? {
Some(mut bobstate) => match bobstate.handle_message(context, message).await? {
Some(BobHandshakeStage::Terminated(why)) => {
bobstate.notify_aborted(context, why).await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
if bobstate.is_join_group() {
// The message reads "Alice replied, waiting to be added to the group…",
// so only show it on secure-join and not on setup-contact.
let contact_id = bobstate.invite().contact_id();
let msg = stock_str::secure_join_replies(context, contact_id).await;
let chat_id = bobstate.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
}
bobstate.emit_progress(context, JoinerProgress::RequestWithAuthSent);
Ok(HandshakeMessage::Done)
}
None => Ok(HandshakeMessage::Ignore),
},
None => Ok(HandshakeMessage::Ignore),
}
}
/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages.
///
/// # Bob - the joiner's side
/// ## Step 4 in the "Setup Contact protocol"
pub(super) async fn handle_contact_confirm(
context: &Context,
mut bobstate: BobState,
message: &MimeMessage,
) -> Result<HandshakeMessage> {
let retval = if bobstate.is_join_group() {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Ignore
};
match bobstate.handle_message(context, message).await? {
Some(BobHandshakeStage::Terminated(why)) => {
bobstate.notify_aborted(context, why).await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Note this goes to the 1:1 chat, as when joining a group we implicitly also
// verify both contacts (this could be a bug/security issue, see
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
bobstate.notify_peer_verified(context).await?;
Ok(retval)
}
Some(_) => {
warn!(
context,
"Impossible state returned from handling handshake message"
);
Ok(retval)
}
None => Ok(retval),
}
}
/// Private implementations for user interactions about this [`BobState`].
impl BobState {
fn is_join_group(&self) -> bool {
match self.invite() {
QrInvite::Contact { .. } => false,
QrInvite::Group { .. } => true,
}
}
fn emit_progress(&self, context: &Context, progress: JoinerProgress) {
let contact_id = self.invite().contact_id();
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: progress.into(),
});
}
/// Returns the [`ChatId`] of the chat being joined.
///
/// This is the chat in which you want to notify the user as well.
///
/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
/// contact this is the [`ChatId`] of the 1:1 chat. The 1:1 chat is assumed to exist
/// because a [`BobState`] can not exist without, the group chat will be created if it
/// does not yet exist.
async fn joining_chat_id(&self, context: &Context) -> Result<ChatId> {
match self.invite() {
QrInvite::Contact { .. } => Ok(self.alice_chat()),
QrInvite::Group {
ref grpid,
ref name,
..
} => {
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
Some((chat_id, _protected, _blocked)) => {
chat_id.unblock(context).await?;
chat_id
}
None => {
ChatId::create_multiuser_record(
context,
Chattype::Group,
grpid,
name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
None,
)
.await?
}
};
Ok(group_chat_id)
}
}
}
/// Notifies the user that the SecureJoin was aborted.
///
/// This creates an info message in the chat being joined.
async fn notify_aborted(&self, context: &Context, why: &str) -> Result<()> {
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
let msg = stock_str::contact_not_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
warn!(
context,
"StockMessage::ContactNotVerified posted to joining chat ({})", why
);
Ok(())
}
/// Notifies the user that the SecureJoin peer is verified.
///
/// This creates an info message in the chat being joined.
async fn notify_peer_verified(&self, context: &Context) -> Result<()> {
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
let msg = stock_str::contact_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}
}
/// Progress updates for [`EventType::SecurejoinJoinerProgress`].
///
/// This has an `From<JoinerProgress> for usize` impl yielding numbers between 0 and a 1000
/// which can be shown as a progress bar.
enum JoinerProgress {
/// An error occurred.
Error,
/// vg-vc-request-with-auth sent.
///
/// Typically shows as "alice@addr verified, introducing myself."
RequestWithAuthSent,
// /// Completed securejoin.
// Succeeded,
}
impl From<JoinerProgress> for usize {
fn from(progress: JoinerProgress) -> Self {
match progress {
JoinerProgress::Error => 0,
JoinerProgress::RequestWithAuthSent => 400,
// JoinerProgress::Succeeded => 1000,
}
}
}

View File

@@ -5,22 +5,21 @@
//! provides all the information to its driver so it can perform the correct interactions.
//!
//! The [`BobState`] is only directly used to initially create it when starting the
//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be
//! used to work with the state.
//! protocol.
use anyhow::{bail, Error, Result};
use async_std::sync::MutexGuard;
use anyhow::{Error, Result};
use rusqlite::Connection;
use crate::chat::{self, ChatId};
use crate::constants::{Blocked, Viewtype};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, SignedPublicKey};
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::sql::Sql;
use super::qrinvite::QrInvite;
use super::{
@@ -46,123 +45,14 @@ pub enum BobHandshakeStage {
Terminated(&'static str),
}
/// A handle to work with the [`BobState`] of Bob's securejoin protocol.
/// The securejoin state kept while Bob is joining.
///
/// This handle can only be created for when an underlying [`BobState`] exists. It keeps
/// open a lock which guarantees unique access to the state and this struct must be dropped
/// to return the lock.
pub struct BobStateHandle<'a> {
guard: MutexGuard<'a, Option<BobState>>,
bobstate: BobState,
clear_state_on_drop: bool,
}
impl<'a> BobStateHandle<'a> {
/// Creates a new instance, upholding the guarantee that [`BobState`] must exist.
pub fn from_guard(mut guard: MutexGuard<'a, Option<BobState>>) -> Option<Self> {
guard.take().map(|bobstate| Self {
guard,
bobstate,
clear_state_on_drop: false,
})
}
/// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice.
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
match self.bobstate.invite {
QrInvite::Group { ref grpid, .. } => {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, grpid).await? {
Ok(chat_id)
} else {
bail!("chat not found")
}
}
QrInvite::Contact { .. } => Ok(self.bobstate.chat_id),
}
}
/// Returns a reference to the [`QrInvite`] of the joiner process.
pub fn invite(&self) -> &QrInvite {
&self.bobstate.invite
}
/// Handles the given message for the securejoin handshake for Bob.
///
/// This proxies to [`BobState::handle_message`] and makes sure to clear the state when
/// the protocol state is terminal. It returns `Some` if the message successfully
/// advanced the state of the protocol state machine, `None` otherwise.
pub async fn handle_message(
&mut self,
context: &Context,
mime_message: &MimeMessage,
) -> Option<BobHandshakeStage> {
info!(context, "Handling securejoin message for BobStateHandle");
match self.bobstate.handle_message(context, mime_message).await {
Ok(Some(stage)) => {
if matches!(
stage,
BobHandshakeStage::Completed | BobHandshakeStage::Terminated(_)
) {
self.finish_protocol(context).await;
}
Some(stage)
}
Ok(None) => None,
Err(err) => {
warn!(
context,
"Error handling handshake message, aborting handshake: {}", err
);
self.finish_protocol(context).await;
None
}
}
}
/// Marks the bob handshake as finished.
///
/// This will clear the state on [`InnerContext::bob`] once this handle is dropped,
/// allowing a new handshake to be started from [`Bob`].
///
/// Note that the state is only cleared on Drop since otherwise the invariant that the
/// state is always consistent is violated. However the "ongoing" process is released
/// here a little bit earlier as this requires access to the Context, which we do not
/// have on Drop (Drop can not run asynchronous code). Stopping the "ongoing" process
/// will release [`securejoin`](super::securejoin) which in turn will finally free the
/// ongoing process using [`Context::free_ongoing`].
///
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
/// [`Bob`]: super::Bob
async fn finish_protocol(&mut self, context: &Context) {
info!(context, "Finishing securejoin handshake protocol for Bob");
self.clear_state_on_drop = true;
if let QrInvite::Group { .. } = self.bobstate.invite {
context.stop_ongoing().await;
}
}
}
impl<'a> Drop for BobStateHandle<'a> {
fn drop(&mut self) {
if self.clear_state_on_drop {
// The Option should already be empty because we take it out in the ctor,
// however the typesystem doesn't guarantee this so do it again anyway.
self.guard.take();
} else {
// Make sure to put back the BobState into the Option of the Mutex, it was taken
// out by the constructor.
self.guard.replace(self.bobstate.clone());
}
}
}
/// The securejoin state kept in-memory while Bob is joining.
/// This is stored in the database and loaded from there using [`BobState::from_db`]. To
/// create a new one use [`BobState::start_protocol`].
///
/// This is currently stored in [`Bob`] which is stored on the [`Context`], thus Bob can
/// only run one securejoin joiner protocol at a time.
///
/// This purposefully has nothing optional, the state is always fully valid. See
/// [`Bob::state`] to get access to this state.
/// This purposefully has nothing optional, the state is always fully valid. However once a
/// terminal state is reached in [`BobState::next`] the entry in the database will already
/// have been deleted.
///
/// # Conducting the securejoin handshake
///
@@ -177,6 +67,8 @@ impl<'a> Drop for BobStateHandle<'a> {
/// [`Bob::state`]: super::Bob::state
#[derive(Debug, Clone)]
pub struct BobState {
/// Database primary key.
id: i64,
/// The QR Invite code.
invite: QrInvite,
/// The next expected message from Alice.
@@ -188,39 +80,120 @@ pub struct BobState {
impl BobState {
/// Starts the securejoin protocol and creates a new [`BobState`].
///
/// The `chat_id` needs to be the ID of the 1:1 chat with Alice, this chat will be used
/// to exchange the SecureJoin handshake messages as well as for showing error messages.
///
/// # Bob - the joiner's side
/// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0
///
/// This currently aborts any other securejoin process if any did not yet complete. The
/// ChatIds of the relevant 1:1 chat of any aborted handshakes are returned so that you
/// can report the aboreted handshake in the chat. (Yes, there can only ever be one
/// ChatId in that Vec, the database doesn't care though.)
pub async fn start_protocol(
context: &Context,
invite: QrInvite,
) -> Result<(Self, BobHandshakeStage), JoinError> {
let chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes)
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? {
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
let state = Self {
invite,
next: SecureJoinStep::ContactConfirm,
chat_id,
chat_id: ChatId,
) -> Result<(Self, BobHandshakeStage, Vec<Self>), JoinError> {
let (stage, next) =
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await?
{
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
.await?;
(
BobHandshakeStage::RequestWithAuthSent,
SecureJoinStep::ContactConfirm,
)
} else {
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
(BobHandshakeStage::RequestSent, SecureJoinStep::AuthRequired)
};
state
.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
.await?;
Ok((state, BobHandshakeStage::RequestWithAuthSent))
} else {
let state = Self {
invite,
next: SecureJoinStep::AuthRequired,
chat_id,
};
state
.send_handshake_message(context, BobHandshakeMsg::Request)
.await?;
Ok((state, BobHandshakeStage::RequestSent))
}
let (id, aborted_states) =
Self::insert_new_db_entry(context, next, invite.clone(), chat_id).await?;
let state = Self {
id,
invite,
next,
chat_id,
};
Ok((state, stage, aborted_states))
}
/// Inserts a new entry in the bobstate table, deleting all previous entries.
///
/// Returns the ID of the newly inserted entry and all the aborted states.
async fn insert_new_db_entry(
context: &Context,
next: SecureJoinStep,
invite: QrInvite,
chat_id: ChatId,
) -> Result<(i64, Vec<Self>)> {
context
.sql
.transaction(move |transaction| {
// We need to start a write transaction right away, so that we have the
// database locked and no one else can write to this table while we read the
// rows that we will delete. So start with a dummy UPDATE.
transaction.execute(
r#"UPDATE bobstate SET next_step=?;"#,
params![SecureJoinStep::Terminated],
)?;
let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?;
let mut aborted = Vec::new();
for id in stmt.query_map(params![], |row| row.get::<_, i64>(0))? {
let id = id?;
let state = BobState::from_db_id(transaction, id)?;
aborted.push(state);
}
// Finally delete everything and insert new row.
transaction.execute("DELETE FROM bobstate;", params![])?;
transaction.execute(
"INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
params![invite, next, chat_id],
)?;
let id = transaction.last_insert_rowid();
Ok((id, aborted))
})
.await
}
/// Load [`BobState`] from the database.
pub async fn from_db(sql: &Sql) -> Result<Option<Self>> {
// Because of how Self::start_protocol() updates the database we are currently
// guaranteed to only have one row.
sql.query_row_optional(
"SELECT id, invite, next_step, chat_id FROM bobstate;",
paramsv![],
|row| {
let s = BobState {
id: row.get(0)?,
invite: row.get(1)?,
next: row.get(2)?,
chat_id: row.get(3)?,
};
Ok(s)
},
)
.await
}
fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result<Self> {
connection.query_row(
"SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;",
params![id],
|row| {
let s = BobState {
id,
invite: row.get(0)?,
next: row.get(1)?,
chat_id: row.get(2)?,
};
Ok(s)
},
)
}
/// Returns the [`QrInvite`] used to create this [`BobState`].
@@ -228,20 +201,45 @@ impl BobState {
&self.invite
}
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
pub fn alice_chat(&self) -> ChatId {
self.chat_id
}
/// Updates the [`BobState::next`] field in memory and the database.
///
/// If the next state is a terminal state it will remove this [`BobState`] from the
/// database.
///
/// If a user scanned a new QR code after this [`BobState`] was loaded this update will
/// fail currently because starting a new joiner process currently kills any previously
/// running processes. This is a limitation which will go away in the future.
async fn update_next(&mut self, sql: &Sql, next: SecureJoinStep) -> Result<()> {
// TODO: write test verifying how this would fail.
match next {
SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => {
sql.execute(
"UPDATE bobstate SET next_step=? WHERE id=?;",
paramsv![next, self.id],
)
.await?;
}
SecureJoinStep::Terminated | SecureJoinStep::Completed => {
sql.execute("DELETE FROM bobstate WHERE id=?;", paramsv!(self.id))
.await?;
}
}
self.next = next;
Ok(())
}
/// Handles the given message for the securejoin handshake for Bob.
///
/// If the message was not used for this handshake `None` is returned, otherwise the new
/// stage is returned. Once [`BobHandshakeStage::Completed`] or
/// [`BobHandshakeStage::Terminated`] are reached this [`BobState`] should be destroyed,
/// further calling it will just result in the messages being unused by this handshake.
///
/// # Errors
///
/// Under normal operation this should never return an error, regardless of what kind of
/// message it is called with. Any errors therefore should be treated as fatal internal
/// errors and this entire [`BobState`] should be thrown away as the state machine can
/// no longer be considered consistent.
async fn handle_message(
pub async fn handle_message(
&mut self,
context: &Context,
mime_message: &MimeMessage,
@@ -304,17 +302,20 @@ impl BobState {
} else {
"Required encryption missing"
};
self.next = SecureJoinStep::Terminated;
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
return Ok(Some(BobHandshakeStage::Terminated(reason)));
}
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
.await?
{
self.next = SecureJoinStep::Terminated;
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
}
info!(context, "Fingerprint verified.",);
self.next = SecureJoinStep::ContactConfirm;
self.update_next(&context.sql, SecureJoinStep::ContactConfirm)
.await?;
self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
.await?;
Ok(Some(BobHandshakeStage::RequestWithAuthSent))
@@ -362,7 +363,8 @@ impl BobState {
if vg_expect_encrypted
&& !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint()))
{
self.next = SecureJoinStep::Terminated;
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
return Ok(Some(BobHandshakeStage::Terminated(
"Contact confirm message not encrypted",
)));
@@ -394,7 +396,8 @@ impl BobState {
// This is not an error affecting the protocol outcome.
.ok();
self.next = SecureJoinStep::Completed;
self.update_next(&context.sql, SecureJoinStep::Completed)
.await?;
Ok(Some(BobHandshakeStage::Completed))
}
@@ -406,48 +409,60 @@ impl BobState {
context: &Context,
step: BobHandshakeMsg,
) -> Result<(), SendMsgError> {
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(step.body_text(&self.invite)),
hidden: true,
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
// Sends the step in Secure-Join header.
msg.param
.set(Param::Arg, step.securejoin_header(&self.invite));
match step {
BobHandshakeMsg::Request => {
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
msg.param.set(Param::Arg2, self.invite.invitenumber());
msg.force_plaintext();
}
BobHandshakeMsg::RequestWithAuth => {
// Sends the Secure-Join-Auth header in mimefactory.rs.
msg.param.set(Param::Arg2, self.invite.authcode());
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
BobHandshakeMsg::ContactConfirmReceived => {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
};
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.
if let QrInvite::Group { ref grpid, .. } = self.invite {
msg.param.set(Param::Arg4, grpid);
}
chat::send_msg(context, self.chat_id, &mut msg).await?;
Ok(())
send_handshake_message(context, &self.invite, self.chat_id, step).await
}
}
/// Sends the requested handshake message to Alice.
///
/// Same as [`BobState::send_handshake_message`] but this variation allows us to send this
/// message before we create the state in [`BobState::start_protocol`].
async fn send_handshake_message(
context: &Context,
invite: &QrInvite,
chat_id: ChatId,
step: BobHandshakeMsg,
) -> Result<(), SendMsgError> {
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(step.body_text(invite)),
hidden: true,
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
// Sends the step in Secure-Join header.
msg.param.set(Param::Arg, step.securejoin_header(invite));
match step {
BobHandshakeMsg::Request => {
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
msg.param.set(Param::Arg2, invite.invitenumber());
msg.force_plaintext();
}
BobHandshakeMsg::RequestWithAuth => {
// Sends the Secure-Join-Auth header in mimefactory.rs.
msg.param.set(Param::Arg2, invite.authcode());
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
BobHandshakeMsg::ContactConfirmReceived => {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
};
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.
if let QrInvite::Group { ref grpid, .. } = invite {
msg.param.set(Param::Arg4, grpid);
}
chat::send_msg(context, chat_id, &mut msg).await?;
Ok(())
}
/// Identifies the SecureJoin handshake messages Bob can send.
enum BobHandshakeMsg {
/// vc-request or vg-request
@@ -492,8 +507,8 @@ impl BobHandshakeMsg {
}
/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol.
#[derive(Debug, Clone, PartialEq)]
enum SecureJoinStep {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SecureJoinStep {
/// Expecting the auth-required message.
///
/// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d.
@@ -533,3 +548,29 @@ impl SecureJoinStep {
}
}
}
impl rusqlite::types::ToSql for SecureJoinStep {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let num = match &self {
SecureJoinStep::AuthRequired => 0,
SecureJoinStep::ContactConfirm => 1,
SecureJoinStep::Terminated => 2,
SecureJoinStep::Completed => 3,
};
let val = rusqlite::types::Value::Integer(num);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for SecureJoinStep {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| match val {
0 => Ok(SecureJoinStep::AuthRequired),
1 => Ok(SecureJoinStep::ContactConfirm),
2 => Ok(SecureJoinStep::Terminated),
3 => Ok(SecureJoinStep::Completed),
_ => Err(rusqlite::types::FromSqlError::OutOfRange(val)),
})
}
}

View File

@@ -8,22 +8,23 @@ use std::convert::TryFrom;
use anyhow::{bail, Error, Result};
use crate::contact::ContactId;
use crate::key::Fingerprint;
use crate::qr::Qr;
/// Represents the data from a QR-code scan.
///
/// There are methods to conveniently access fields present in both variants.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum QrInvite {
Contact {
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
},
Group {
contact_id: u32,
contact_id: ContactId,
fingerprint: Fingerprint,
name: String,
grpid: String,
@@ -37,7 +38,7 @@ impl QrInvite {
///
/// The actual QR-code contains a URL-encoded email address, but upon scanning this is
/// translated to a contact ID.
pub fn contact_id(&self) -> u32 {
pub fn contact_id(&self) -> ContactId {
match self {
Self::Contact { contact_id, .. } | Self::Group { contact_id, .. } => *contact_id,
}
@@ -100,3 +101,22 @@ impl TryFrom<Qr> for QrInvite {
}
}
}
impl rusqlite::types::ToSql for QrInvite {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let json = serde_json::to_string(self)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
let val = rusqlite::types::Value::Text(json);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for QrInvite {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
String::column_result(value).and_then(|val| {
serde_json::from_str(&val)
.map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err)))
})
}
}

View File

@@ -240,7 +240,7 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
ret += " [...]";
}
// redo escaping done by escape_message_footer_marks()
ret.replace("\u{200B}", "")
ret.replace('\u{200B}', "")
}
/// Returns true if the line contains only whitespace.

View File

@@ -225,7 +225,7 @@ pub(crate) async fn smtp_send(
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {:?}", &err);
info!(context, "SMTP failed to send: {:?}", &err);
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
@@ -246,14 +246,16 @@ pub(crate) async fn smtp_send(
// 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())
response.first_word() == Some("5.5.0")
}
_ => false,
};
if maybe_transient {
info!(context, "Permanent error that is likely to actually be transient, postponing retry for later");
Status::RetryLater
} else {
info!(context, "Permanent error, message sending failed");
// 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
@@ -277,23 +279,38 @@ pub(crate) async fn smtp_send(
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 {
info!(
context,
"Transient error with status code {}, postponing retry for later",
first_word
);
Status::RetryLater
}
} else {
info!(
context,
"Transient error without status code, postponing retry for later"
);
Status::RetryLater
}
}
_ => {
info!(
context,
"Message sending failed without error returned by the server"
);
if smtp.has_maybe_stale_connection().await {
info!(context, "stale connection? immediately reconnecting");
info!(context, "Connection is probably stale, retry immediately");
Status::RetryNow
} else {
info!(context, "Connection is not stale, retry later");
Status::RetryLater
}
}
};
// this clears last_success info
info!(context, "Failed to send message over SMTP, disconnecting");
smtp.disconnect().await;
res
@@ -343,19 +360,52 @@ pub(crate) async fn send_msg_to_smtp(
return Err(err);
}
let (body, recipients, msg_id) = context
// 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 FROM smtp WHERE id=?",
"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)?;
Ok((mime, recipients, msg_id))
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");
}
info!(
context,
"Retry number {} to send message {} over SMTP", retries, msg_id
);
let recipients_list = recipients
.split(' ')
.filter_map(
@@ -421,27 +471,16 @@ pub(crate) async fn send_msg_to_smtp(
};
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?;
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await?;
}
res
}
Status::RetryNow | Status::RetryLater => {
context
.sql
.execute(
"UPDATE smtp SET retries=retries+1 WHERE id=?",
paramsv![rowid],
)
.await
.context("failed to update retries count")?;
Err(format_err!("Retry"))
}
Status::RetryNow | Status::RetryLater => Err(format_err!("Retry")),
}
}
@@ -454,10 +493,6 @@ pub(crate) async fn send_smtp_messages(
connection: &mut Smtp,
) -> anyhow::Result<()> {
context.send_sync_msg().await?; // Add sync message to the end of the queue if needed.
context
.sql
.execute("DELETE FROM smtp WHERE retries > 5", paramsv![])
.await?;
let rowids = context
.sql
.query_map(

View File

@@ -3,7 +3,7 @@
use async_std::path::Path;
use async_std::sync::RwLock;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::time::Duration;
@@ -15,11 +15,11 @@ use rusqlite::{config::DbConfig, Connection, OpenFlags};
use crate::blob::BlobObject;
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
use crate::config::Config;
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
use crate::constants::DC_CHAT_ID_TRASH;
use crate::context::Context;
use crate::dc_tools::{dc_delete_file, time};
use crate::ephemeral::start_ephemeral_timers;
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str;
@@ -47,6 +47,8 @@ pub struct Sql {
/// 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>>,
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
}
impl Sql {
@@ -55,6 +57,7 @@ impl Sql {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
config_cache: Default::default(),
}
}
@@ -497,6 +500,8 @@ impl Sql {
/// will already have been logged.
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
let key = key.as_ref();
let mut lock = self.config_cache.write().await;
if let Some(value) = value {
let exists = self
.exists(
@@ -522,12 +527,23 @@ impl Sql {
self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key])
.await?;
}
lock.insert(key.to_string(), value.map(|s| s.to_string()));
drop(lock);
Ok(())
}
/// Get configuration options from the database.
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
let lock = self.config_cache.read().await;
let cached = lock.get(key.as_ref()).cloned();
drop(lock);
if let Some(c) = cached {
return Ok(c);
}
let mut lock = self.config_cache.write().await;
let value = self
.query_get_value(
"SELECT value FROM config WHERE keyname=?;",
@@ -535,6 +551,8 @@ impl Sql {
)
.await
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
lock.insert(key.as_ref().to_string(), value.clone());
drop(lock);
Ok(value)
}
@@ -573,6 +591,11 @@ impl Sql {
.await
.map(|s| s.and_then(|r| r.parse().ok()))
}
#[cfg(feature = "internals")]
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
&self.config_cache
}
}
pub async fn housekeeping(context: &Context) -> Result<()> {
@@ -787,7 +810,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!=''
)",
@@ -797,6 +820,19 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
Ok(())
}
/// Helper function to return comma-separated sequence of `?` chars.
///
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
/// parameter lists.
pub fn repeat_vars(count: usize) -> Result<String> {
if count == 0 {
bail!("Must have at least one repeat variable");
}
let mut s = "?,".repeat(count);
s.pop(); // Remove trailing comma
Ok(s)
}
#[cfg(test)]
mod tests {
use async_std::channel;
@@ -906,6 +942,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

View File

@@ -36,6 +36,13 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
Ok(())
})
.await?;
let mut lock = context.sql.config_cache.write().await;
lock.insert(
VERSION_CFG.to_string(),
Some(format!("{}", dbversion_before_update)),
);
drop(lock);
} else {
exists_before_update = true;
dbversion_before_update = sql
@@ -579,6 +586,28 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 86 {
info!(context, "[migration] v86");
sql.execute_migration(
r#"CREATE TABLE bobstate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invite TEXT NOT NULL,
next_step INTEGER NOT NULL,
chat_id INTEGER NOT NULL
);"#,
86,
)
.await?;
}
if dbversion < 87 {
info!(context, "[migration] v87");
// the index is used to speed up delete_expired_messages()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
87,
)
.await?;
}
Ok((
recalc_fingerprints,
@@ -609,6 +638,10 @@ impl Sql {
.await
.with_context(|| format!("execute_migration failed for version {}", version))?;
let mut lock = self.config_cache.write().await;
lock.insert(VERSION_CFG.to_string(), Some(format!("{}", version)));
drop(lock);
Ok(())
}
}

View File

@@ -168,6 +168,14 @@ CREATE TABLE tokens (
timestamp INTEGER DEFAULT 0
);
-- The currently running securejoin protocols, joiner-side.
-- CREATE TABLE bobstate (
-- id INTEGER PRIMARY KEY AUTOINCREMENT,
-- invite TEXT NOT NULL,
-- next_step INTEGER NOT NULL,
-- chat_id INTEGER NOT NULL
-- );
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,

View File

@@ -10,11 +10,11 @@ use strum_macros::EnumProperty;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::{Contact, Origin};
use crate::constants::DC_CONTACT_ID_SELF;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::dc_tools::dc_timestamp_to_str;
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use humansize::{file_size_opts, FileSize};
@@ -389,7 +389,7 @@ trait StockStringMods: AsRef<str> + Sized {
fn action_by_contact<'a>(
self,
context: &'a Context,
contact_id: u32,
contact_id: ContactId,
) -> Pin<Box<dyn Future<Output = String> + Send + 'a>>
where
Self: Send + 'a,
@@ -457,7 +457,7 @@ pub(crate) async fn msg_grp_name(
context: &Context,
from_group: impl AsRef<str>,
to_group: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgGrpName)
.await
@@ -468,7 +468,7 @@ pub(crate) async fn msg_grp_name(
}
/// Stock string: `Group image changed.`.
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgGrpImgChanged)
.await
.action_by_contact(context, by_contact)
@@ -482,7 +482,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: u32) -> S
pub(crate) async fn msg_add_member(
context: &Context,
added_member_addr: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
let addr = added_member_addr.as_ref();
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
@@ -506,7 +506,7 @@ pub(crate) async fn msg_add_member(
pub(crate) async fn msg_del_member(
context: &Context,
removed_member_addr: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
let addr = removed_member_addr.as_ref();
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
@@ -524,7 +524,7 @@ pub(crate) async fn msg_del_member(
}
/// Stock string: `Group left.`.
pub(crate) async fn msg_group_left(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgGroupLeft)
.await
.action_by_contact(context, by_contact)
@@ -574,7 +574,7 @@ pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef<s
}
/// Stock string: `Group image deleted.`.
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgGrpImgDeleted)
.await
.action_by_contact(context, by_contact)
@@ -587,7 +587,10 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String {
}
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String {
pub(crate) async fn secure_join_started(
context: &Context,
inviter_contact_id: ContactId,
) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
@@ -602,7 +605,7 @@ pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u
}
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String {
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
@@ -636,20 +639,19 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
}
/// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_name_n_addr();
translated(context, StockMessage::ContactVerified)
.await
.replace1(contact_addr)
.replace1(addr)
}
/// Stock string: `Cannot verify %1$s`.
pub(crate) async fn contact_not_verified(
context: &Context,
contact_addr: impl AsRef<str>,
) -> String {
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_name_n_addr();
translated(context, StockMessage::ContactNotVerified)
.await
.replace1(contact_addr)
.replace1(addr)
}
/// Stock string: `Changed setup for %1$s`.
@@ -719,7 +721,7 @@ pub(crate) async fn msg_location_enabled(context: &Context) -> String {
}
/// Stock string: `Location streaming enabled by ...`.
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: u32) -> String {
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
translated(context, StockMessage::MsgLocationEnabled)
.await
.action_by_contact(context, contact)
@@ -785,7 +787,10 @@ pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef<str>)
}
/// Stock string: `Message deletion timer is disabled.`.
pub(crate) async fn msg_ephemeral_timer_disabled(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_ephemeral_timer_disabled(
context: &Context,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerDisabled)
.await
.action_by_contact(context, by_contact)
@@ -796,7 +801,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(context: &Context, by_contact:
pub(crate) async fn msg_ephemeral_timer_enabled(
context: &Context,
timer: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerEnabled)
.await
@@ -806,7 +811,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
}
/// Stock string: `Message deletion timer is set to 1 minute.`.
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgEphemeralTimerMinute)
.await
.action_by_contact(context, by_contact)
@@ -814,7 +819,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: u3
}
/// Stock string: `Message deletion timer is set to 1 hour.`.
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgEphemeralTimerHour)
.await
.action_by_contact(context, by_contact)
@@ -822,7 +827,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: u32)
}
/// Stock string: `Message deletion timer is set to 1 day.`.
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgEphemeralTimerDay)
.await
.action_by_contact(context, by_contact)
@@ -830,7 +835,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: u32)
}
/// Stock string: `Message deletion timer is set to 1 week.`.
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: u32) -> String {
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::MsgEphemeralTimerWeek)
.await
.action_by_contact(context, by_contact)
@@ -875,7 +880,7 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
}
/// Stock string: `Chat protection enabled.`.
pub(crate) async fn protection_enabled(context: &Context, by_contact: u32) -> String {
pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::ProtectionEnabled)
.await
.action_by_contact(context, by_contact)
@@ -883,7 +888,7 @@ pub(crate) async fn protection_enabled(context: &Context, by_contact: u32) -> St
}
/// Stock string: `Chat protection disabled.`.
pub(crate) async fn protection_disabled(context: &Context, by_contact: u32) -> String {
pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String {
translated(context, StockMessage::ProtectionDisabled)
.await
.action_by_contact(context, by_contact)
@@ -909,7 +914,7 @@ pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context,
minutes: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerMinutes)
.await
@@ -922,7 +927,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
pub(crate) async fn msg_ephemeral_timer_hours(
context: &Context,
hours: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerHours)
.await
@@ -935,7 +940,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
pub(crate) async fn msg_ephemeral_timer_days(
context: &Context,
days: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerDays)
.await
@@ -948,7 +953,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
pub(crate) async fn msg_ephemeral_timer_weeks(
context: &Context,
weeks: impl AsRef<str>,
by_contact: u32,
by_contact: ContactId,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerWeeks)
.await
@@ -1105,7 +1110,7 @@ impl Context {
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
from_id: u32,
from_id: ContactId,
) -> String {
match protect {
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
@@ -1197,8 +1202,15 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_str() {
let t = TestContext::new().await;
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
// uses %1$s substitution
assert_eq!(contact_verified(&t, "Foo").await, "Foo verified.");
assert_eq!(
contact_verified(&t, &contact).await,
"Someone (someone@example.org) verified."
);
// We have no string using %1$d to test...
}

View File

@@ -1,11 +1,11 @@
//! # Message summary for chatlist.
use crate::chat::Chat;
use crate::constants::{Chattype, Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::{Chattype, DC_CONTACT_ID_SELF};
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::dc_truncate;
use crate::message::{Message, MessageState};
use crate::message::{Message, MessageState, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;

View File

@@ -2,10 +2,10 @@
use crate::chat::{Chat, ChatId};
use crate::config::Config;
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::{Blocked, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::time;
use crate::message::{Message, MsgId};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};

View File

@@ -22,14 +22,14 @@ use crate::chat::{self, Chat, ChatId};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::Chattype;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::contact::{Contact, Origin};
use crate::constants::{DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::contact::{Contact, Modifier, Origin};
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::EmailAddress;
use crate::events::{Event, EventType};
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::MimeMessage;
#[allow(non_upper_case_globals)]
@@ -39,6 +39,34 @@ pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avata
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
pub struct TestContextManager {
log_tx: Sender<Event>,
_log_sink: LogSink,
}
impl TestContextManager {
pub async fn new() -> Self {
let (log_tx, _log_sink) = LogSink::create();
Self { log_tx, _log_sink }
}
pub async fn alice(&mut self) -> TestContext {
TestContext::builder()
.configure_alice()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
pub async fn bob(&mut self) -> TestContext {
TestContext::builder()
.configure_bob()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
}
#[derive(Debug, Clone, Default)]
pub struct TestContextBuilder {
key_pair: Option<KeyPair>,
@@ -371,31 +399,56 @@ impl TestContext {
.expect("failed to load msg")
}
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
let name = other
.ctx
.get_config(Config::Displayname)
.await
.unwrap_or_default()
.unwrap_or_default();
let addr = other
.ctx
.get_config(Config::ConfiguredAddr)
.await
.unwrap()
.unwrap();
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
let (contact_id, modified) =
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
.await
.unwrap();
match modified {
Modifier::None => (),
Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr),
Modifier::Created => warn!(&self.ctx, "Contact {} created by TestContext", &addr),
}
Contact::load_from_db(&self.ctx, contact_id).await.unwrap()
}
/// Returns 1:1 [`Chat`] with another account, if it exists.
///
/// This first creates a contact using the configured details on the other account, then
/// creates a 1:1 chat with this contact.
pub async fn get_chat(&self, other: &TestContext) -> Option<Chat> {
let contact = self.add_or_lookup_contact(other).await;
match ChatId::lookup_by_contact(&self.ctx, contact.id)
.await
.unwrap()
{
Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()),
None => None,
}
}
/// Creates or returns an existing 1:1 [`Chat`] with another account.
///
/// This first creates a contact using the configured details on the other account, then
/// creates a 1:1 chat with this contact.
pub async fn create_chat(&self, other: &TestContext) -> Chat {
let (contact_id, _modified) = Contact::add_or_lookup(
self,
&other
.ctx
.get_config(Config::Displayname)
.await
.unwrap_or_default()
.unwrap_or_default(),
&other
.ctx
.get_config(Config::ConfiguredAddr)
.await
.unwrap()
.unwrap(),
Origin::ManuallyCreated,
)
.await
.unwrap();
let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap();
let contact = self.add_or_lookup_contact(other).await;
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
Chat::load_from_db(self, chat_id).await.unwrap()
}
@@ -798,7 +851,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.get_from_id() == 1u32 {
if msg.get_from_id() == DC_CONTACT_ID_SELF {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -849,17 +902,10 @@ mod tests {
#[async_std::test]
async fn test_with_both() {
let (log_sender, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_sender.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_sender)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
alice.ctx.emit_event(EventType::Info("hello".into()));
bob.ctx.emit_event(EventType::Info("there".into()));
// panic!("Both fail");

View File

@@ -1,7 +1,7 @@
//! # Functions to update timestamps.
use crate::chat::{Chat, ChatId};
use crate::contact::Contact;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::param::{Param, Params};
use anyhow::Result;
@@ -12,7 +12,7 @@ impl Context {
/// (if we have a ContactId type at some point, the function should go there)
pub(crate) async fn update_contacts_timestamp(
&self,
contact_id: u32,
contact_id: ContactId,
scope: Param,
new_timestamp: i64,
) -> Result<bool> {

View File

@@ -1,22 +1,22 @@
//! # Handle webxdc messages.
use crate::chat::Chat;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::{dc_create_smeared_timestamp, dc_open_file_std};
use crate::message::{Message, MessageState, MsgId};
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::{chat, EventType};
use anyhow::{bail, ensure, format_err, Result};
use async_std::path::PathBuf;
use lettre_email::mime::{self};
use deltachat_derive::FromSql;
use lettre_email::mime;
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::TryFrom;
use std::fs::File;
use std::io::Read;
use std::io::{Read, Seek, SeekFrom};
use zip::ZipArchive;
pub const WEBXDC_SUFFIX: &str = "xdc";
@@ -32,12 +32,12 @@ const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
///
/// The limit is also an experiment to see how small we can go;
/// it is planned to raise that limit as needed in subsequent versions.
const WEBXDC_SENDING_LIMIT: usize = 655360;
const WEBXDC_SENDING_LIMIT: u64 = 655360;
/// Be more tolerant for .xdc sizes on receiving -
/// might be, the senders version uses already a larger limit
/// and not showing the .xdc on some devices would be even worse ux.
const WEBXDC_RECEIVING_LIMIT: usize = 4194304;
const WEBXDC_RECEIVING_LIMIT: u64 = 4194304;
/// Raw information read from manifest.toml
#[derive(Debug, Deserialize)]
@@ -56,14 +56,26 @@ pub struct WebxdcInfo {
/// Status Update ID.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
Debug,
Copy,
Clone,
Default,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
FromSql,
FromPrimitive,
)]
pub struct StatusUpdateId(u32);
pub struct StatusUpdateSerial(u32);
impl StatusUpdateId {
impl StatusUpdateSerial {
/// Create a new [MsgId].
pub fn new(id: u32) -> StatusUpdateId {
StatusUpdateId(id)
pub fn new(id: u32) -> StatusUpdateSerial {
StatusUpdateSerial(id)
}
/// Gets StatusUpdateId as untyped integer.
@@ -73,9 +85,9 @@ impl StatusUpdateId {
}
}
impl rusqlite::types::ToSql for StatusUpdateId {
impl rusqlite::types::ToSql for StatusUpdateSerial {
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)
}
@@ -99,48 +111,68 @@ pub(crate) struct StatusUpdateItem {
summary: Option<String>,
}
/// Update items as passed to the UIs.
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct StatusUpdateItemAndSerial {
#[serde(flatten)]
item: StatusUpdateItem,
serial: StatusUpdateSerial,
max_serial: StatusUpdateSerial,
}
impl Context {
/// check if a file is an acceptable webxdc for sending or receiving.
pub(crate) async fn is_webxdc_file(&self, filename: &str, buf: &[u8]) -> Result<bool> {
if filename.ends_with(WEBXDC_SUFFIX) {
let reader = std::io::Cursor::new(buf);
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
if let Ok(_index_html) = archive.by_name("index.html") {
if buf.len() <= WEBXDC_RECEIVING_LIMIT {
return Ok(true);
} else {
info!(
self,
"{} exceeds receiving limit of {} bytes",
&filename,
WEBXDC_RECEIVING_LIMIT
);
}
} else {
info!(self, "{} misses index.html", &filename);
}
} else {
info!(self, "{} cannot be opened as zip-file", &filename);
}
pub(crate) async fn is_webxdc_file<R>(&self, filename: &str, mut reader: R) -> Result<bool>
where
R: Read + Seek,
{
if !filename.ends_with(WEBXDC_SUFFIX) {
return Ok(false);
}
Ok(false)
let size = reader.seek(SeekFrom::End(0))?;
if size > WEBXDC_RECEIVING_LIMIT {
info!(
self,
"{} exceeds receiving limit of {} bytes", &filename, WEBXDC_RECEIVING_LIMIT
);
return Ok(false);
}
reader.seek(SeekFrom::Start(0))?;
let mut archive = match zip::ZipArchive::new(reader) {
Ok(archive) => archive,
Err(_) => {
info!(self, "{} cannot be opened as zip-file", &filename);
return Ok(false);
}
};
if archive.by_name("index.html").is_err() {
info!(self, "{} misses index.html", &filename);
return Ok(false);
}
Ok(true)
}
/// ensure that a file is an acceptable webxdc for sending
/// (sending has more strict size limits).
pub(crate) async fn ensure_sendable_webxdc_file(&self, path: &PathBuf) -> Result<()> {
let mut file = std::fs::File::open(path)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
if !self
.is_webxdc_file(path.to_str().unwrap_or_default(), &buf)
.is_webxdc_file(path.to_str().unwrap_or_default(), &mut file)
.await?
{
bail!(
"{} is not a valid webxdc file",
path.to_str().unwrap_or_default()
);
} else if buf.len() > WEBXDC_SENDING_LIMIT {
}
let size = file.seek(SeekFrom::End(0))?;
if size > WEBXDC_SENDING_LIMIT {
bail!(
"webxdc {} exceeds acceptable size of {} bytes",
path.to_str().unwrap_or_default(),
@@ -157,7 +189,7 @@ impl Context {
instance: &mut Message,
update_str: &str,
timestamp: i64,
) -> Result<StatusUpdateId> {
) -> Result<StatusUpdateSerial> {
let update_str = update_str.trim();
if update_str.is_empty() {
bail!("create_status_update_record: empty update.");
@@ -176,13 +208,7 @@ impl Context {
_ => item,
}
} else {
// TODO: this fallback (legacy `PAYLOAD`) should be deleted soon, together with the test below
let payload: Value = serde_json::from_str(update_str)?; // checks if input data are valid json
StatusUpdateItem {
payload,
info: None,
summary: None,
}
bail!("create_status_update_record: no valid update item.");
}
};
@@ -219,14 +245,10 @@ impl Context {
paramsv![instance.id, serde_json::to_string(&status_update_item)?],
)
.await?;
let status_update_id = StatusUpdateId(u32::try_from(rowid)?);
self.emit_event(EventType::WebxdcStatusUpdate {
msg_id: instance.id,
status_update_id,
});
self.emit_event(EventType::WebxdcStatusUpdate(instance.id));
Ok(status_update_id)
Ok(StatusUpdateSerial(u32::try_from(rowid)?))
}
/// Sends a status update for an webxdc instance.
@@ -250,7 +272,7 @@ impl Context {
let chat = Chat::load_from_db(self, instance.chat_id).await?;
ensure!(chat.can_send(self).await?, "cannot send to {}", chat.id);
let status_update_id = self
let status_update_serial = self
.create_status_update_record(
&mut instance,
update_str,
@@ -279,7 +301,7 @@ impl Context {
Param::Arg,
self.render_webxdc_status_update_object(
instance_msg_id,
Some(status_update_id),
Some(status_update_serial),
)
.await?
.ok_or_else(|| format_err!("Status object expected."))?,
@@ -338,24 +360,79 @@ impl Context {
Ok(())
}
/// Returns status updates as an JSON-array.
/// Returns status updates as an JSON-array, ready to be consumed by a webxdc.
///
/// Example: `[{"payload":"any update data"},{"payload":"another update data"}]`
/// The updates may be filtered by a given status_update_id;
/// if no updates are available, an empty JSON-array is returned.
/// Example: `[{"serial":1, "max_serial":3, "payload":"any update data"},
/// {"serial":3, "max_serial":3, "payload":"another update data"}]`
/// Updates with serials larger than `last_known_serial` are returned.
/// If no last serial is known, set `last_known_serial` to 0.
/// If no updates are available, an empty JSON-array is returned.
pub async fn get_webxdc_status_updates(
&self,
instance_msg_id: MsgId,
status_update_id: Option<StatusUpdateId>,
last_known_serial: StatusUpdateSerial,
) -> Result<String> {
let json = self
.sql
.query_map(
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND (1=? OR id=?)",
"SELECT update_item, id FROM msgs_status_updates WHERE msg_id=? AND id>? ORDER BY id",
paramsv![instance_msg_id, last_known_serial],
|row| {
let update_item_str = row.get::<_, String>(0)?;
let serial = row.get::<_, StatusUpdateSerial>(1)?;
Ok((update_item_str, serial))
},
|rows| {
let mut rows_copy : Vec<(String, StatusUpdateSerial)> = Vec::new(); // `rows_copy` needed as `rows` cannot be iterated twice.
let mut max_serial = StatusUpdateSerial(0);
for row in rows {
let row = row?;
if row.1 > max_serial {
max_serial = row.1;
}
rows_copy.push(row);
}
let mut json = String::default();
for row in rows_copy {
let (update_item_str, serial) = row;
let update_item = StatusUpdateItemAndSerial
{
item: serde_json::from_str(&*update_item_str)?,
serial,
max_serial,
};
if !json.is_empty() {
json.push_str(",\n");
}
json.push_str(&*serde_json::to_string(&update_item)?);
}
Ok(json)
},
)
.await?;
Ok(format!("[{}]", json))
}
/// Renders JSON-object for status updates as used on the wire.
///
/// Example: `{"updates": [{"payload":"any update data"},
/// {"payload":"another update data"}]}`
/// If `status_update_serial` is set, exactly that update is rendered, otherwise all updates are rendered.
pub(crate) async fn render_webxdc_status_update_object(
&self,
instance_msg_id: MsgId,
status_update_serial: Option<StatusUpdateSerial>,
) -> Result<Option<String>> {
let json = self
.sql
.query_map(
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND (1=? OR id=?) ORDER BY id",
paramsv![
instance_msg_id,
if status_update_id.is_some() { 0 } else { 1 },
status_update_id.unwrap_or(StatusUpdateId(0))
if status_update_serial.is_some() { 0 } else { 1 },
status_update_serial.unwrap_or(StatusUpdateSerial(0))
],
|row| row.get::<_, String>(0),
|rows| {
@@ -371,22 +448,10 @@ impl Context {
},
)
.await?;
Ok(format!("[{}]", json))
}
/// Render JSON-object for status updates as used on the wire.
pub(crate) async fn render_webxdc_status_update_object(
&self,
instance_msg_id: MsgId,
status_update_id: Option<StatusUpdateId>,
) -> Result<Option<String>> {
let updates_array = self
.get_webxdc_status_updates(instance_msg_id, status_update_id)
.await?;
if updates_array == "[]" {
if json.is_empty() {
Ok(None)
} else {
Ok(Some(format!(r#"{{"updates":{}}}"#, updates_array)))
Ok(Some(format!(r#"{{"updates":[{}]}}"#, json)))
}
}
}
@@ -483,7 +548,11 @@ impl Message {
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use async_std::fs::File;
use async_std::io::WriteExt;
use crate::chat::{
add_contact_to_chat, create_group_chat, forward_msgs, send_msg, send_text_msg, ChatId,
ProtectionStatus,
@@ -492,8 +561,8 @@ mod tests {
use crate::contact::Contact;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use async_std::fs::File;
use async_std::io::WriteExt;
use super::*;
#[allow(clippy::assertions_on_constants)]
#[async_std::test]
@@ -511,35 +580,35 @@ mod tests {
assert!(
!t.is_webxdc_file(
"bad-ext-no-zip.txt",
include_bytes!("../test-data/message/issue_523.txt")
Cursor::new(include_bytes!("../test-data/message/issue_523.txt"))
)
.await?
);
assert!(
!t.is_webxdc_file(
"bad-ext-good-zip.txt",
include_bytes!("../test-data/webxdc/minimal.xdc")
Cursor::new(include_bytes!("../test-data/webxdc/minimal.xdc"))
)
.await?
);
assert!(
!t.is_webxdc_file(
"good-ext-no-zip.xdc",
include_bytes!("../test-data/message/issue_523.txt")
Cursor::new(include_bytes!("../test-data/message/issue_523.txt"))
)
.await?
);
assert!(
!t.is_webxdc_file(
"good-ext-no-index-html.xdc",
include_bytes!("../test-data/webxdc/no-index-html.xdc")
Cursor::new(include_bytes!("../test-data/webxdc/no-index-html.xdc"))
)
.await?
);
assert!(
t.is_webxdc_file(
"good-ext-good-zip.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc")
Cursor::new(include_bytes!("../test-data/webxdc/minimal.xdc"))
)
.await?
);
@@ -635,8 +704,9 @@ mod tests {
.await?;
assert!(!instance.is_forwarded());
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":42,"info":"foo","summary":"bar"}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"info":"foo","summary":"bar","serial":1,"max_serial":1}]"#
);
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
let info = Message::load_from_db(&t, instance.id)
@@ -649,7 +719,11 @@ mod tests {
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
let instance2 = t.get_last_msg_in(chat_id).await;
assert!(instance2.is_forwarded());
assert_eq!(t.get_webxdc_status_updates(instance2.id, None).await?, "[]");
assert_eq!(
t.get_webxdc_status_updates(instance2.id, StatusUpdateSerial(0))
.await?,
"[]"
);
assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); // two instances, only one info
let info = Message::load_from_db(&t, instance2.id)
.await?
@@ -712,7 +786,8 @@ mod tests {
.await
.is_err());
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
"[]"
);
@@ -723,8 +798,9 @@ mod tests {
.await
.is_ok());
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":42}]"#
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"serial":1,"max_serial":1}]"#
);
Ok(())
@@ -746,14 +822,16 @@ mod tests {
t.send_webxdc_status_update(instance.id, r#"{"payload": 42}"#, "descr")
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":42}]"#.to_string()
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"serial":1,"max_serial":1}]"#.to_string()
);
// set_draft(None) deletes the message without the need to simulate network
chat_id.set_draft(&t, None).await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
"[]".to_string()
);
assert_eq!(
@@ -772,9 +850,13 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let mut instance = send_webxdc_instance(&t, chat_id).await?;
assert_eq!(t.get_webxdc_status_updates(instance.id, None).await?, "[]");
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
"[]"
);
let id = t
let update_id1 = t
.create_status_update_record(
&mut instance,
"\n\n{\"payload\": {\"foo\":\"bar\"}}\n",
@@ -782,8 +864,9 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo":"bar"}}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
assert!(t
@@ -795,15 +878,12 @@ mod tests {
.await
.is_err());
assert_eq!(
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
let id = t
let update_id2 = t
.create_status_update_record(
&mut instance,
r#"{"payload" : { "foo2":"bar2"}}"#,
@@ -811,19 +891,20 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo2":"bar2"}}]"#
t.get_webxdc_status_updates(instance.id, update_id1).await?,
r#"[{"payload":{"foo2":"bar2"},"serial":2,"max_serial":2}]"#
);
t.create_status_update_record(&mut instance, r#"{"payload":true}"#, 1640178619)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":{"foo2":"bar2"}},
{"payload":true}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3},
{"payload":{"foo2":"bar2"},"serial":2,"max_serial":3},
{"payload":true,"serial":3,"max_serial":3}]"#
);
let id = t
let _update_id3 = t
.create_status_update_record(
&mut instance,
r#"{"payload" : 1, "sender": "that is not used"}"#,
@@ -831,17 +912,9 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":1}]"#
);
// TODO: legacy `PAYLOAD` support should be deleted soon
let id = t
.create_status_update_record(&mut instance, r#"{"foo" : 1}"#, 1640178619)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo":1}}]"#
t.get_webxdc_status_updates(instance.id, update_id2).await?,
r#"[{"payload":true,"serial":3,"max_serial":4},
{"payload":1,"serial":4,"max_serial":4}]"#
);
Ok(())
@@ -873,8 +946,9 @@ mod tests {
t.receive_status_update(instance.id, r#"{"updates":[{"payload":{"foo":"bar"}}]}"#)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
t.receive_status_update(
@@ -883,10 +957,11 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":42},
{"payload":23}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3},
{"payload":42,"serial":2,"max_serial":3},
{"payload":23,"serial":3,"max_serial":3}]"#
);
t.receive_status_update(
@@ -895,11 +970,12 @@ mod tests {
)
.await?; // ignore members that may be added in the future
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":42},
{"payload":23},
{"payload":"ok"}]"#
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4},
{"payload":42,"serial":2,"max_serial":4},
{"payload":23,"serial":3,"max_serial":4},
{"payload":"ok","serial":4,"max_serial":4}]"#
);
Ok(())
@@ -911,15 +987,7 @@ mod tests {
.get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. }))
.await;
match event {
EventType::WebxdcStatusUpdate {
msg_id,
status_update_id,
} => {
assert_eq!(
t.get_webxdc_status_updates(msg_id, Some(status_update_id))
.await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
EventType::WebxdcStatusUpdate(msg_id) => {
assert_eq!(msg_id, instance_id);
}
_ => unreachable!(),
@@ -964,9 +1032,9 @@ mod tests {
assert!(sent2.payload().contains("descr text"));
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, None)
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"}}]"#
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
alice
@@ -979,10 +1047,10 @@ mod tests {
.unwrap();
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, None)
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":{"snipp":"snapp"}}]"#
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"#
);
// Bob receives all messages
@@ -998,8 +1066,9 @@ mod tests {
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}}]"#
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
// Alice has a second device and also receives messages there
@@ -1091,9 +1160,10 @@ mod tests {
assert!(sent1.payload().contains("status-update.json"));
assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#));
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":42}]"# // 'info: "i"' ignored as sent in draft mode
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
{"payload":42,"serial":2,"max_serial":2}]"# // 'info: "i"' ignored as sent in draft mode
);
Ok(())
@@ -1411,9 +1481,9 @@ sth_for_the = "future""#
assert!(info_msg.quoted_message(&alice).await?.is_none());
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, None)
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":"sth. else","info":"this appears in-chat"}]"#
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
);
// Bob receives all messages
@@ -1431,8 +1501,9 @@ sth_for_the = "future""#
assert_eq!(info_msg.parent(&bob).await?.unwrap().id, bob_instance.id);
assert!(info_msg.quoted_message(&bob).await?.is_none());
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":"sth. else","info":"this appears in-chat"}]"#
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
);
// Alice has a second device and also receives the info message there
@@ -1455,9 +1526,9 @@ sth_for_the = "future""#
assert!(info_msg.quoted_message(&alice2).await?.is_none());
assert_eq!(
alice2
.get_webxdc_status_updates(alice2_instance.id, None)
.get_webxdc_status_updates(alice2_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":"sth. else","info":"this appears in-chat"}]"#
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
);
Ok(())

View File

@@ -16,7 +16,7 @@ Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162](https://
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)
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/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))
User and chat colors | [XEP-0392](https://xmpp.org/extensions/xep-0392.html): Consistent Color Generation

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