Compare commits

..

65 Commits

Author SHA1 Message Date
link2xt
bd81ecdb5d Add option to force E2EE encryption preference
Enabling this option ignores Autocrypt recommendation taking others
encryption preferences into account and overrides it with our own
encryption preference when possible.

This is similar to user always manually enabling/disabling encryption
manually in a classic Autocrypt-capable MUA UI whenever the control is
not disabled.

The goal is to allow encrypting responses to MUAs which can send
Autocrypt header but don't support setting encryption preference, such
as Thunderbird 91.
2021-11-28 16:32:18 +00:00
bjoern
7f97768c56 prepare 1.68 (#2844)
* update changelog for 1.68.0

* bump version to 1.68.0
2021-11-28 12:22:14 +01:00
dependabot[bot]
ecd548a7aa Merge pull request #2829 from deltachat/dependabot/cargo/libc-0.2.108 2021-11-27 19:03:56 +00:00
dependabot[bot]
62efb0795b Merge pull request #2830 from deltachat/dependabot/cargo/anyhow-1.0.48 2021-11-27 19:03:22 +00:00
bjoern
53f042ee08 tweak qr svg (#2842)
* repl: allow groupname arguments with more than one word (came over that when testing qr codes)

* calcualte text-size from the real number of lines

* shift text and watermark apart when text get longer

* make clippy happy
2021-11-27 18:57:46 +01:00
link2xt
6ce97bd0cd Merge fixes for chat assignment when forwarding messages
GitHub PR #2843
2021-11-27 00:00:00 +00:00
link2xt
c29149e74c Add group forwarding test 2021-11-27 00:00:00 +00:00
link2xt
487f7593ce Reset In-Reply-To when forwarding a message 2021-11-27 00:00:00 +00:00
link2xt
6b3b33d2a0 Test forwarded quoted messages 2021-11-27 00:00:00 +00:00
link2xt
2d70ccc2bf Do not return a quoted message for forwarded messages
For forwarded messages, parent message is not a quoted message.
2021-11-27 00:00:00 +00:00
link2xt
e90fc9504a Test get_parent_message 2021-11-27 00:00:00 +00:00
link2xt
5108314c03 Do not return trashed messages from get_rfc724_mid_in_list
This function is used to lookup the chat by `References` and
`In-Reply-To` header, so it does not make sense to return trashed
message when there is another non-trashed message in one of these
headers with a real chat ID.
2021-11-27 00:00:00 +00:00
Hocuri
f2b86a1c0f Update aeap-mvp.rst 2021-11-25 16:18:30 +01:00
Hocuri
9583d41446 Add a draft how an AEAP MVP could look 2021-11-25 16:18:30 +01:00
dependabot[bot]
e594064f93 cargo: bump anyhow from 1.0.47 to 1.0.48
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.47 to 1.0.48.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.47...1.0.48)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-25 14:30:59 +00:00
bjoern
7c52fd95ec prepare 1.67 (#2838)
* update changelog for 1.67.0

* bump version to 1.67.0
2021-11-25 15:29:10 +01:00
Simon Laux
416bf3a829 generate qr code svg (#2815)
* generate qr code svg prototype

* qr code for groups
fix formatting

* - letter avatar in qrcode
- escape xml in userinput (display/groupname)
- fix "Me" display name
- merge import declarations

* remove dot at the end of VerifyContactQRDescription

* if addr == displayname, show only one of them

Especially useful for yggmail accounts without usernames,
because the text would overflow otherwise.

* use real clipPath for rounded avatar

* - center avatar text better (dominant-baseline)
- add "sans-serif" to font fallback for text if arial is missing

* make corner always blue

* add [logo + "get.delta.chat"] footer to qrcode

* Update deltachat-ffi/deltachat.h

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

* Apply suggestions from code review

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

* new card design
- add stockstrings
- update changelog

* make qrcode pixels also #f2f2f2 instead of full white

* rename VERIFY_CONTACT_QR_DESC to SETUP_CONTACT
make footer text a tiny bit darker upon r10s's request

* avoid using  which is a doxygen command

* point out that one will join a group (this is still shorted and was also suggested in recent chats)

* add option to generate qr-code-svg to repl tool

* use same font-family in text and footer

* thinner card border

* remove superfluous <tspan> from footer to make color tweaking easier

* move font-weight to style, ios renderer does not pick it up from attribute; remove default font attributes not used consequently

* make get.delta.chat more visible

* align properly using dominant-baseline=central and alignment-baseline=middle, this makes things nice on all systems but android (before, ios was wrong and all others not 100% aligned as font metrics are ignored) (android needs a subsequent improvement)

Co-authored-by: bjoern <r10s@b44t.com>
2021-11-24 23:23:01 +01:00
dependabot[bot]
cc78347293 Merge pull request #2834 from deltachat/dependabot/cargo/futures-0.3.18 2021-11-23 23:22:46 +00:00
dependabot[bot]
27e4ae992a cargo: bump futures from 0.3.17 to 0.3.18
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.17 to 0.3.18.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.17...0.3.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-23 21:15:34 +00:00
bjoern
a1767dc153 prepare 1.66 (#2831)
* update changelog for 1.66.0

* bump version to 1.66.0

* also mention Contact.last_seen python api
2021-11-23 11:40:15 +01:00
dependabot[bot]
eb610c27bf cargo: bump libc from 0.2.107 to 0.2.108
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.107 to 0.2.108.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.107...0.2.108)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 21:11:56 +00:00
link2xt
016fb2ceb2 Remove indexmap dependency
`indexmap` is a large dependency (4K SLoC) containing `unsafe` code.

Contact IDs are now passed around as a Vec<u32> or &[u32].

QUOTA roots are now sorted by name instead of perserving original order.
2021-11-22 16:40:06 +01:00
link2xt
5c571520a0 contact: use last_seen column
It was there since the C core, labeled with "/* last_seen is for
future use */" but never actually used. The comment was lost during
the translation from C to Rust.
2021-11-21 21:14:17 +03:00
link2xt
ddefd2cf09 python: add cutil.from_optional_dc_charpointer()
`cutil.from_dc_charpointer()` is guaranteed to return `str`, while
`cutil.from_optional_dc_charpointer()` may return `None` if C function
returns `NULL`.
2021-11-21 20:00:29 +03:00
dependabot[bot]
30a3eeece8 cargo: bump strum_macros from 0.22.0 to 0.23.0
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.22.0 to 0.23.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>
2021-11-20 12:20:36 +01:00
dependabot[bot]
5919388588 cargo: bump libc from 0.2.106 to 0.2.107
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.106 to 0.2.107.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.106...0.2.107)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-20 12:16:18 +01:00
dependabot[bot]
2cc738f481 cargo: bump strum from 0.22.0 to 0.23.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.22.0 to 0.23.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>
2021-11-20 11:44:57 +01:00
dependabot[bot]
babd405928 cargo: bump serde_json from 1.0.69 to 1.0.71
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.69 to 1.0.71.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.69...v1.0.71)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-20 11:44:21 +01:00
link2xt
e885857875 Remove pretty_assertions dependency
It was only used in two places. Rather than adding `use
pretty_assertions::*` everywhere, it's easier to remove, and it
removes two additional dependency crates.
2021-11-20 11:42:52 +01:00
link2xt
a1d57a2645 Disable unnecessary proptest features 2021-11-20 11:42:52 +01:00
link2xt
a28aecd4d1 Update sqlite dependencies 2021-11-20 11:42:52 +01:00
link2xt
a3f1ff2827 Disable xattr feature on async-tar
This removes `xattr` dependency

We only care about backed up file contents, not attributes.
2021-11-20 11:42:52 +01:00
link2xt
c4d1a639b0 Remove itertools dependency
Collecting into Vec of &str and joining may even be faster according
to benchmarks:
https://gist.github.com/green-s/fbd0d374b290781ac9b3f8ff03e3245d
2021-11-20 11:42:52 +01:00
dependabot[bot]
8732b7a55c cargo: bump anyhow from 1.0.45 to 1.0.47
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.45 to 1.0.47.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.45...1.0.47)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-20 11:42:34 +01:00
Hocuri
1b9148f28e Add section comments to get_connectivity_html() (#2807) 2021-11-18 10:13:47 +01:00
Hocuri
e0129c3b43 Don't set draft after creating group (#2805)
* Don't set draft after creating group

* Remove DC_STR_NEWGROUPDRAFT, add note to Changelog

* Fix the (Rust) test

* Also fix python test
2021-11-16 10:44:30 +01:00
link2xt
60d41022ea scripts: switch from python 3.6 to python 3.7
PEP 562 (__getattr__ in deltachat.const module) is implemented only since python 3.7
2021-11-14 00:00:00 +00:00
bjoern
dd4f2ac671 prepare 1.65 (#2812)
* update changelog for 1.65.0

* bump version to 1.65.0
2021-11-15 14:51:28 +01:00
bjoern
eebb2a3b68 do not assume 'no ephemeral timer' on partial downloads (#2811)
* do not assume 'no ephemeral timer' on partial downloads

* add a test to check that ephemeral timers are not disabled on partial downloads
2021-11-15 11:45:06 +01:00
link2xt
0d62069b67 python: add mypy support and some type hints
`deltachat.const` module now defines `__getattr__` and `__dir__` as
suggested by https://www.python.org/dev/peps/pep-0562/
mypy detects that `__getattr__` is defined and does not show errors
for `DC_*` constants which cannot be detected statically.
mypy is added to `tox.ini`, so type check can be run with `tox -e mypy`.
2021-11-14 11:06:44 +03:00
link2xt
56cf2e6596 Replace error! on verification failure with warn!
A message is added into 1:1 chat anyway, and user does not know what `StockMessage::ContactNotVerified` means.
2021-11-14 02:02:23 +03:00
Simon Laux
59bd5481b9 fix 1.61.0 changelog (#2806) 2021-11-13 22:06:29 +01:00
Hocuri
6c8da526a0 Fix: Only show the "Cannot login" device message if it's actually authentication that failed (#2804)
Generally we could also just remove the device message as we have the
connectivity view, OTOH if you are not using DC a lot, it may be useful
to be notified without opening DC.

And apart from this one bug it's working fine.
2021-11-13 19:20:01 +01:00
link2xt
13bc8b78d7 python: enable isolated build in tox.ini
This makes tox install build system configured in pyproject.toml
according to PEP 518 rather than assuming setuptools.
2021-11-13 03:45:58 +00:00
link2xt
c7c68094d9 setup.py: restore compatibility with setup.py sdist command
Otherwise source package with version 0.0.0 is created.
2021-11-12 23:41:17 +00:00
bjoern
84f54b10dc prepare 1.64 (#2802)
* update changelog for 1.64

* bump version to 1.64.0
2021-11-11 16:45:30 +01:00
bjoern
cebc9e3e91 add 'waiting for being added to the group' only for group-joins (#2797)
* add 'waiting for being added to the group' only for group-joins, not for setup-contact

* add a comment why the message is not added on setup-contact
2021-11-07 20:30:55 +01:00
link2xt
1379f8a055 Factor apply_group_changes out of create_or_lookup_group
`apply_group_changes` is executed regardless of whether the group is
created via `create_or_lookup_group` or found via
`lookup_chat_by_reply`. This change removes the need for
`lookup_chat_by_reply` to return `None` when group ID exists in the
database to let `create_or_lookup_group` run. As a side effect of this
Delta Chat replies to ad hoc groups are now correctly assigned to
chats when there are multiple groups with the same group ID, such as
ad hoc groups with the same member lists.
2021-11-07 18:34:44 +03:00
bjoern
53d049e5f5 prepare 1.63 (#2796)
* update changelog for 1.63

* bump version to 1.63.0
2021-11-06 21:00:54 +01:00
bjoern
4968f72dfb fix permanently hiding of one-to-one chats after secure-join (#2791)
* test one-to-one chats on setup-contact/secure-join

only one chat is created after scanning a QR code:

- on setup-contact, one-to-ones are created on both sided

- on secure-join, the joined group chat is created;
  one-to-ones are not created intitally,
  but should become visible on receiving messages

* make sure, Alice creates the chat with Bob on setup-contact

not totally sure if that change in #2508 was on-purpose,
however, all yet released versions
did create the one-to-one chat also on the Inviter's (Alice) side,
so, let's stay with that,
i do not see many reasons to change that.

* unblock hidden (Blocked::Yes) one-to-one chats

one-to-one chats may be hidden by secure-join,
in case someone later writes a message to it
(not unlikely), the chat needs to be shown.

before, messages are just not shown,
the corresponding chat did not appear.

the 'Blocked' wording of a 'Chat' must not be mixed with the
'Blocking' of a contact. 'Chat-Blocking' is mostly a visibility thing,
that may change as messages come in.

this change should not affect _really_ blocked contacts -
they are filtered out already before
and their messages are usually not even downloaded.
also, before allow_creation is checked,
that may disallow chat creation for show_emails reasons.

all in all, it just does the same
as if the user has manualy deleted the chat before and it would be created.

* simplify test
2021-11-06 18:46:10 +01:00
dependabot[bot]
b24a0ed8fd Merge pull request #2794 from deltachat/dependabot/cargo/serde_json-1.0.69 2021-11-05 23:29:32 +00:00
dependabot[bot]
c810347c7c cargo: bump serde_json from 1.0.68 to 1.0.69
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.68 to 1.0.69.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.68...v1.0.69)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-05 21:13:20 +00:00
dependabot[bot]
cf8c4142c7 Merge pull request #2784 from deltachat/dependabot/cargo/libc-0.2.106 2021-11-05 19:35:21 +00:00
dependabot[bot]
f71901b5f9 Merge pull request #2786 from deltachat/dependabot/cargo/backtrace-0.3.63 2021-11-05 19:33:54 +00:00
dependabot[bot]
4419f9c4e7 Merge pull request #2789 from deltachat/dependabot/cargo/anyhow-1.0.45 2021-11-05 19:33:02 +00:00
dependabot[bot]
4e5982b682 cargo: bump anyhow from 1.0.44 to 1.0.45
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.44 to 1.0.45.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.44...1.0.45)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-02 21:14:41 +00:00
bjoern
39e1510e64 add dc_get_last_error() (#2788)
* add dc_get_last_error()

* make clippy happy

* simplify block_on() call
2021-11-02 22:09:04 +01:00
dependabot[bot]
64206160cc Merge pull request #2785 from deltachat/dependabot/cargo/surf-2.3.2 2021-11-01 22:56:28 +00:00
dependabot[bot]
6376659348 cargo: bump backtrace from 0.3.62 to 0.3.63
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.62 to 0.3.63.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.62...0.3.63)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 21:17:04 +00:00
dependabot[bot]
43b2a4ad27 cargo: bump surf from 2.3.1 to 2.3.2
Bumps [surf](https://github.com/http-rs/surf) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/http-rs/surf/releases)
- [Changelog](https://github.com/http-rs/surf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/http-rs/surf/compare/v2.3.1...v2.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 21:16:52 +00:00
dependabot[bot]
eaf06bb239 cargo: bump libc from 0.2.105 to 0.2.106
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.105 to 0.2.106.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.105...0.2.106)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 21:16:34 +00:00
link2xt
5e26b5bfdc Merge pull request #2743 from deltachat/improve-gossip
Optimize Autocrypt gossip
2021-11-01 02:01:51 +03:00
link2xt
60fbb6df5a Add python test for gossip optimization 2021-10-31 22:39:05 +00:00
link2xt
3e60ee9d3e python: use datetime.fromtimestamp() instead of datetime.utcfromtimestamp()
utcfromtimestamp() is not recommended by the official documentation,
because many methods, including timestamp(), work incorrectly with
"naive" datetimes returned by utcfromtimestamp().
2021-10-31 22:39:05 +00:00
link2xt
54e79409e6 Optimize Autocrypt gossip
Update gossiped_timestamp when someone else sends autocrypt gossip in
the group, so we postpone sending gossip again ourselves.

- Warn about failures to parse Autocrypt-Gossip header
- Move gossip-related methods into ChatId impl
- Fix a "gossi_pp_ed" typo
2021-11-01 01:17:51 +03:00
58 changed files with 1892 additions and 762 deletions

View File

@@ -150,4 +150,4 @@ jobs:
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,doc,py3
run: tox -e lint,mypy,doc,py3

View File

@@ -1,5 +1,66 @@
# Changelog
## 1.68.0
### Fixes
- fix chat assignment when forwarding #2843
- fix layout issues with the generated QR code svg #2842
## 1.67.0
### API changes
- `dc_get_securejoin_qr_svg(chat_id)` added #2815
- added stock-strings `DC_STR_SETUP_CONTACT_QR_DESC` and `DC_STR_SECURE_JOIN_GROUP_QR_DESC`
## 1.66.0
### API changes
- `dc_contact_get_last_seen()` added #2823
- python: `Contact.last_seen` added #2823
- removed `DC_STR_NEWGROUPDRAFT`, we don't set draft after creating group anymore #2805
### Changes
- python: add cutil.from_optional_dc_charpointer() #2824
- refactorings #2807 #2822 #2825
## 1.65.0
### Changes
- python: add mypy support and some type hints #2809
### Fixes
- do not disable ephemeral timer when downloading a message partially #2811
- apply existing ephemeral timer also to partially downloaded messages;
after full download, the ephemeral timer starts over #2811
- replace user-visible error on verification failure with warning;
the error is logged to the corresponding chat anyway #2808
## 1.64.0
### Fixes
- add 'waiting for being added to the group' only for group-joins,
not for setup-contact #2797
- prioritize In-Reply-To: and References: headers over group IDs when assigning
messages to chats to fix incorrect assignment of Delta Chat replies to
classic email threads #2795
## 1.63.0
### API changes
- `dc_get_last_error()` added #2788
### Changes
- Optimize Autocrypt gossip #2743
### Fixes
- fix permanently hiding of one-to-one chats after secure-join #2791
## 1.62.0
### API Changes
@@ -28,10 +89,10 @@
## 1.61.0
### API Changes
- download-on-demand added: `dc_msg_get_download_status()`, `dc_download_full_msg()`
- download-on-demand added: `dc_msg_get_download_state()`, `dc_download_full_msg()`
and `download_limit` config option #2631 #2696
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
- allow ui-specific configs: `dc_set_ui_config()` and `dc_get_ui_config()` #2672
- allow ui-specific configs using `ui.`-prefix in key (`dc_set_config(context, "ui.*", value)`) #2672
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
- emit warnings and errors from account manager with account-id 0 #2712

233
Cargo.lock generated
View File

@@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.16.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
@@ -114,9 +114,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.44"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
[[package]]
name = "arrayvec"
@@ -372,7 +372,6 @@ dependencies = [
"libc",
"pin-project",
"redox_syscall",
"xattr",
]
[[package]]
@@ -423,9 +422,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.62"
version = "0.3.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091bcdf2da9950f96aa522681ce805e6857f6ca8df73833d35736ab2dc78e152"
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
dependencies = [
"addr2line",
"cc",
@@ -469,21 +468,6 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bit-set"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitfield"
version = "0.13.2"
@@ -722,7 +706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"bitflags",
"textwrap",
"textwrap 0.11.0",
"unicode-width",
]
@@ -1072,7 +1056,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.62.0"
version = "1.68.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1100,8 +1084,6 @@ dependencies = [
"hex",
"humansize",
"image",
"indexmap",
"itertools",
"kamadak-exif",
"lettre_email",
"libc",
@@ -1114,9 +1096,9 @@ dependencies = [
"once_cell",
"percent-encoding",
"pgp",
"pretty_assertions",
"pretty_env_logger",
"proptest",
"qrcodegen",
"quick-xml",
"r2d2",
"r2d2_sqlite",
@@ -1135,7 +1117,9 @@ dependencies = [
"strum",
"strum_macros",
"surf",
"tagger",
"tempfile",
"textwrap 0.14.2",
"thiserror",
"toml",
"url",
@@ -1152,7 +1136,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.62.0"
version = "1.68.0"
dependencies = [
"anyhow",
"async-std",
@@ -1201,12 +1185,6 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "diff"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]]
name = "digest"
version = "0.9.0"
@@ -1573,9 +1551,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
dependencies = [
"futures-channel",
"futures-core",
@@ -1588,9 +1566,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
dependencies = [
"futures-core",
"futures-sink",
@@ -1598,15 +1576,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
[[package]]
name = "futures-executor"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
dependencies = [
"futures-core",
"futures-task",
@@ -1615,9 +1593,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
[[package]]
name = "futures-lite"
@@ -1636,12 +1614,10 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
dependencies = [
"autocfg 1.0.1",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
@@ -1649,23 +1625,22 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
[[package]]
name = "futures-task"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
[[package]]
name = "futures-util"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
dependencies = [
"autocfg 1.0.1",
"futures-channel",
"futures-core",
"futures-io",
@@ -1675,8 +1650,6 @@ dependencies = [
"memchr",
"pin-project-lite",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
"slab",
]
@@ -1734,9 +1707,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.25.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
checksum = "81a03ce013ffccead76c11a15751231f777d9295b845cc1266ed4d34fcbd7977"
[[package]]
name = "gloo-timers"
@@ -1959,16 +1932,6 @@ dependencies = [
"nom 6.1.2",
]
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
]
[[package]]
name = "infer"
version = "0.2.3"
@@ -2104,9 +2067,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.105"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
[[package]]
name = "libm"
@@ -2116,9 +2079,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]]
name = "libsqlite3-sys"
version = "0.22.2"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2"
dependencies = [
"cc",
"pkg-config",
@@ -2517,15 +2480,6 @@ dependencies = [
"syn",
]
[[package]]
name = "output_vt100"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
dependencies = [
"winapi",
]
[[package]]
name = "packed_simd"
version = "0.3.3"
@@ -2741,18 +2695,6 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "pretty_assertions"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc"
dependencies = [
"ansi_term",
"ctor",
"diff",
"output_vt100",
]
[[package]]
name = "pretty_env_logger"
version = "0.4.0"
@@ -2793,12 +2735,6 @@ version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro-nested"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
version = "1.0.32"
@@ -2814,7 +2750,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
dependencies = [
"bit-set",
"bitflags",
"byteorder",
"lazy_static",
@@ -2824,10 +2759,14 @@ dependencies = [
"rand_chacha 0.3.1",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
]
[[package]]
name = "qrcodegen"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135e6754eed8ca897dd70584d895e72e36860b3e163b6bcedce48571cbaef343"
[[package]]
name = "quick-error"
version = "1.2.3"
@@ -2877,9 +2816,9 @@ dependencies = [
[[package]]
name = "r2d2_sqlite"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d24607049214c5e42d3df53ac1d8a23c34cc6a5eefe3122acb2c72174719959"
checksum = "54ca3c9468a76fc2ad724c486a59682fc362efeac7b18d1c012958bc19f34800"
dependencies = [
"r2d2",
"rusqlite",
@@ -3113,9 +3052,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.25.3"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57adcf67c8faaf96f3248c2a7b419a0dbc52ebe36ba83dd57fe83827c1ea4eb3"
checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7"
dependencies = [
"bitflags",
"fallible-iterator",
@@ -3157,16 +3096,10 @@ dependencies = [
]
[[package]]
name = "rusty-fork"
version = "0.3.0"
name = "rustversion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "rustyline"
@@ -3324,9 +3257,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.68"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
dependencies = [
"itoa",
"ryu",
@@ -3457,6 +3390,12 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "socket2"
version = "0.3.19"
@@ -3590,19 +3529,20 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "strum"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb"
[[package]]
name = "strum_macros"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb"
checksum = "00ad150e9d51e33e8142984f577662c1324d49f3be45ed37bac8645fdcbe0fe5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
@@ -3614,10 +3554,11 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "surf"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa"
checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
dependencies = [
"async-native-tls",
"async-std",
"async-trait",
"cfg-if 1.0.0",
@@ -3656,6 +3597,12 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tagger"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c933e626196d509b053f49573e35ad237c0ff6a36c23c1aa61ffeab49251d24f"
[[package]]
name = "tap"
version = "1.0.1"
@@ -3694,6 +3641,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.30"
@@ -3891,6 +3849,15 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
dependencies = [
"regex",
]
[[package]]
name = "unicode-normalization"
version = "0.1.19"
@@ -3985,15 +3952,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
@@ -4186,15 +4144,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "xattr"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
dependencies = [
"libc",
]
[[package]]
name = "zeroize"
version = "1.3.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.62.0"
version = "1.68.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -22,7 +22,7 @@ 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-tar = "0.4"
async-tar = { version = "0.4", default-features=false }
async-trait = "0.1"
backtrace = "0.3"
base64 = "0.13"
@@ -36,8 +36,6 @@ 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"] }
indexmap = "1.7"
itertools = "0.10"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
@@ -53,10 +51,10 @@ 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.18"
r2d2_sqlite = "0.19"
rand = "0.7"
regex = "1.5"
rusqlite = "0.25"
rusqlite = "0.26"
rust-hsluv = "0.1"
rustyline = { version = "9.0", optional = true }
sanitize-filename = "0.3"
@@ -66,8 +64,8 @@ sha-1 = "0.9"
sha2 = "0.9"
smallvec = "1"
stop-token = "0.6"
strum = "0.22"
strum_macros = "0.22"
strum = "0.23"
strum_macros = "0.23"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
@@ -75,6 +73,9 @@ url = "2"
uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "3.2.1"
textwrap = "0.14.2"
[dev-dependencies]
ansi_term = "0.12.0"
@@ -82,9 +83,8 @@ async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.12"
log = "0.4"
pretty_assertions = "1.0"
pretty_env_logger = "0.4"
proptest = "1"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
[workspace]

View File

@@ -0,0 +1,10 @@
<text
xml:space="preserve"
style="font-weight:bold;font-size:24.4118px;line-height:1.25;font-family:sans-serif;fill:#aaaaaa;fill-opacity:1;stroke:none;stroke-width:0.915439"
x="42.325161"
y="23.32255"
id="text72398">get.delta.chat</text>
<path
id="path84310"
style="opacity:0.25;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.915439"
d="M 17.13769,0.00129321 C 7.6753075,0.11650893 0,7.8915283 0,17.362467 c 0,9.47094 7.6753075,17.059745 17.13769,16.944599 8.99669,-0.03598 6.880074,-5.025654 16.824785,-0.405885 -5.447648,-8.510047 0.184241,-9.642482 0.311117,-16.955289 0,-9.4709395 -7.673512,-17.0597453 -17.135895,-16.94459879 z M 17.0769,4.9986797 c 1.84214,0 3.447355,0.253959 4.815003,0.7616693 1.381603,0.5076411 2.072253,1.207862 2.072253,2.0990711 0,0.4286855 -0.167495,0.7836052 -0.50242,1.0656242 -0.334921,0.2819844 -0.724544,0.4237724 -1.171121,0.4237724 -0.641952,0 -1.396532,-0.3909376 -2.261778,-1.169353 C 19.14963,7.3898036 18.402555,6.83791 17.788507,6.5220182 17.188416,6.1950547 16.484552,6.0321266 15.675129,6.0321266 c -1.032717,0 -1.883352,0.1854523 -2.553215,0.5578447 -0.655913,0.372254 -0.98517,0.8460916 -0.98517,1.4214436 0,0.5414792 0.272815,1.0495355 0.817093,1.5233385 0.544275,0.4738026 1.946291,1.3367446 4.207097,2.5889976 2.414319,1.342419 4.117377,2.390985 5.108232,3.146807 1.004795,0.755857 1.821505,1.675853 2.449514,2.758846 0.628002,1.082993 0.942253,2.227607 0.942253,3.434674 0,2.120834 -0.929555,3.993314 -2.785656,5.617786 -1.84214,1.613228 -3.99694,2.41915 -6.467082,2.41915 -2.246845,0 -4.145607,-0.647976 -5.694677,-1.945312 -1.5490699,-1.297336 -2.3225722,-3.028063 -2.3225722,-5.194049 0,-2.087031 0.8506345,-3.83094 2.5532182,-5.229825 1.716541,-1.398884 3.824203,-2.245599 6.322256,-2.538897 -0.697774,-0.631749 -1.668763,-1.387225 -2.910816,-2.267155 -1.367648,-0.970199 -2.287914,-1.73045 -2.762402,-2.283243 -0.474491,-0.5640381 -0.711618,-1.1795944 -0.711618,-1.8451814 0,-0.9927581 0.572093,-1.7710351 1.716451,-2.3351077 1.144362,-0.5753173 2.636724,-0.8635642 4.478865,-0.8635642 z m 1.110327,10.3738083 c -4.005262,0.5302 -6.007576,2.75279 -6.007576,6.667322 0,2.01932 0.49495,3.587291 1.485805,4.704157 1.004806,1.116832 2.169696,1.675299 3.495479,1.675299 1.381602,0 2.520072,-0.535632 3.413229,-1.60738 0.893168,-1.082959 1.339187,-2.545264 1.339187,-4.384079 0,-2.662348 -1.242022,-5.013441 -3.726124,-7.055319 z" />

View File

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

View File

@@ -287,6 +287,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* To save traffic, however, the avatar is attached only as needed
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `e2ee_force` = 1=ignore encryption preferences of others,
* 0=use majority vote when deciding whether to encrypt (default).
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts (default)
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
@@ -2243,6 +2245,20 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
char* dc_get_securejoin_qr (dc_context_t* context, uint32_t chat_id);
/**
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
* See dc_get_securejoin_qr() for details about the contained QR code.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
* see dc_get_securejoin_qr() for details.
* @return SVG-Image with the QR code.
* On errors, an empty string is returned.
* The returned string must be released using dc_str_unref() after usage.
*/
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
@@ -2414,6 +2430,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
void dc_delete_all_locations (dc_context_t* context);
/**
* Get last error string.
*
* This is the same error string as logged via #DC_EVENT_ERROR,
* however, using this function avoids race conditions
* if the failing function is called in another thread than dc_get_next_event().
*
* @memberof dc_context_t
* @param context The context object.
* @return Last error or an empty string if there is no last error.
* NULL is never returned.
* The returned value must be released using dc_str_unref() after usage.
*/
char* dc_get_last_error (dc_context_t* context);
/**
* Release a string returned by another deltachat-core function.
* - Strings returned by any deltachat-core-function
@@ -4366,6 +4398,16 @@ uint32_t dc_contact_get_color (const dc_contact_t* contact);
*/
char* dc_contact_get_status (const dc_contact_t* contact);
/**
* Get the contact's last seen timestamp.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return Last seen timestamp.
* 0 on error or if the contact was never seen.
*/
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
/**
* Check if a contact is blocked.
*
@@ -5597,12 +5639,6 @@ void dc_event_unref(dc_event_t* event);
/// if nothing else is set by the dc_set_config()-option `selfstatus`.
#define DC_STR_STATUSLINE 13
/// "Hi, i've created the group %1$s for us."
///
/// Used as a draft text after group creation.
/// - %1$s will be replaced by the group name
#define DC_STR_NEWGROUPDRAFT 14
/// "Group name changed from %1$s to %2$s."
///
/// Used in status messages for group name changes.
@@ -6057,6 +6093,20 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_REPLIES 118
/// "Scan to chat with %1$s"
///
/// Subtitle for verification qrcode svg image generated by the core.
///
/// `%1$s` will be replaced by name and address of the inviter.
#define DC_STR_SETUP_CONTACT_QR_DESC 119
/// "Scan to join %1$s"
///
/// Subtitle for group join qrcode svg image generated by the core.
///
/// `%1$s` will be replaced with the group name.
#define DC_STR_SECURE_JOIN_GROUP_QR_DESC 120
/**
* @}
*/

View File

@@ -25,6 +25,7 @@ use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use async_std::sync::RwLock;
use async_std::task::{block_on, spawn};
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
@@ -2075,6 +2076,27 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
context: *mut dc_context_t,
chat_id: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to generate_verification_qr()");
return "".strdup();
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
} else {
Some(ChatId::new(chat_id))
};
block_on(get_securejoin_qr_svg(ctx, chat_id))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_join_securejoin(
context: *mut dc_context_t,
@@ -2206,6 +2228,16 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_last_error()");
return "".strdup();
}
let ctx = &*context;
block_on(ctx.get_last_error()).strdup()
}
// dc_array_t
pub type dc_array_t = dc_array::dc_array_t;
@@ -3534,6 +3566,16 @@ pub unsafe extern "C" fn dc_contact_get_status(contact: *mut dc_contact_t) -> *m
ffi_contact.contact.get_status().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) -> i64 {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_last_seen()");
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.last_seen()
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {

33
draft/aeap-mvp.rst Normal file
View File

@@ -0,0 +1,33 @@
AEAP MVP
========
Changes to the UIs
------------------
- The secondary self addresses (see below) are shown in the UI, but not editable.
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
Changes in the core
-------------------
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
- The key stays the same.
- No changes for 1:1 chats, there simply is a new one
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
Other
-----
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.

View File

@@ -423,6 +423,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
listblocked\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getqrsvg [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
joinqr <qr-content>\n\
@@ -754,7 +755,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"groupname" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <name> missing.");
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
chat::set_chat_name(
&context,
sel_chat.as_ref().unwrap().get_id(),
&format!("{} {}", arg1, arg2).trim(),
)
.await?;
println!("Chat name set");
}

View File

@@ -33,6 +33,8 @@ use rustyline::{
mod cmdline;
use self::cmdline::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use std::fs;
/// Event Handler
fn receive_event(event: EventType) {
@@ -224,8 +226,9 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 11] = [
const MISC_COMMANDS: [&str; 12] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
@@ -427,6 +430,20 @@ async fn handle_cmd(
io::stderr().write_all(&output.stderr).unwrap();
}
}
"getqrsvg" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {
fs::write(&file, svg)?;
println!("QR code svg written to: {:#?}", file);
}
Err(err) => {
bail!("Failed to get QR code svg: {}", err);
}
}
}
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {

19
python/mypy.ini Normal file
View File

@@ -0,0 +1,19 @@
[mypy]
[mypy-deltachat.capi.*]
ignore_missing_imports = True
[mypy-pluggy.*]
ignore_missing_imports = True
[mypy-cffi.*]
ignore_missing_imports = True
[mypy-imapclient.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
[mypy-_pytest.*]
ignore_missing_imports = True

View File

@@ -1,5 +1,6 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
root = ".."

View File

@@ -12,6 +12,7 @@ def main():
long_description=long_description,
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],

View File

@@ -19,9 +19,9 @@ except DistributionNotFound:
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name, val in vars(const).items():
for name in dir(const):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[val] = name
_DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer]

View File

@@ -8,13 +8,14 @@ import os
from array import array
from . import const
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
from .chat import Chat
from .message import Message
from .contact import Contact
from .tracker import ImexTracker, ConfigureTracker
from . import hookspec
from .events import EventThread
from typing import Union, Any, Dict, Optional, List, Generator
class MissingCredentials(ValueError):
@@ -28,7 +29,7 @@ class Account(object):
"""
MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True):
def __init__(self, db_path, os_name=None, logging=True) -> None:
""" initialize account object.
:param db_path: a path to the account database. The database
@@ -58,11 +59,11 @@ class Account(object):
hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_init(account=self)
def disable_logging(self):
def disable_logging(self) -> None:
""" disable logging. """
self._logging = False
def enable_logging(self):
def enable_logging(self) -> None:
""" re-enable logging. """
self._logging = True
@@ -73,12 +74,12 @@ class Account(object):
if self._logging:
self._pm.hook.ac_log_line(message=msg)
def _check_config_key(self, name):
def _check_config_key(self, name: str) -> None:
if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
name, self._configkeys))
def get_info(self):
def get_info(self) -> Dict[str, str]:
""" return dictionary of built config parameters. """
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
d = {}
@@ -105,19 +106,19 @@ class Account(object):
cursor += len(entry) + 1
log("")
def set_stock_translation(self, id, string):
def set_stock_translation(self, id: int, string: str) -> None:
""" set stock translation string.
:param id: id of stock string (const.DC_STR_*)
:param value: string to set as new transalation
:returns: None
"""
string = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, string)
bytestring = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
if res == 0:
raise ValueError("could not set translation string")
def set_config(self, name, value):
def set_config(self, name: str, value: Optional[str]) -> None:
""" set configuration values.
:param name: config key name (unicode)
@@ -125,16 +126,16 @@ class Account(object):
:returns: None
"""
self._check_config_key(name)
name = name.encode("utf8")
if name == b"addr" and self.is_configured():
namebytes = name.encode("utf8")
if namebytes == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.")
if value is not None:
value = value.encode("utf8")
valuebytes = value.encode("utf8")
else:
value = ffi.NULL
lib.dc_set_config(self._dc_context, name, value)
valuebytes = ffi.NULL
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
def get_config(self, name):
def get_config(self, name: str) -> str:
""" return unicode string value.
:param name: configuration key to lookup (eg "addr" or "mail_pw")
@@ -143,12 +144,12 @@ class Account(object):
"""
if name != "sys.config_keys":
self._check_config_key(name)
name = name.encode("utf8")
res = lib.dc_get_config(self._dc_context, name)
namebytes = name.encode("utf8")
res = lib.dc_get_config(self._dc_context, namebytes)
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr, public, secret):
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
@@ -160,7 +161,7 @@ class Account(object):
if res == 0:
raise Exception("Failed to set key")
def update_config(self, kwargs):
def update_config(self, kwargs: Dict[str, Any]) -> None:
""" update config values.
:param kwargs: name=value config settings for this account.
@@ -170,7 +171,7 @@ class Account(object):
for key, value in kwargs.items():
self.set_config(key, str(value))
def is_configured(self):
def is_configured(self) -> bool:
""" determine if the account is configured already; an initial connection
to SMTP/IMAP has been verified.
@@ -178,7 +179,7 @@ class Account(object):
"""
return True if lib.dc_is_configured(self._dc_context) else False
def set_avatar(self, img_path):
def set_avatar(self, img_path: Optional[str]) -> None:
"""Set self avatar.
:raises ValueError: if profile image could not be set
@@ -190,20 +191,18 @@ class Account(object):
assert os.path.exists(img_path), img_path
self.set_config("selfavatar", img_path)
def check_is_configured(self):
def check_is_configured(self) -> None:
""" Raise ValueError if this account is not configured. """
if not self.is_configured():
raise ValueError("need to configure first")
def get_latest_backupfile(self, backupdir):
def get_latest_backupfile(self, backupdir) -> Optional[str]:
""" return the latest backup file in a given directory.
"""
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
if res == ffi.NULL:
return None
return from_dc_charpointer(res)
return from_optional_dc_charpointer(res)
def get_blobdir(self):
def get_blobdir(self) -> str:
""" return the directory for files.
All sent files are copied to this directory if necessary.
@@ -211,15 +210,15 @@ class Account(object):
"""
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
def get_self_contact(self):
def get_self_contact(self) -> Contact:
""" return this account's identity as a :class:`deltachat.contact.Contact`.
:returns: :class:`deltachat.contact.Contact`
"""
return Contact(self, const.DC_CONTACT_ID_SELF)
def create_contact(self, obj, name=None):
""" create a (new) Contact or return an existing one.
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
"""create a (new) Contact or return an existing one.
Calling this method will always result in the same
underlying contact id. If there already is a Contact
@@ -236,13 +235,13 @@ class Account(object):
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj):
def get_contact(self, obj) -> Optional[Contact]:
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr)
def get_contact_addr_and_name(self, obj, name=None):
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
@@ -260,7 +259,7 @@ class Account(object):
name = displayname
return (name, addr)
def delete_contact(self, contact):
def delete_contact(self, contact: Contact) -> bool:
""" delete a Contact.
:param contact: contact object obtained
@@ -271,22 +270,23 @@ class Account(object):
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email):
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
""" get a contact for the email address or None if it's blocked or doesn't exist. """
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
if contact_id:
return self.get_contact_by_id(contact_id)
return None
def get_contact_by_id(self, contact_id):
""" return Contact instance or None.
def get_contact_by_id(self, contact_id: int) -> Contact:
""" return Contact instance or raise an exception.
:param contact_id: integer id of this contact.
:returns: None or :class:`deltachat.contact.Contact` instance.
:returns: :class:`deltachat.contact.Contact` instance.
"""
return Contact(self, contact_id)
def get_blocked_contacts(self):
def get_blocked_contacts(self) -> List[Contact]:
""" return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects.
@@ -297,8 +297,13 @@ class Account(object):
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
only_verified: bool = False,
) -> List[Contact]:
"""get a (filtered) list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
@@ -318,7 +323,7 @@ class Account(object):
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self):
def get_fresh_messages(self) -> Generator[Message, None, None]:
""" yield all fresh messages from all chats. """
dc_array = ffi.gc(
lib.dc_get_fresh_msgs(self._dc_context),
@@ -326,12 +331,17 @@ class Account(object):
)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat(self, obj):
def create_chat(self, obj) -> Chat:
""" Create a 1:1 chat with Account, Contact or e-mail address. """
return self.create_contact(obj).create_chat()
def create_group_chat(self, name, contacts=None, verified=False):
""" create a new group chat object.
def create_group_chat(
self,
name: str,
contacts: Optional[List[Contact]] = None,
verified: bool = False,
) -> Chat:
"""create a new group chat object.
Chats are unpromoted until the first message is sent.
@@ -347,7 +357,7 @@ class Account(object):
chat.add_contact(contact)
return chat
def get_chats(self):
def get_chats(self) -> List[Chat]:
""" return list of chats.
:returns: a list of :class:`deltachat.chat.Chat` objects.
@@ -364,17 +374,17 @@ class Account(object):
chatlist.append(Chat(self, chat_id))
return chatlist
def get_device_chat(self):
def get_device_chat(self) -> Chat:
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
def get_message_by_id(self, msg_id: int) -> Message:
""" return Message instance.
:param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance.
"""
return Message.from_db(self, msg_id)
def get_chat_by_id(self, chat_id):
def get_chat_by_id(self, chat_id: int) -> Chat:
""" return Chat instance.
:param chat_id: integer id of this chat.
:returns: :class:`deltachat.chat.Chat` instance.
@@ -386,19 +396,18 @@ class Account(object):
lib.dc_chat_unref(res)
return Chat(self, chat_id)
def mark_seen_messages(self, messages):
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
""" mark the given set of messages as seen.
:param messages: a list of message ids or Message instances.
"""
arr = array("i")
for msg in messages:
msg = getattr(msg, "id", msg)
arr.append(msg)
arr.append(getattr(msg, "id", msg))
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
def forward_messages(self, messages, chat):
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
""" Forward list of messages to a chat.
:param messages: list of :class:`deltachat.message.Message` object.
@@ -408,7 +417,7 @@ class Account(object):
msg_ids = [msg.id for msg in messages]
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
def delete_messages(self, messages):
def delete_messages(self, messages: List[Message]) -> None:
""" delete messages (local and remote).
:param messages: list of :class:`deltachat.message.Message` object.
@@ -466,7 +475,7 @@ class Account(object):
def imex(self, path, imex_cmd):
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
def initiate_key_transfer(self):
def initiate_key_transfer(self) -> str:
"""return setup code after a Autocrypt setup message
has been successfully sent to our own e-mail address ("self-sent message").
If sending out was unsuccessful, a RuntimeError is raised.
@@ -477,7 +486,7 @@ class Account(object):
raise RuntimeError("could not send out autocrypt setup message")
return from_dc_charpointer(res)
def get_setup_contact_qr(self):
def get_setup_contact_qr(self) -> str:
""" get/create Setup-Contact QR Code as ascii-string.
this string needs to be transferred to another DC account
@@ -527,7 +536,9 @@ class Account(object):
raise ValueError("could not join group")
return Chat(self, chat_id)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
def set_location(
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
) -> None:
"""set a new location. It effects all chats where we currently
have enabled location streaming.
@@ -571,7 +582,7 @@ class Account(object):
def get_connectivity(self):
return lib.dc_get_connectivity(self._dc_context)
def get_connectivity_html(self):
def get_connectivity_html(self) -> str:
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
def all_work_done(self):
@@ -621,7 +632,7 @@ class Account(object):
"""
lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure=False):
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
""" Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success
value for the configuration process.
@@ -634,11 +645,11 @@ class Account(object):
lib.dc_configure(self._dc_context)
return configtracker
def wait_shutdown(self):
def wait_shutdown(self) -> None:
""" wait until shutdown of this account has completed. """
self._shutdown_event.wait()
def stop_io(self):
def stop_io(self) -> None:
""" stop core IO scheduler if it is running. """
self.log("stop_ongoing")
self.stop_ongoing()
@@ -646,7 +657,7 @@ class Account(object):
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
def shutdown(self):
def shutdown(self) -> None:
""" shutdown and destroy account (stop callback thread, close and remove
underlying dc_context)."""
if self._dc_context is None:

View File

@@ -3,12 +3,13 @@
import mimetypes
import calendar
import json
from datetime import datetime
from datetime import datetime, timezone
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
from typing import Optional
class Chat(object):
@@ -17,20 +18,20 @@ class Chat(object):
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, account, id):
def __init__(self, account, id) -> None:
from .account import Account
assert isinstance(account, Account), repr(account)
self.account = account
self.id = id
def __eq__(self, other):
def __eq__(self, other) -> bool:
return self.id == getattr(other, "id", None) and \
self.account._dc_context == other.account._dc_context
def __ne__(self, other):
def __ne__(self, other) -> bool:
return not (self == other)
def __repr__(self):
def __repr__(self) -> str:
return "<Chat id={} name={}>".format(self.id, self.get_name())
@property
@@ -40,7 +41,7 @@ class Chat(object):
lib.dc_chat_unref
)
def delete(self):
def delete(self) -> None:
"""Delete this chat and all its messages.
Note:
@@ -50,24 +51,24 @@ class Chat(object):
"""
lib.dc_delete_chat(self.account._dc_context, self.id)
def block(self):
def block(self) -> None:
"""Block this chat."""
lib.dc_block_chat(self.account._dc_context, self.id)
def accept(self):
def accept(self) -> None:
"""Accept this contact request chat."""
lib.dc_accept_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------
def is_group(self):
def is_group(self) -> bool:
""" return true if this chat is a group chat.
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_muted(self):
def is_muted(self) -> bool:
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
@@ -90,7 +91,7 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def can_send(self):
def can_send(self) -> bool:
"""Check if messages can be sent to a give chat.
This is not true eg. for the contact requests or for the device-talk
@@ -98,30 +99,30 @@ class Chat(object):
"""
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
def is_protected(self) -> bool:
""" return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return lib.dc_chat_is_protected(self._dc_chat)
def get_name(self):
def get_name(self) -> Optional[str]:
""" return name of this chat.
:returns: unicode name
"""
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
def set_name(self, name):
def set_name(self, name: str) -> bool:
""" set name of this chat.
:param name: as a unicode string.
:returns: None
:returns: True on success, False otherwise
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
def mute(self, duration=None):
def mute(self, duration: Optional[int] = None) -> None:
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
@@ -135,7 +136,7 @@ class Chat(object):
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
def unmute(self) -> None:
""" unmutes the chat
:returns: None
@@ -144,7 +145,7 @@ class Chat(object):
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
def get_mute_duration(self) -> int:
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
@@ -152,37 +153,37 @@ class Chat(object):
"""
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
def get_ephemeral_timer(self):
def get_ephemeral_timer(self) -> int:
""" get ephemeral timer.
:returns: ephemeral timer value in seconds
"""
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
def set_ephemeral_timer(self, timer):
def set_ephemeral_timer(self, timer: int) -> bool:
""" set ephemeral timer.
:param: timer value in seconds
:returns: None
:returns: True on success, False otherwise
"""
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
def get_type(self):
def get_type(self) -> int:
""" (deprecated) return type of this chat.
:returns: one of const.DC_CHAT_TYPE_*
"""
return lib.dc_chat_get_type(self._dc_chat)
def get_encryption_info(self):
def get_encryption_info(self) -> Optional[str]:
"""Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members"""
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
return from_dc_charpointer(res)
def get_join_qr(self):
def get_join_qr(self) -> Optional[str]:
""" get/create Join-Group QR Code as ascii-string.
this string needs to be transferred to another DC account
@@ -194,7 +195,7 @@ class Chat(object):
# ------ chat messaging API ------------------------------
def send_msg(self, msg):
def send_msg(self, msg: Message) -> Message:
"""send a message by using a ready Message object.
:param msg: a :class:`deltachat.message.Message` instance
@@ -512,8 +513,9 @@ class Chat(object):
latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.utcfromtimestamp(
lib.dc_array_get_timestamp(dc_array, i)
timestamp=datetime.fromtimestamp(
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)

View File

@@ -1,7 +1,13 @@
from typing import Any, List
from .capi import lib
for name in dir(lib):
def __getattr__(name: str) -> Any:
if name.startswith("DC_"):
globals()[name] = getattr(lib, name)
del name
return getattr(lib, name)
return globals()[name]
def __dir__() -> List[str]:
return sorted(name for name in dir(lib) if name.startswith("DC_"))

View File

@@ -1,10 +1,12 @@
""" Contact object. """
from . import props
from .cutil import from_dc_charpointer
from .capi import lib, ffi
from datetime import date, datetime, timezone
from typing import Optional
from . import const, props
from .capi import ffi, lib
from .chat import Chat
from . import const
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
class Contact(object):
@@ -35,18 +37,25 @@ class Contact(object):
)
@props.with_doc
def addr(self):
def addr(self) -> str:
""" normalized e-mail address for this account. """
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc
def name(self):
def name(self) -> str:
""" display name for this contact. """
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
# deprecated alias
display_name = name
@props.with_doc
def last_seen(self) -> date:
"""Last seen timestamp."""
return datetime.fromtimestamp(
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
)
def is_blocked(self):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
@@ -67,15 +76,13 @@ class Contact(object):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
def get_profile_image(self):
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)
return from_optional_dc_charpointer(dc_res)
@property
def status(self):

View File

@@ -1,6 +1,9 @@
from .capi import lib
from .capi import ffi
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional, TypeVar, Generator, Callable
T = TypeVar('T')
def as_dc_charpointer(obj):
@@ -11,21 +14,28 @@ def as_dc_charpointer(obj):
return obj
def iter_array(dc_array_t, constructor):
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i))
def from_dc_charpointer(obj):
def from_dc_charpointer(obj) -> str:
if obj != ffi.NULL:
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
raise ValueError
def from_optional_dc_charpointer(obj) -> Optional[str]:
if obj != ffi.NULL:
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
return None
class DCLot:
def __init__(self, dc_lot):
def __init__(self, dc_lot) -> None:
self._dc_lot = dc_lot
def id(self):
def id(self) -> int:
return lib.dc_lot_get_id(self._dc_lot)
def state(self):
@@ -44,4 +54,4 @@ class DCLot:
ts = lib.dc_lot_get_timestamp(self._dc_lot)
if ts == 0:
return None
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)

View File

@@ -11,7 +11,7 @@ from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError
import imaplib
import deltachat
from deltachat import const
from deltachat import const, Account
SEEN = b'\\Seen'
@@ -62,7 +62,7 @@ def dc_account_after_shutdown(account):
class DirectImap:
def __init__(self, account):
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False

View File

@@ -9,11 +9,11 @@ from .hookspec import account_hookimpl
from contextlib import contextmanager
from .capi import ffi, lib
from .message import map_system_message
from .cutil import from_dc_charpointer
from .cutil import from_optional_dc_charpointer
class FFIEvent:
def __init__(self, name, data1, data2):
def __init__(self, name: str, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
@@ -29,13 +29,13 @@ class FFIEventLogger:
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account):
def __init__(self, account) -> None:
self.account = account
self.logid = self.account.get_config("displayname")
self.init_time = time.time()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
self.account.log(str(ffi_event))
@account_hookimpl
@@ -69,7 +69,7 @@ class FFIEventTracker:
self._event_queue = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
def ac_process_ffi_event(self, ffi_event: FFIEvent):
self._event_queue.put(ffi_event)
def set_timeout(self, timeout):
@@ -96,7 +96,7 @@ class FFIEventTracker:
if rex.match(ev.name):
return ev
def get_info_contains(self, regex):
def get_info_contains(self, regex: str) -> FFIEvent:
rex = re.compile(regex)
while 1:
ev = self.get_matching("DC_EVENT_INFO")
@@ -176,6 +176,7 @@ class FFIEventTracker:
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)
return None
def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
@@ -189,7 +190,7 @@ class EventThread(threading.Thread):
With each Account init this callback thread is started.
"""
def __init__(self, account):
def __init__(self, account) -> None:
self.account = account
super(EventThread, self).__init__(name="events")
self.setDaemon(True)
@@ -202,17 +203,17 @@ class EventThread(threading.Thread):
yield
self.account.log(message + " FINISHED")
def mark_shutdown(self):
def mark_shutdown(self) -> None:
self._marked_for_shutdown = True
def wait(self, timeout=None):
def wait(self, timeout=None) -> None:
if self == threading.current_thread():
# we are in the callback thread and thus cannot
# wait for the thread-loop to finish.
return
self.join(timeout=timeout)
def run(self):
def run(self) -> None:
""" get and run events until shutdown. """
with self.log_execution("EVENT THREAD"):
self._inner_run()
@@ -234,7 +235,7 @@ class EventThread(threading.Thread):
# function which provides us signature info of an event call
evt_name = deltachat.get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
else:
data2 = lib.dc_event_get_data2_int(event)
@@ -250,7 +251,7 @@ class EventThread(threading.Thread):
if self.account._dc_context is not None:
raise
def _map_ffi_event(self, ffi_event):
def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name
account = self.account
if name == "DC_EVENT_CONFIGURE_PROGRESS":

View File

@@ -3,10 +3,11 @@
import os
import re
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
from . import const
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
class Message(object):
@@ -75,7 +76,7 @@ class Message(object):
return lib.dc_msg_get_id(self._dc_msg)
@props.with_doc
def text(self):
def text(self) -> str:
"""unicode text of this messages (might be empty if not a text message). """
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
@@ -84,9 +85,9 @@ class Message(object):
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
@props.with_doc
def html(self):
def html(self) -> str:
"""html text of this messages (might be empty if not an html message). """
return from_dc_charpointer(
return from_optional_dc_charpointer(
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
def has_html(self):
@@ -113,12 +114,13 @@ class Message(object):
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
@props.with_doc
def basename(self):
def basename(self) -> str:
"""basename of the attachment if it exists, otherwise empty string. """
# FIXME, it does not return basename
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
@props.with_doc
def filemime(self):
def filemime(self) -> str:
"""mime type of the file (if it exists)"""
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
@@ -130,7 +132,7 @@ class Message(object):
""" return True if this message is a setup message. """
return lib.dc_msg_is_setupmessage(self._dc_msg)
def get_setupcodebegin(self):
def get_setupcodebegin(self) -> str:
""" return the first characters of a setup code in a setup message. """
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
@@ -146,7 +148,7 @@ class Message(object):
""" return True if this message was forwarded. """
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
def get_message_info(self):
def get_message_info(self) -> str:
""" Return informational text for a single message.
The text is multiline and may contain eg. the raw text of the message.
@@ -170,7 +172,7 @@ class Message(object):
:returns: naive datetime.datetime() object.
"""
ts = lib.dc_msg_get_timestamp(self._dc_msg)
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
@props.with_doc
def time_received(self):
@@ -180,7 +182,7 @@ class Message(object):
"""
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
@props.with_doc
def ephemeral_timer(self):
@@ -200,14 +202,14 @@ class Message(object):
"""
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
@property
def quoted_text(self):
def quoted_text(self) -> Optional[str]:
"""Text inside the quote
:returns: Quoted text"""
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property
def quote(self):
@@ -240,9 +242,9 @@ class Message(object):
return email.message_from_string(s)
@property
def error(self):
def error(self) -> Optional[str]:
"""Error message"""
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
@property
def chat(self):
@@ -255,12 +257,12 @@ class Message(object):
return Chat(self.account, chat_id)
@props.with_doc
def override_sender_name(self):
def override_sender_name(self) -> Optional[str]:
"""the name that should be shown over the message instead of the contact display name.
Usually used to impersonate someone else.
"""
return from_dc_charpointer(
return from_optional_dc_charpointer(
lib.dc_msg_get_override_sender_name(self._dc_msg))
def set_override_sender_name(self, name):

View File

@@ -14,7 +14,7 @@ class Provider(object):
:param domain: The email to get the provider info for.
"""
def __init__(self, account, addr):
def __init__(self, account, addr) -> None:
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_unref,
@@ -24,19 +24,19 @@ class Provider(object):
self._provider = provider
@property
def overview_page(self):
def overview_page(self) -> str:
"""URL to the overview page of the provider on providers.delta.chat."""
return from_dc_charpointer(
lib.dc_provider_get_overview_page(self._provider))
@property
def get_before_login_hints(self):
def get_before_login_hints(self) -> str:
"""Should be shown to the user on login."""
return from_dc_charpointer(
lib.dc_provider_get_before_login_hints(self._provider))
lib.dc_provider_get_before_login_hint(self._provider))
@property
def status(self):
def status(self) -> int:
"""The status of the provider information.
This is one of the

View File

@@ -9,6 +9,7 @@ import fnmatch
import time
import weakref
import tempfile
from typing import List, Dict, Callable
import pytest
import requests
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
class SessionLiveConfigFromFile:
def __init__(self, fn):
def __init__(self, fn) -> None:
self.fn = fn
self.configlist = []
for line in open(fn):
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
d[name] = value
self.configlist.append(d)
def get(self, index):
def get(self, index: int):
return self.configlist[index]
def exists(self):
def exists(self) -> bool:
return bool(self.configlist)
class SessionLiveConfigFromURL:
def __init__(self, url):
configlist: List[Dict[str, str]]
def __init__(self, url: str) -> None:
self.configlist = []
self.url = url
def get(self, index):
def get(self, index: int):
try:
return self.configlist[index]
except IndexError:
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
self.configlist.append(config)
return config
def exists(self):
def exists(self) -> bool:
return bool(self.configlist)
@@ -179,7 +182,7 @@ def session_liveconfig(request):
@pytest.fixture
def data(request):
class Data:
def __init__(self):
def __init__(self) -> None:
# trying to find test data heuristically
# because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding
@@ -210,7 +213,10 @@ def data(request):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class AccountMaker:
def __init__(self):
_finalizers: List[Callable[[], None]]
_accounts: List[Account]
def __init__(self) -> None:
self.live_count = 0
self.offline_count = 0
self._finalizers = []
@@ -423,7 +429,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
pass
imap.dump_imap_structures(tmpdir, logfile=logfile)
def get_accepted_chat(self, ac1, ac2):
def get_accepted_chat(self, ac1: Account, ac2: Account):
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
@@ -451,7 +457,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class BotProcess:
def __init__(self, popen, bot_cfg):
stdout_queue: queue.Queue
def __init__(self, popen, bot_cfg) -> None:
self.popen = popen
self.addr = bot_cfg["addr"]
@@ -459,10 +467,10 @@ class BotProcess:
# the (unicode) lines available for readers through a queue.
self.stdout_queue = queue.Queue()
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
t.setDaemon(1)
t.setDaemon(True)
t.start()
def _run_stdout_thread(self):
def _run_stdout_thread(self) -> None:
try:
while 1:
line = self.popen.stdout.readline()
@@ -474,10 +482,10 @@ class BotProcess:
finally:
self.stdout_queue.put(None)
def kill(self):
def kill(self) -> None:
self.popen.kill()
def wait(self, timeout=30):
def wait(self, timeout=30) -> None:
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
@@ -509,14 +517,14 @@ def tmp_db_path(tmpdir):
@pytest.fixture
def lp():
class Printer:
def sec(self, msg):
def sec(self, msg: str) -> None:
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg):
def step(self, msg: str) -> None:
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg):
def indent(self, msg: str) -> None:
print(" " + msg)
return Printer()

View File

@@ -10,7 +10,7 @@ from deltachat.tracker import ImexTracker
from deltachat.hookspec import account_hookimpl
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
@pytest.mark.parametrize("msgtext,res", [
@@ -265,23 +265,23 @@ class TestOfflineChat:
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
assert chat.get_name() == "title1"
assert contact1 in chat.get_contacts()
assert contact2 in chat.get_contacts()
assert not chat.is_promoted()
msg = chat.get_draft()
assert msg.text == "xyz title1"
chat = ac1.create_group_chat(name="homework", contacts=[])
assert chat.get_name() == "homework"
chat.send_text("Now we have a group for homework")
assert chat.is_promoted()
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
@@ -447,7 +447,7 @@ class TestOfflineChat:
contact1.create_chat().send_text("hello")
def test_chat_message_distinctions(self, ac1, chat1):
past1s = datetime.utcnow() - timedelta(seconds=1)
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
msg = chat1.send_text("msg1")
ts = msg.time_sent
assert msg.time_received is None
@@ -1221,6 +1221,40 @@ class TestOnlineAccount:
assert not msg.is_encrypted()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
def test_gossip_optimization(self, acfactory, lp):
"""Test that gossip timestamp is updated when someone else sends gossip,
so we don't have to send gossip ourselves.
"""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2])
acfactory.introduce_each_other([ac2, ac3])
lp.sec("ac1 creates a group chat with ac2")
group_chat = ac1.create_group_chat("hello")
group_chat.add_contact(ac2)
msg = group_chat.send_text("hi")
# No Autocrypt gossip was sent yet.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == 0
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.text == "hi"
lp.sec("ac2 adds ac3 to the group")
msg.chat.add_contact(ac3)
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
# ac1 does not need to send gossip because ac2 already did it.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == int(msg.time_sent.timestamp())
def test_gossip_encryption_preference(self, acfactory, lp):
"""Test that encryption preference of group members is gossiped to new members.
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
@@ -2163,7 +2197,7 @@ class TestOnlineAccount:
break # DC is done with reading messages
def test_send_receive_locations(self, acfactory, lp):
now = datetime.utcnow()
now = datetime.now(timezone.utc)
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")

View File

@@ -1,7 +1,9 @@
[tox]
isolated_build = true
envlist =
py3
lint
mypy
auditwheels
[testenv]
@@ -42,6 +44,15 @@ commands =
flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:mypy]
deps =
mypy
typing
types-setuptools
types-requests
commands =
mypy --no-incremental src/
[testenv:doc]
changedir=doc
deps =

View File

@@ -2,13 +2,13 @@
set -x -e
# we use the python3.6 environment as the base environment
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
# we use the python3.7 environment as the base environment
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
pushd /usr/bin
ln -s /opt/_internal/cpython-3.6.*/bin/tox
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
ln -s /opt/_internal/cpython-3.7.*/bin/tox
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
popd

View File

@@ -2,13 +2,13 @@
set -x -e
# we use the python3.6 environment as the base environment
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
# we use the python3.7 environment as the base environment
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
pushd /usr/bin
ln -s /opt/_internal/cpython-3.6.*/bin/tox
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
ln -s /opt/_internal/cpython-3.7.*/bin/tox
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
popd

View File

@@ -19,11 +19,9 @@ export DCC_RS_TARGET=release
# Configure access to a base python and to several python interpreters
# needed by tox below.
export PATH=$PATH:/opt/python/cp36-cp36m/bin
export PATH=$PATH:/opt/python/cp37-cp37m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
rm -f python3.6
ln -s /opt/python/cp36-cp36m/bin/python3.6
rm -f python3.7
ln -s /opt/python/cp37-cp37m/bin/python3.7
rm -f python3.8

View File

@@ -7,7 +7,6 @@ use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
@@ -266,7 +265,7 @@ impl ChatId {
/// Updates chat blocked status.
///
/// Returns true if the value was modified.
async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
if self.is_special() {
bail!("ignoring setting of Block-status for {}", self);
}
@@ -397,7 +396,7 @@ impl ChatId {
context.emit_event(EventType::ChatModified(self));
// make sure, the receivers will get all keys
reset_gossiped_timestamp(context, self).await?;
self.reset_gossiped_timestamp(context).await?;
Ok(())
}
@@ -857,6 +856,48 @@ impl ChatId {
pub fn to_u32(self) -> u32 {
self.0
}
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
self.set_gossiped_timestamp(context, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
self,
context: &Context,
timestamp: i64,
) -> Result<()> {
ensure!(
!self.is_special(),
"can not set gossiped timestamp for special chats"
);
info!(
context,
"set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, self],
)
.await?;
Ok(())
}
}
impl std::fmt::Display for ChatId {
@@ -1057,10 +1098,6 @@ impl Chat {
Ok(None)
}
pub async fn get_gossiped_timestamp(&self, context: &Context) -> Result<i64> {
get_gossiped_timestamp(context, self.id).await
}
pub async fn get_color(&self, context: &Context) -> Result<u32> {
let mut color = 0;
@@ -1093,7 +1130,7 @@ impl Chat {
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
param: self.param.to_string(),
gossiped_timestamp: self.get_gossiped_timestamp(context).await?,
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
is_sending_locations: self.is_sending_locations,
color: self.get_color(context).await?,
profile_image: self
@@ -2229,7 +2266,6 @@ pub async fn create_group_chat(
let chat_name = improve_single_line_input(chat_name);
ensure!(!chat_name.is_empty(), "Invalid chat name");
let draft_txt = stock_str::new_group_draft(context, &chat_name).await;
let grpid = dc_create_id();
let row_id = context
@@ -2250,9 +2286,6 @@ pub async fn create_group_chat(
let chat_id = ChatId::new(u32::try_from(row_id)?);
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await?;
let mut draft_msg = Message::new(Viewtype::Text);
draft_msg.set_text(Some(draft_txt));
chat_id.set_draft_raw(context, &mut draft_msg).await?;
}
context.emit_event(EventType::MsgsChanged {
@@ -2371,7 +2404,7 @@ pub(crate) async fn add_contact_to_chat_ex(
let contact = Contact::get_by_id(context, contact_id).await?;
let mut msg = Message::default();
reset_gossiped_timestamp(context, chat_id).await?;
chat_id.reset_gossiped_timestamp(context).await?;
/*this also makes sure, not contacts are added to special or normal chats*/
let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -2453,45 +2486,6 @@ pub(crate) async fn add_contact_to_chat_ex(
Ok(true)
}
pub(crate) async fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<()> {
set_gossiped_timestamp(context, chat_id, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![chat_id],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<()> {
ensure!(!chat_id.is_special(), "can not add member to special chats");
info!(
context,
"set gossiped_timestamp for chat #{} to {}.", chat_id, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, chat_id],
)
.await?;
Ok(())
}
pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
@@ -2822,7 +2816,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(|_| "?").join(",")
msg_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
),
rusqlite::params_from_iter(msg_ids),
|row| row.get::<_, MsgId>(0),
@@ -2852,6 +2846,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
msg.param.remove(Param::OverrideSenderDisplayname);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimfactory
msg.subject = "".to_string();
@@ -4297,6 +4292,98 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_forward_quote() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
// Alice sends a message to Bob.
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
bob.recv_msg(&sent_msg).await;
let received_msg = bob.get_last_msg().await;
// Bob quotes received message and sends a reply to Alice.
let mut reply = Message::new(Viewtype::Text);
reply.set_text(Some("Reply".to_owned()));
reply.set_quote(&bob, &received_msg).await?;
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
alice.recv_msg(&sent_reply).await;
let received_reply = alice.get_last_msg().await;
// Alice forwards a reply.
forward_msgs(&alice, &[received_reply.id], alice_chat.get_id()).await?;
let forwarded_msg = alice.pop_sent_msg().await;
bob.recv_msg(&forwarded_msg).await;
let alice_forwarded_msg = alice.get_last_msg().await;
assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none());
assert_eq!(
alice_forwarded_msg.quoted_text(),
Some("Hi Bob".to_string())
);
let bob_forwarded_msg = bob.get_last_msg().await;
assert!(bob_forwarded_msg.quoted_message(&bob).await?.is_none());
assert_eq!(bob_forwarded_msg.quoted_text(), Some("Hi Bob".to_string()));
Ok(())
}
#[async_std::test]
async fn test_forward_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
// Alice creates a group with Bob.
let alice_group_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?;
let sent_group_msg = alice
.send_text(alice_group_chat_id, "Hi Bob and Claire")
.await;
bob.recv_msg(&sent_group_msg).await;
let bob_group_chat_id = bob.get_last_msg().await.chat_id;
// Alice deletes a message on her device.
// This is needed to make assignment of further messages received in this group
// based on `References:` header harder.
// Previously this exposed a bug, so this is a regression test.
message::delete_msgs(&alice, &[sent_group_msg.sender_msg_id]).await?;
// Alice sends a message to Bob.
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
bob.recv_msg(&sent_msg).await;
let received_msg = bob.get_last_msg().await;
assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
assert_eq!(received_msg.chat_id, bob_chat.id);
// Alice sends another message to Bob, this has first message as a parent.
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await;
bob.recv_msg(&sent_msg).await;
let received_msg = bob.get_last_msg().await;
assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
assert_eq!(received_msg.chat_id, bob_chat.id);
// Bob forwards message to a group chat with Alice.
forward_msgs(&bob, &[received_msg.id], bob_group_chat_id).await?;
let forwarded_msg = bob.pop_sent_msg().await;
alice.recv_msg(&forwarded_msg).await;
let received_forwarded_msg = alice.get_last_msg_in(alice_group_chat_id).await;
assert!(received_forwarded_msg.is_forwarded());
assert_eq!(received_forwarded_msg.chat_id, alice_group_chat_id);
Ok(())
}
#[async_std::test]
async fn test_only_minimal_data_are_forwarded() -> Result<()> {
// send a message from Alice to a group with Bob

View File

@@ -399,10 +399,20 @@ mod tests {
assert_eq!(chats.get_chat_id(1), chat_id2);
assert_eq!(chats.get_chat_id(2), chat_id1);
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
// New drafts are sorted to the top
// We have to set a draft on the other two messages, too, as
// chat timestamps are only exact to the second and sorting by timestamp
// would not work.
// Message timestamps are "smeared" and unique, so we don't have this problem
// if we have any message (can be a draft) in all chats.
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);

View File

@@ -35,6 +35,10 @@ pub(crate) fn str_to_color(s: &str) -> u32 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{:#08x}", color).replace("0x", "#")
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -64,6 +64,13 @@ pub enum Config {
#[strum(props(default = "1"))]
E2eeEnabled,
/// Ignore Autocrypt recommendation for message encryption if possible.
///
/// The only expection is when recommendation is "disable", i.e. encryption is not possible
/// because some recipient has no OpenPGP key.
#[strum(props(default = "0"))]
E2eeForce,
#[strum(props(default = "1"))]
MdnsEnabled,

View File

@@ -8,7 +8,6 @@ mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
@@ -682,7 +681,11 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
return first_err.msg.to_string();
}
errors.iter().map(|e| e.to_string()).join("\n\n")
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n\n")
}
#[derive(Debug, thiserror::Error)]

View File

@@ -5,7 +5,6 @@ use std::convert::{TryFrom, TryInto};
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -67,6 +66,9 @@ pub struct Contact {
/// Blocked state. Use dc_contact_is_blocked to access this field.
pub blocked: bool,
/// Time when the contact was seen last time, Unix time in seconds.
last_seen: i64,
/// The origin/source of the contact.
pub origin: Origin,
@@ -185,7 +187,8 @@ impl Contact {
let mut contact = context
.sql
.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id as i32],
@@ -194,15 +197,17 @@ impl Contact {
let addr: String = row.get(1)?;
let origin: Origin = row.get(2)?;
let blocked: Option<bool> = row.get(3)?;
let authname: String = row.get(4)?;
let param: String = row.get(5)?;
let status: Option<String> = row.get(6)?;
let last_seen: i64 = row.get(4)?;
let authname: String = row.get(5)?;
let param: String = row.get(6)?;
let status: Option<String> = row.get(7)?;
let contact = Self {
id: contact_id,
name,
authname,
addr,
blocked: blocked.unwrap_or_default(),
last_seen,
origin,
param: param.parse().unwrap_or_default(),
status: status.unwrap_or_default(),
@@ -234,6 +239,11 @@ impl Contact {
self.blocked
}
/// Returns last seen timestamp.
pub fn last_seen(&self) -> i64 {
self.last_seen
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
@@ -1289,6 +1299,27 @@ pub(crate) async fn set_status(
Ok(())
}
/// 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,
timestamp: i64,
) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not update special contact last seen timestamp"
);
context
.sql
.execute(
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
paramsv![timestamp, contact_id],
)
.await?;
Ok(())
}
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
@@ -1356,15 +1387,13 @@ pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.collect::<Vec<&str>>()
.chunks(2)
.into_iter()
.filter_map(|mut chunk| {
let name = chunk.next().unwrap();
let addr = match chunk.next() {
Some(a) => a,
None => return None,
};
Some((name, addr))
.filter_map(|chunk| {
let name = chunk.get(0)?;
let addr = chunk.get(1)?;
Some((*name, *addr))
})
.collect()
}
@@ -1377,6 +1406,7 @@ mod tests {
use super::*;
use crate::chat::send_text_msg;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Message;
use crate::test_utils::{self, TestContext};
@@ -2034,4 +2064,34 @@ CCCB 5AA9 F6E1 141C 9431
Ok(())
}
#[async_std::test]
async fn test_last_seen() -> Result<()> {
let alice = TestContext::new_alice().await;
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), 0);
let mime = br#"Subject: Hello
Message-ID: message@example.net
To: Alice <alice@example.com>
From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Chat-Version: 1.0
Date: Sun, 22 Mar 2020 22:37:55 +0000
Hi."#;
dc_receive_imf(&alice, mime, "Inbox", 1, false).await?;
let msg = alice.get_last_msg().await;
let timestamp = msg.get_timestamp();
assert!(timestamp > 0);
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), timestamp);
Ok(())
}
}

View File

@@ -76,6 +76,11 @@ pub struct InnerContext {
pub(crate) id: u32,
creation_time: SystemTime,
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: RwLock<String>,
}
#[derive(Debug)]
@@ -147,6 +152,7 @@ impl Context {
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: RwLock::new("".to_string()),
};
let ctx = Context {
@@ -301,6 +307,7 @@ impl Context {
.await?
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let e2ee_force = self.get_config_int(Config::E2eeForce).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
@@ -388,6 +395,7 @@ impl Context {
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert("e2ee_force", e2ee_force.to_string());
res.insert(
"key_gen_type",
self.get_config_int(Config::KeyGenType).await?.to_string(),

View File

@@ -1,10 +1,10 @@
//! Internet Message Format reception pipeline.
use std::cmp::min;
use std::collections::BTreeSet;
use std::convert::TryFrom;
use anyhow::{bail, ensure, Result};
use itertools::join;
use anyhow::{bail, ensure, Context as _, Result};
use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
@@ -36,9 +36,6 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
use crate::stock_str;
use crate::{contact, location};
// IndexSet is like HashSet but maintains order of insertion.
type ContactIds = indexmap::IndexSet<u32>;
#[derive(Debug, PartialEq, Eq)]
enum CreateEvent {
MsgsChanged,
@@ -163,23 +160,19 @@ pub(crate) async fn dc_receive_imf_inner(
let incoming = from_id != DC_CONTACT_ID_SELF;
let mut to_ids = ContactIds::new();
to_ids.extend(
&dc_add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
prevent_rename,
)
.await?,
);
let to_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
prevent_rename,
)
.await?;
let rcvd_timestamp = dc_smeared_time(context).await;
let sent_timestamp = mime_parser
@@ -187,6 +180,11 @@ pub(crate) async fn dc_receive_imf_inner(
.and_then(|value| mailparse::dateparse(value).ok())
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp));
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
let better_msg = stock_str::msg_location_enabled_by(context, from_id as u32).await;
set_better_msg(&mut mime_parser, &better_msg);
}
// Add parts
let chat_id = add_parts(
context,
@@ -212,6 +210,30 @@ pub(crate) async fn dc_receive_imf_inner(
.await
.map_err(|err| err.context("add_parts error"))?;
if from_id > DC_CONTACT_ID_LAST_SPECIAL {
contact::update_last_seen(context, from_id, sent_timestamp).await?;
}
// Update gossiped timestamp for the chat if someone else or our other device sent
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
// and waste traffic.
if !chat_id.is_special()
&& mime_parser
.recipients
.iter()
.all(|recipient| mime_parser.gossiped_addr.contains(&recipient.addr))
{
info!(
context,
"Received message contains Autocrypt-Gossip for all members, updating timestamp."
);
if chat_id.get_gossiped_timestamp(context).await? < sent_timestamp {
chat_id
.set_gossiped_timestamp(context, sent_timestamp)
.await?;
}
}
let insert_msg_id = if let Some((_chat_id, msg_id)) = created_db_entries.last() {
*msg_id
} else {
@@ -374,7 +396,7 @@ pub async fn from_field_to_contact_id(
"mail has more than one From address, only using first: {:?}", from_address_list
);
}
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
let from_id = from_ids.get(0).cloned().unwrap_or_default();
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
@@ -402,7 +424,7 @@ async fn add_parts(
incoming_origin: Origin,
server_folder: &str,
server_uid: u32,
to_ids: &ContactIds,
to_ids: &[u32],
rfc724_mid: String,
sent_timestamp: i64,
rcvd_timestamp: i64,
@@ -531,7 +553,6 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
sent_timestamp,
if test_normal_chat.is_none() {
allow_creation
} else {
@@ -569,6 +590,16 @@ async fn add_parts(
}
}
}
apply_group_changes(
context,
mime_parser,
sent_timestamp,
chat_id,
from_id,
to_ids,
)
.await?;
}
if chat_id.is_none() {
@@ -644,11 +675,12 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
if Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
} else if parent.is_some() {
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if create_blocked == Blocked::Request && parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
@@ -686,7 +718,7 @@ async fn add_parts(
// the mail is on the IMAP server, probably it is also delivered.
// We cannot recreate other states (read, error).
state = MessageState::OutDelivered;
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
to_id = to_ids.get(0).cloned().unwrap_or_default();
let self_sent = from_id == DC_CONTACT_ID_SELF
&& to_ids.len() == 1
@@ -755,7 +787,6 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
sent_timestamp,
allow_creation,
Blocked::Not,
from_id,
@@ -786,9 +817,9 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked && Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
if chat_id_blocked != Blocked::Not && chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
}
}
@@ -830,9 +861,10 @@ async fn add_parts(
DC_CHAT_ID_TRASH
});
// Extract ephemeral timer from the message.
let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
{
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
chat_id.get_ephemeral_timer(context).await?
} else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
match value.parse::<EphemeralTimer>() {
Ok(timer) => timer,
Err(err) => {
@@ -1278,7 +1310,7 @@ async fn lookup_chat_by_reply(
mime_parser: &mut MimeMessage,
parent: &Option<Message>,
from_id: u32,
to_ids: &ContactIds,
to_ids: &[u32],
) -> Result<Option<(ChatId, Blocked)>> {
// Try to assign message to the same chat as the parent message.
@@ -1288,15 +1320,6 @@ async fn lookup_chat_by_reply(
if let Some(parent) = parent {
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
if let Some(msg_grpid) = try_getting_grpid(mime_parser) {
if msg_grpid == parent_chat.grpid {
// This message will be assigned to this chat, anyway.
// But if we assigned it now, create_or_lookup_group() will not be called
// and group commands will not be executed.
return Ok(None);
}
}
if parent.error.is_some() {
// If the parent msg is undecipherable, then it may have been assigned to the wrong chat
// (undecipherable group msgs often get assigned to the 1:1 chat with the sender).
@@ -1327,7 +1350,7 @@ async fn lookup_chat_by_reply(
/// If it returns false, it shall be assigned to the parent chat.
async fn is_probably_private_reply(
context: &Context,
to_ids: &indexmap::IndexSet<u32>,
to_ids: &[u32],
mime_parser: &MimeMessage,
parent_chat_id: ChatId,
from_id: u32,
@@ -1339,7 +1362,7 @@ async fn is_probably_private_reply(
// should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages
// contain a Chat-Group-Id header and can be sorted into the correct chat this way.
let private_message = to_ids == &[DC_CONTACT_ID_SELF].iter().copied().collect::<ContactIds>();
let private_message = to_ids == [DC_CONTACT_ID_SELF].iter().copied().collect::<Vec<u32>>();
if !private_message {
return Ok(false);
}
@@ -1357,42 +1380,22 @@ async fn is_probably_private_reply(
Ok(true)
}
/// This function tries to extract the group-id from the message and returns the
/// corresponding chat_id. If the chat does not exist, it is created.
/// If the message contains groups commands (name, profile image, changed members),
/// they are executed as well.
///
/// If no group-id could be extracted, message is assigned to the same chat as the
/// parent message.
///
/// If there is no parent message in the database, and there are more than two members,
/// a new ad-hoc group is created.
/// This function tries to extract the group-id from the message and returns the corresponding
/// chat_id. If the chat does not exist, it is created. If there is no group-id and there are more
/// than two members, a new ad hoc group is created.
///
/// On success the function returns the found/created (chat_id, chat_blocked) tuple.
#[allow(non_snake_case, clippy::cognitive_complexity)]
async fn create_or_lookup_group(
context: &Context,
mime_parser: &mut MimeMessage,
sent_timestamp: i64,
allow_creation: bool,
create_blocked: Blocked,
from_id: u32,
to_ids: &ContactIds,
to_ids: &[u32],
) -> Result<Option<(ChatId, Blocked)>> {
let mut chat_id_blocked = Blocked::Not;
let mut recreate_member_list = false;
let mut send_EVENT_CHAT_MODIFIED = false;
let mut X_MrAddToGrp = None;
let mut better_msg: String = From::from("");
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = stock_str::msg_location_enabled_by(context, from_id as u32).await;
set_better_msg(mime_parser, &better_msg);
}
let grpid = if let Some(grpid) = try_getting_grpid(mime_parser) {
grpid
} else {
} else if allow_creation {
let mut member_ids: Vec<u32> = to_ids.iter().copied().collect();
if !member_ids.contains(&(from_id as u32)) {
member_ids.push(from_id as u32);
@@ -1400,24 +1403,26 @@ async fn create_or_lookup_group(
if !member_ids.contains(&(DC_CONTACT_ID_SELF as u32)) {
member_ids.push(DC_CONTACT_ID_SELF as u32);
}
if !allow_creation {
info!(context, "creating ad-hoc group prevented from caller");
return Ok(None);
}
return create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
let res = create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
.await
.map(|chat_id| chat_id.map(|chat_id| (chat_id, create_blocked)))
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
});
.context("could not create ad hoc group")?
.map(|chat_id| (chat_id, create_blocked));
return Ok(res);
} else {
info!(context, "creating ad-hoc group prevented from caller");
return Ok(None);
};
// check, if we have a chat with this group ID
let mut chat_id = chat::get_chat_id_by_grpid(context, &grpid)
.await?
.map(|(chat_id, _protected, _blocked)| chat_id);
let mut chat_id;
let mut chat_id_blocked;
if let Some((id, _protected, blocked)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else {
chat_id = None;
chat_id_blocked = Default::default();
}
// For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that
// they belong to the group because of the Chat-Group-Id or Message-Id header
@@ -1429,69 +1434,6 @@ async fn create_or_lookup_group(
}
}
// now we have a grpid that is non-empty
// but we might not know about this group
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).cloned();
let mut removed_id = None;
if let Some(removed_addr) = mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.cloned()
{
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await?;
match removed_id {
Some(contact_id) => {
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
better_msg = if contact_id == from_id {
stock_str::msg_group_left(context, from_id).await
} else {
stock_str::msg_del_member(context, &removed_addr, from_id).await
};
}
None => warn!(context, "removed {:?} has no contact_id", removed_addr),
}
} else {
let field = mime_parser
.get_header(HeaderDef::ChatGroupMemberAdded)
.cloned();
if let Some(added_member) = field {
mime_parser.is_system_message = SystemMessage::MemberAddedToGroup;
better_msg = stock_str::msg_add_member(context, &added_member, from_id).await;
X_MrAddToGrp = Some(added_member);
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
better_msg = stock_str::msg_grp_name(
context,
old_name,
if let Some(ref name) = grpname {
name
} else {
""
},
from_id as u32,
)
.await;
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
better_msg = match avatar_action {
AvatarAction::Delete => {
stock_str::msg_grp_img_deleted(context, from_id).await
}
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
};
}
}
}
}
set_better_msg(mime_parser, &better_msg);
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "verification problem: {}", err);
@@ -1503,55 +1445,56 @@ async fn create_or_lookup_group(
ProtectionStatus::Unprotected
};
// check if the group does not exist but should be created
let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid)
.await
.unwrap_or_default();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
.context("no address configured")?;
if chat_id.is_none()
&& !mime_parser.is_mailinglist_message()
&& !grpid.is_empty()
&& grpname.is_some()
&& mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
// otherwise, a pending "quit" message may pop up
&& removed_id.is_none()
&& mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
// re-create explicitly left groups only if ourself is re-added
&& (!group_explicitly_left
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
&& (!chat::is_group_explicitly_left(context, &grpid).await?
|| mime_parser.get_header(HeaderDef::ChatGroupMemberAdded).map_or(false, |member_addr| addr_cmp(&self_addr, member_addr)))
{
// group does not exist but should be created
// Group does not exist but should be created.
if !allow_creation {
info!(context, "creating group forbidden by caller");
return Ok(None);
}
chat_id = ChatId::create_multiuser_record(
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).unwrap();
let new_chat_id = ChatId::create_multiuser_record(
context,
Chattype::Group,
&grpid,
grpname.as_ref().unwrap(),
grpname,
create_blocked,
create_protected,
)
.await
.map(Some)
.unwrap_or_else(|err| {
warn!(
context,
"Failed to create group '{}' for grpid={}: {:?}",
grpname.as_ref().unwrap(),
grpid,
err,
);
None
});
.with_context(|| format!("Failed to create group '{}' for grpid={}", grpname, grpid))?;
chat_id = Some(new_chat_id);
chat_id_blocked = create_blocked;
recreate_member_list = true;
// Create initial member list.
chat::add_to_chat_contacts_table(context, new_chat_id, DC_CONTACT_ID_SELF).await?;
if from_id > DC_CONTACT_ID_LAST_SPECIAL
&& !chat::is_contact_in_chat(context, new_chat_id, from_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, from_id).await?;
}
for &to_id in to_ids.iter() {
info!(context, "adding to={:?} to chat id={}", to_id, new_chat_id);
if !Contact::addr_equals_contact(context, &self_addr, to_id).await?
&& !chat::is_contact_in_chat(context, new_chat_id, to_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, to_id).await?;
}
}
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
@@ -1559,25 +1502,16 @@ async fn create_or_lookup_group(
//if create_protected == ProtectionStatus::Protected {
// set from_id=0 as it is not clear that the sender of this random group message
// actually really has enabled chat-protection at some point.
//chat_id
//new_chat_id
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
} else if let Some(chat_id) = chat_id {
if create_protected == ProtectionStatus::Protected {
let chat = Chat::load_from_db(context, chat_id).await?;
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
context.emit_event(EventType::ChatModified(new_chat_id));
}
// again, check chat_id
let chat_id = if let Some(chat_id) = chat_id {
chat_id
if let Some(chat_id) = chat_id {
Ok(Some((chat_id, chat_id_blocked)))
} else if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
@@ -1585,65 +1519,141 @@ async fn create_or_lookup_group(
// not found. We can't create a properly named group in
// this case, so assign error message to 1:1 chat with the
// sender instead.
return Ok(None);
Ok(None)
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
info!(context, "message belongs to unwanted group (TRASH)");
return Ok(Some((DC_CHAT_ID_TRASH, chat_id_blocked)));
};
Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
}
}
// We have a valid chat_id > DC_CHAT_ID_LAST_SPECIAL.
/// Apply group member list, name, avatar and protection status changes from the MIME message.
async fn apply_group_changes(
context: &Context,
mime_parser: &mut MimeMessage,
sent_timestamp: i64,
chat_id: ChatId,
from_id: u32,
to_ids: &[u32],
) -> Result<()> {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok(());
}
// execute group commands
if X_MrAddToGrp.is_some() {
recreate_member_list = true;
} else if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")?;
let mut recreate_member_list = false;
let mut send_event_chat_modified = false;
let removed_id;
if let Some(removed_addr) = mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.cloned()
{
if let Some(ref grpname) = grpname {
if grpname.len() < 200
&& chat_id
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await?;
match removed_id {
Some(contact_id) => {
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
let better_msg = if contact_id == from_id {
stock_str::msg_group_left(context, from_id).await
} else {
stock_str::msg_del_member(context, &removed_addr, from_id).await
};
set_better_msg(mime_parser, &better_msg);
}
None => warn!(context, "removed {:?} has no contact_id", removed_addr),
}
} else {
removed_id = None;
if let Some(added_member) = mime_parser
.get_header(HeaderDef::ChatGroupMemberAdded)
.cloned()
{
mime_parser.is_system_message = SystemMessage::MemberAddedToGroup;
let better_msg = stock_str::msg_add_member(context, &added_member, from_id).await;
set_better_msg(mime_parser, &better_msg);
recreate_member_list = true;
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
.filter(|grpname| grpname.len() < 200)
{
if chat_id
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
.await?
{
info!(context, "updating grpname for chat {}", chat_id);
if context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
paramsv![grpname.to_string(), chat_id],
)
.await
.is_ok()
{
context.emit_event(EventType::ChatModified(chat_id));
info!(context, "updating grpname for chat {}", chat_id);
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
paramsv![grpname.to_string(), chat_id],
)
.await?;
send_event_chat_modified = true;
}
let better_msg =
stock_str::msg_grp_name(context, old_name, grpname, from_id as u32).await;
set_better_msg(mime_parser, &better_msg);
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
let better_msg = match avatar_action {
AvatarAction::Delete => {
stock_str::msg_grp_img_deleted(context, from_id).await
}
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
};
set_better_msg(mime_parser, &better_msg);
}
}
}
} else if mime_parser.is_system_message == SystemMessage::ChatProtectionEnabled {
recreate_member_list = true;
}
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "group-avatar change for {}", chat_id);
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
if chat
.param
.update_timestamp(Param::AvatarTimestamp, sent_timestamp)?
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
send_EVENT_CHAT_MODIFIED = true;
}
if chat
.param
.update_timestamp(Param::AvatarTimestamp, sent_timestamp)?
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
send_event_chat_modified = true;
}
}
@@ -1663,8 +1673,7 @@ async fn create_or_lookup_group(
"DELETE FROM chats_contacts WHERE chat_id=?;",
paramsv![chat_id],
)
.await
.ok();
.await?;
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await?;
}
@@ -1682,7 +1691,7 @@ async fn create_or_lookup_group(
chat::add_to_chat_contacts_table(context, chat_id, to_id).await?;
}
}
send_EVENT_CHAT_MODIFIED = true;
send_event_chat_modified = true;
}
} else if let Some(contact_id) = removed_id {
if chat_id
@@ -1690,14 +1699,14 @@ async fn create_or_lookup_group(
.await?
{
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
send_EVENT_CHAT_MODIFIED = true;
send_event_chat_modified = true;
}
}
if send_EVENT_CHAT_MODIFIED {
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok(Some((chat_id, chat_id_blocked)))
Ok(())
}
/// Create or lookup a mailing list chat.
@@ -1919,7 +1928,11 @@ async fn create_adhoc_group(
/// are hidden in BCC. This group ID is sent by DC in the messages sent to this chat,
/// so having the same ID prevents group split.
async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result<String> {
let member_ids_str = join(member_ids.iter().map(|x| x.to_string()), ",");
let member_ids_str = member_ids
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(",");
let member_cs = context
.get_config(Config::ConfiguredAddr)
.await?
@@ -1962,7 +1975,7 @@ async fn check_verified_properties(
context: &Context,
mimeparser: &MimeMessage,
from_id: u32,
to_ids: &ContactIds,
to_ids: &[u32],
) -> Result<()> {
let contact = Contact::load_from_db(context, from_id).await?;
@@ -2005,13 +2018,20 @@ async fn check_verified_properties(
}
// we do not need to check if we are verified with ourself
let mut to_ids = to_ids.clone();
to_ids.remove(&DC_CONTACT_ID_SELF);
let to_ids = to_ids
.iter()
.copied()
.filter(|id| *id != DC_CONTACT_ID_SELF)
.collect::<Vec<u32>>();
if to_ids.is_empty() {
return Ok(());
}
let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ",");
let to_ids_str = to_ids
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(",");
let rows = context
.sql
@@ -2044,7 +2064,7 @@ async fn check_verified_properties(
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
// mark gossiped keys (if any) as verified
if mimeparser.gossipped_addr.contains(&to_addr) {
if mimeparser.gossiped_addr.contains(&to_addr) {
if let Some(mut peerstate) = peerstate {
// if we're here, we know the gossip key is verified:
// - use the gossip-key as verified-key if there is no verified-key
@@ -2093,6 +2113,8 @@ fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef<str>) {
/// Returns the last message referenced from `References` header if it is in the database.
///
/// For Delta Chat messages it is the last message in the chat of the sender.
///
/// Note that the returned message may be trashed.
async fn get_previous_message(
context: &Context,
mime_parser: &MimeMessage,
@@ -2108,6 +2130,8 @@ async fn get_previous_message(
}
/// Given a list of Message-IDs, returns the latest message found in the database.
///
/// Only messages that are not in the trash chat are considered.
async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Option<Message>> {
if mid_list.is_empty() {
return Ok(None);
@@ -2115,7 +2139,10 @@ async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Opt
for id in parse_message_ids(mid_list).iter().rev() {
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, id).await? {
return Ok(Some(Message::load_from_db(context, msg_id).await?));
let msg = Message::load_from_db(context, msg_id).await?;
if msg.chat_id != DC_CHAT_ID_TRASH {
return Ok(Some(msg));
}
}
}
@@ -2165,6 +2192,10 @@ pub(crate) async fn get_prefetch_parent_message(
Ok(None)
}
/// Looks up contact IDs from the database given the list of recipients.
///
/// Returns vector of IDs guaranteed to be unique.
///
/// * param `prevent_rename`: if true, the display_name of this contact will not be changed. Useful for
/// mailing lists: In some mailing lists, many users write from the same address but with different
/// display names. We don't want the display name to change everytime the user gets a new email from
@@ -2174,8 +2205,8 @@ async fn dc_add_or_lookup_contacts_by_address_list(
address_list: &[SingleInfo],
origin: Origin,
prevent_rename: bool,
) -> Result<ContactIds> {
let mut contact_ids = ContactIds::new();
) -> Result<Vec<u32>> {
let mut contact_ids = BTreeSet::new();
for info in address_list.iter() {
let display_name = if prevent_rename {
Some("")
@@ -2187,7 +2218,7 @@ async fn dc_add_or_lookup_contacts_by_address_list(
);
}
Ok(contact_ids)
Ok(contact_ids.into_iter().collect::<Vec<u32>>())
}
/// Add contacts to database on receiving messages.
@@ -4576,6 +4607,71 @@ Reply to all"#,
}
}
/// Tests that replies to similar ad hoc groups are correctly assigned to chats.
///
/// The difficutly here is that ad hoc groups don't have unique group IDs, because both
/// messages have the same recipient lists and only differ in the subject and message contents.
/// The messages can be properly assigned to chats only using the In-Reply-To or References
/// headers.
#[async_std::test]
async fn test_chat_assignment_adhoc() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
bob.set_config(Config::ShowEmails, Some("2")).await?;
let first_thread_mime = br#"Subject: First thread
Message-ID: first@example.org
To: Alice <alice@example.com>, Bob <bob@example.net>
From: Claire <claire@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First thread."#;
let second_thread_mime = br#"Subject: Second thread
Message-ID: second@example.org
To: Alice <alice@example.com>, Bob <bob@example.net>
From: Claire <claire@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Second thread."#;
// Alice receives two classic emails from Claire.
dc_receive_imf(&alice, first_thread_mime, "Inbox", 1, false).await?;
let alice_first_msg = alice.get_last_msg().await;
dc_receive_imf(&alice, second_thread_mime, "Inbox", 2, false).await?;
let alice_second_msg = alice.get_last_msg().await;
// Bob receives the same two emails.
dc_receive_imf(&bob, first_thread_mime, "Inbox", 1, false).await?;
let bob_first_msg = bob.get_last_msg().await;
dc_receive_imf(&bob, second_thread_mime, "Inbox", 2, false).await?;
let bob_second_msg = bob.get_last_msg().await;
// Messages go to separate chats both for Alice and Bob.
assert!(alice_first_msg.chat_id != alice_second_msg.chat_id);
assert!(bob_first_msg.chat_id != bob_second_msg.chat_id);
// Alice replies to both chats. Bob receives two messages and assigns them to corresponding
// chats.
alice_first_msg.chat_id.accept(&alice).await?;
let alice_first_reply = alice
.send_text(alice_first_msg.chat_id, "First reply")
.await;
bob.recv_msg(&alice_first_reply).await;
let bob_first_reply = bob.get_last_msg().await;
assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id);
alice_second_msg.chat_id.accept(&alice).await?;
let alice_second_reply = alice
.send_text(alice_second_msg.chat_id, "Second reply")
.await;
bob.recv_msg(&alice_second_reply).await;
let bob_second_reply = bob.get_last_msg().await;
assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id);
Ok(())
}
/// Test that read receipts don't create chats.
#[async_std::test]
async fn test_read_receipts_dont_create_chats() -> Result<()> {
@@ -4652,4 +4748,66 @@ Reply to all"#,
}
}
}
#[async_std::test]
async fn test_get_parent_message() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
let mime = br#"Subject: First
Message-ID: first@example.net
To: Alice <alice@example.com>
From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
dc_receive_imf(&t, mime, "INBOX", 1, false).await?;
let first = t.get_last_msg().await;
let mime = br#"Subject: Second
Message-ID: second@example.net
To: Alice <alice@example.com>
From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
dc_receive_imf(&t, mime, "INBOX", 2, false).await?;
let second = t.get_last_msg().await;
let mime = br#"Subject: Third
Message-ID: third@example.net
To: Alice <alice@example.com>
From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
dc_receive_imf(&t, mime, "INBOX", 3, false).await?;
let third = t.get_last_msg().await;
let mime = br#"Subject: Message with references.
Message-ID: second@example.net
To: Alice <alice@example.com>
From: Bob <bob@example.net>
In-Reply-To: <third@example.net>
References: <second@example.net> <nonexistent@example.net> <first@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Message with references."#;
let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?;
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
assert_eq!(parent.id, first.id);
message::delete_msgs(&t, &[first.id]).await?;
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
assert_eq!(parent.id, second.id);
message::delete_msgs(&t, &[second.id]).await?;
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
assert_eq!(parent.id, third.id);
message::delete_msgs(&t, &[third.id]).await?;
let parent = get_parent_message(&t, &mime_parser).await?;
assert!(parent.is_none());
Ok(())
}
}

View File

@@ -227,6 +227,7 @@ mod tests {
use crate::chat::send_msg;
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf_inner;
use crate::ephemeral::Timer;
use crate::test_utils::TestContext;
use num_traits::FromPrimitive;
@@ -343,4 +344,40 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_partial_download_and_ephemeral() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = t
.create_chat_with_contact("bob", "bob@example.org")
.await
.id;
chat_id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
.await?;
// download message from bob partially, this must not change the ephemeral timer
dc_receive_imf_inner(
&t,
b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.com>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain",
"INBOX",
1,
false,
Some(100000),
false,
)
.await?;
assert_eq!(
chat_id.get_ephemeral_timer(&t).await?,
Timer::Enabled { duration: 60 }
);
Ok(())
}
}

View File

@@ -19,6 +19,7 @@ use crate::pgp;
#[derive(Debug)]
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
force_preference: bool,
pub addr: String,
pub public_key: SignedPublicKey,
}
@@ -28,6 +29,7 @@ impl EncryptHelper {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let force_preference = context.get_config_bool(Config::E2eeForce).await?;
let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => {
bail!("addr not configured!");
@@ -39,6 +41,7 @@ impl EncryptHelper {
Ok(EncryptHelper {
prefer_encrypt,
force_preference,
addr,
public_key,
})
@@ -100,11 +103,17 @@ impl EncryptHelper {
}
}
// Count number of recipients, including self.
// This does not depend on whether we send a copy to self or not.
let recipients_count = peerstates.len() + 1;
let want_encrypt = if self.force_preference {
// Ignore preferences of others.
self.prefer_encrypt == EncryptPreference::Mutual
} else {
// Count number of recipients, including self.
// This does not depend on whether we send a copy to self or not.
let recipients_count = peerstates.len() + 1;
2 * prefer_encrypt_count > recipients_count
};
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
Ok(e2ee_guaranteed || want_encrypt)
}
/// Tries to encrypt the passed in `mail`.
@@ -381,6 +390,7 @@ mod tests {
use crate::chat;
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Message;
use crate::param::Param;
use crate::peerstate::ToSave;
@@ -602,4 +612,68 @@ Sent with my Delta Chat Messenger: https://delta.chat";
Ok(())
}
#[async_std::test]
async fn test_e2ee_force() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
bob.set_config(Config::ShowEmails, Some("2")).await?;
// Alice does not prefer encryption.
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
bob.set_config(Config::E2eeEnabled, Some("1")).await?;
// Alice sends her key to Bob.
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
bob.recv_msg(&sent_msg).await;
let received_msg = bob.get_last_msg().await;
assert!(!received_msg.get_showpadlock());
// Bob should not encrypt, because Alice does not prefer encryption.
let sent_msg = bob
.send_text(bob_chat.id, "This should not be encrypted")
.await;
alice.recv_msg(&sent_msg).await;
let received_msg = alice.get_last_msg().await;
assert!(!received_msg.get_showpadlock());
// Bob ignores Alice's preference for no encryption.
bob.set_config(Config::E2eeForce, Some("1")).await?;
let sent_msg = bob.send_text(bob_chat.id, "This should be encrypted").await;
alice.recv_msg(&sent_msg).await;
let received_msg = alice.get_last_msg().await;
assert!(received_msg.get_showpadlock());
// Alice switches to MUA without Autocrypt support.
dc_receive_imf(
&bob,
br#"Subject: Hello from MUA
Message-ID: foobar@example.com
To: Bob <bob@example.net>
From: Alice <alice@example.com>
Content-Type: text/plain; charset=utf-8
Date: Sun, 14 Mar 2500 00:00:00 +0000
Hello from MUA."#,
"INBOX",
100,
false,
)
.await?;
// Bob can't encrypt now because Alice has no key.
let sent_msg = bob
.send_text(bob_chat.id, "This should not be encrypted again")
.await;
alice.recv_msg(&sent_msg).await;
let received_msg = alice.get_last_msg().await;
assert!(!received_msg.get_showpadlock());
Ok(())
}
}

View File

@@ -361,6 +361,7 @@ impl Imap {
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err.to_string().to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {

View File

@@ -8,7 +8,6 @@ use std::future::Future;
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
use async_smtp::smtp::response::{Category, Code, Detail};
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
@@ -840,7 +839,7 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
let q = format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
);
context
.sql
@@ -991,7 +990,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?;
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
}
if 0 != rendered_msg.last_added_location_id {

View File

@@ -77,6 +77,7 @@ pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod qr_code_generator;
pub mod quota;
pub mod securejoin;
mod simplify;

View File

@@ -1,6 +1,7 @@
//! # Logging.
use crate::context::Context;
use async_std::task::block_on;
#[macro_export]
macro_rules! info {
@@ -39,10 +40,28 @@ macro_rules! error {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
$ctx.set_last_error(&formatted);
$ctx.emit_event($crate::EventType::Error(formatted));
}};
}
impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
block_on(async move {
let mut last_error = self.last_error.write().await;
*last_error = error.to_string();
});
}
/// Get last error string.
pub async fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read().await;
last_error.clone()
}
}
pub trait LogExt<T, E>
where
Self: std::marker::Sized,
@@ -134,3 +153,31 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
self
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::TestContext;
use anyhow::Result;
#[async_std::test]
async fn test_get_last_error() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_last_error().await, "");
error!(t, "foo-error");
assert_eq!(t.get_last_error().await, "foo-error");
warn!(t, "foo-warning");
assert_eq!(t.get_last_error().await, "foo-error");
info!(t, "foo-info");
assert_eq!(t.get_last_error().await, "foo-error");
error!(t, "bar-error");
error!(t, "baz-error");
assert_eq!(t.get_last_error().await, "baz-error");
Ok(())
}
}

View File

@@ -877,7 +877,7 @@ impl Message {
}
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
if self.param.get(Param::Quote).is_some() {
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, in_reply_to).await? {
let msg = Message::load_from_db(context, msg_id).await?;

View File

@@ -361,7 +361,7 @@ impl<'a> MimeFactory<'a> {
match &self.loaded {
Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
Ok(true)
} else {
@@ -1412,7 +1412,6 @@ mod tests {
use crate::test_utils::{get_chat_msg, TestContext};
use async_std::fs::File;
use pretty_assertions::assert_eq;
#[test]
fn test_render_email_address() {

View File

@@ -57,7 +57,9 @@ pub struct MimeMessage {
/// this set is empty.
pub signatures: HashSet<Fingerprint>,
pub gossipped_addr: HashSet<String>,
/// The set of mail recipient addresses for which gossip headers were applied, regardless of
/// whether they modified any peerstates.
pub gossiped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
pub location_kml: Option<location::Kml>,
@@ -198,7 +200,7 @@ impl MimeMessage {
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossipped_addr = Default::default();
let mut gossiped_addr = Default::default();
let (mail, signatures, warn_empty_signature) =
match e2ee::try_decrypt(context, &mail, message_time).await {
@@ -221,7 +223,7 @@ impl MimeMessage {
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr = update_gossip_peerstates(
gossiped_addr = update_gossip_peerstates(
context,
message_time,
&mail,
@@ -279,7 +281,7 @@ impl MimeMessage {
// only non-empty if it was a valid autocrypt message
signatures,
gossipped_addr,
gossiped_addr,
is_forwarded: false,
mdn_reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
@@ -1380,6 +1382,9 @@ impl MimeMessage {
}
}
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
///
/// Returns the set of mail recipient addresses for which valid gossip headers were found.
async fn update_gossip_peerstates(
context: &Context,
message_time: i64,
@@ -1387,42 +1392,46 @@ async fn update_gossip_peerstates(
gossip_headers: Vec<String>,
) -> Result<HashSet<String>> {
// XXX split the parsing from the modification part
let mut gossipped_addr: HashSet<String> = Default::default();
let mut gossiped_addr: HashSet<String> = Default::default();
for value in &gossip_headers {
let gossip_header = value.parse::<Aheader>();
if let Ok(ref header) = gossip_header {
if get_recipients(&mail.headers)
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_gossip(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
if let Some(peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
}
gossipped_addr.insert(header.addr.clone());
} else {
warn!(
context,
"Ignoring gossipped \"{}\" as the address is not in To/Cc list.", &header.addr,
);
let header = match value.parse::<Aheader>() {
Ok(header) => header,
Err(err) => {
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
continue;
}
};
if !get_recipients(&mail.headers)
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
warn!(
context,
"Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
);
continue;
}
let peerstate;
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
p.apply_gossip(&header, message_time);
p.save_to_db(&context.sql, false).await?;
peerstate = p;
} else {
let p = Peerstate::from_gossip(&header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = p;
};
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
gossiped_addr.insert(header.addr.clone());
}
Ok(gossipped_addr)
Ok(gossiped_addr)
}
#[derive(Debug)]

View File

@@ -4,7 +4,6 @@ use std::str;
use anyhow::{bail, Error};
use async_std::path::PathBuf;
use itertools::Itertools;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
@@ -182,7 +181,7 @@ impl fmt::Display for Params {
f,
"{}={}",
*key as u8 as char,
value.split('\n').join("\n\n")
value.split('\n').collect::<Vec<&str>>().join("\n\n")
)?;
}
Ok(())

View File

@@ -524,7 +524,6 @@ pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
mod tests {
use super::*;
use crate::test_utils::alice_keypair;
use pretty_assertions::assert_eq;
#[async_std::test]
async fn test_peerstate_save_to_db() {

269
src/qr_code_generator.rs Normal file
View File

@@ -0,0 +1,269 @@
use anyhow::Result;
use qrcodegen::{QrCode, QrCodeEcc};
use crate::{
blob::BlobObject,
chat::{Chat, ChatId},
color::color_int_to_hex_string,
config::Config,
constants::DC_CONTACT_ID_SELF,
contact::Contact,
context::Context,
securejoin, stock_str,
};
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
if let Some(chat_id) = chat_id {
generate_join_group_qr_code(context, chat_id).await
} else {
generate_verification_qr(context).await
}
}
async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Result<String> {
let chat = Chat::load_from_db(context, chat_id).await?;
let avatar = match chat.get_profile_image(context).await? {
Some(path) => {
let avatar_blob = BlobObject::from_path(context, &path)?;
Some(std::fs::read(avatar_blob.to_abs_path())?)
}
None => None,
};
inner_generate_secure_join_qr_code(
&stock_str::secure_join_group_qr_description(context, &chat).await,
&securejoin::dc_get_securejoin_qr(context, Some(chat_id)).await?,
&color_int_to_hex_string(chat.get_color(context).await?),
avatar,
chat.get_name().chars().next().unwrap_or('#'),
)
}
async fn generate_verification_qr(context: &Context) -> Result<String> {
let contact = Contact::get_by_id(context, DC_CONTACT_ID_SELF).await?;
let avatar = match contact.get_profile_image(context).await? {
Some(path) => {
let avatar_blob = BlobObject::from_path(context, &path)?;
Some(std::fs::read(avatar_blob.to_abs_path())?)
}
None => None,
};
let displayname = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => contact.get_addr().to_owned(),
};
inner_generate_secure_join_qr_code(
&stock_str::setup_contact_qr_description(context, &displayname, contact.get_addr()).await,
&securejoin::dc_get_securejoin_qr(context, None).await?,
&color_int_to_hex_string(contact.get_color()),
avatar,
displayname.chars().next().unwrap_or('#'),
)
}
fn inner_generate_secure_join_qr_code(
raw_qrcode_description: &str,
qrcode_content: &str,
color: &str,
avatar: Option<Vec<u8>>,
avatar_letter: char,
) -> Result<String> {
let qrcode_description = &escaper::encode_minimal(raw_qrcode_description);
// config
let width = 515.0;
let height = 630.0;
let logo_offset = 28.0;
let qr_code_size = 400.0;
let qr_translate_up = 40.0;
let text_y_pos = ((height - qr_code_size) / 2.0) + qr_code_size;
let avatar_border_size = 9.0;
let card_border_size = 2.0;
let card_roundness = 40.0;
let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?;
let mut svg = String::with_capacity(28000);
let mut w = tagger::new(&mut svg);
w.elem("svg", |d| {
d.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("viewBox", format_args!("0 0 {} {}", width, height));
})
.build(|w| {
// White Background apears like a card
w.single("rect", |d| {
d.attr("x", card_border_size)
.attr("y", card_border_size)
.attr("rx", card_roundness)
.attr("stroke", "#c6c6c6")
.attr("stroke-width", card_border_size)
.attr("width", width - (card_border_size * 2.0))
.attr("height", height - (card_border_size * 2.0))
.attr("style", "fill:#f2f2f2");
});
// Qrcode
w.elem("g", |d| {
d.attr(
"transform",
format!(
"translate({},{})",
(width - qr_code_size) / 2.0,
((height - qr_code_size) / 2.0) - qr_translate_up
),
);
// If the qr code should be in the wrong place,
// we could also translate and scale the points in the path already,
// but that would make the resulting svg way bigger in size and might bring up rounding issues,
// so better avoid doing it manually if possible
})
.build(|w| {
w.single("path", |d| {
let mut path_data = String::with_capacity(0);
let scale = qr_code_size / qr.size() as f32;
for y in 0..qr.size() {
for x in 0..qr.size() {
if qr.get_module(x, y) {
path_data += &format!("M{},{}h1v1h-1z", x, y);
}
}
}
d.attr("style", "fill:#000000")
.attr("d", path_data)
.attr("transform", format!("scale({})", scale));
});
});
// Text
const BIG_TEXT_CHARS_PER_LINE: usize = 32;
const SMALL_TEXT_CHARS_PER_LINE: usize = 38;
let chars_per_line = if qrcode_description.len() > SMALL_TEXT_CHARS_PER_LINE*2 {
SMALL_TEXT_CHARS_PER_LINE
} else {
BIG_TEXT_CHARS_PER_LINE
};
let lines = textwrap::fill(qrcode_description, chars_per_line);
let (text_font_size, text_y_shift) = if lines.split('\n').count() <= 2 {
(27.0, 0.0)
} else {
(19.0, -10.0)
};
for (count, line) in lines.split('\n').enumerate()
{
w.elem("text", |d| {
d.attr("y", (count as f32 * (text_font_size * 1.2)) + text_y_pos + text_y_shift)
.attr("x", width / 2.0)
.attr("text-anchor", "middle")
.attr(
"style",
format!(
"font-family:sans-serif;\
font-weight:bold;\
font-size:{}px;\
fill:#000000;\
stroke:none",
text_font_size
),
);
})
.build(|w| {
w.put_raw(line);
});
}
// contact avatar in middle of qrcode
const LOGO_SIZE: f32 = 94.4;
const HALF_LOGO_SIZE: f32 = LOGO_SIZE / 2.0;
let logo_position_in_qr = (qr_code_size / 2.0) - HALF_LOGO_SIZE;
let logo_position_x = ((width - qr_code_size) / 2.0) + logo_position_in_qr;
let logo_position_y =
((height - qr_code_size) / 2.0) - qr_translate_up + logo_position_in_qr;
w.single("circle", |d| {
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
.attr("r", HALF_LOGO_SIZE + avatar_border_size)
.attr("style", "fill:#f2f2f2");
});
if let Some(img) = avatar {
w.elem("defs", |_| {}).build(|w| {
w.elem("clipPath", |d| {
d.attr("id", "avatar-cut");
})
.build(|w| {
w.single("circle", |d| {
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
.attr("r", HALF_LOGO_SIZE);
});
});
});
w.single("image", |d| {
d.attr("x", logo_position_x)
.attr("y", logo_position_y)
.attr("width", HALF_LOGO_SIZE * 2.0)
.attr("height", HALF_LOGO_SIZE * 2.0)
.attr("preserveAspectRatio", "none")
.attr("clip-path", "url(#avatar-cut)")
.attr(
"href" /*might need xlink:href instead if it doesn't work on older devices?*/,
format!("data:image/jpeg;base64,{}", base64::encode(img)),
);
});
} else {
w.single("circle", |d| {
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
.attr("r", HALF_LOGO_SIZE)
.attr("style", format!("fill:{}", &color));
});
let avatar_font_size = LOGO_SIZE * 0.65;
let font_offset = avatar_font_size * 0.1;
w.elem("text", |d| {
d.attr("y", logo_position_y + HALF_LOGO_SIZE + font_offset)
.attr("x", logo_position_x + HALF_LOGO_SIZE)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("alignment-baseline", "middle")
.attr(
"style",
format!(
"font-family:sans-serif;\
font-weight:400;\
font-size:{}px;\
fill:#ffffff;",
avatar_font_size
),
);
})
.build(|w| {
w.put_raw(avatar_letter.to_uppercase());
});
}
// Footer logo
const FOOTER_HEIGHT: f32 = 35.0;
const FOOTER_WIDTH: f32 = 198.0;
w.elem("g", |d| {
d.attr(
"transform",
format!(
"translate({},{})",
(width - FOOTER_WIDTH) / 2.0,
height - logo_offset - FOOTER_HEIGHT - text_y_shift
),
);
})
.build(|w| {
w.put_raw(include_str!("../assets/qrcode_logo_footer.svg"));
});
});
Ok(svg)
}

View File

@@ -2,7 +2,7 @@
use anyhow::{anyhow, Result};
use async_imap::types::{Quota, QuotaResource};
use indexmap::IndexMap;
use std::collections::BTreeMap;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
@@ -43,7 +43,7 @@ pub struct QuotaInfo {
/// set to `Err()` if the provider does not support quota or on other errors,
/// set to `Ok()` for valid quota information.
/// Updated by `Action::UpdateRecentQuota`
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
/// Timestamp when structure was modified.
pub(crate) modified: i64,
@@ -52,8 +52,8 @@ pub struct QuotaInfo {
async fn get_unique_quota_roots_and_usage(
folders: Vec<String>,
imap: &mut Imap,
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
for folder in folders {
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
// if there are new quota roots found in this imap folder, add them to the list
@@ -69,7 +69,7 @@ async fn get_unique_quota_roots_and_usage(
// messages could be recieved and so the usage could have been changed
*unique_quota_roots
.entry(quota_root_name.clone())
.or_insert(vec![]) = quota.resources;
.or_insert_with(Vec::new) = quota.resources;
}
}
}
@@ -77,7 +77,7 @@ async fn get_unique_quota_roots_and_usage(
}
fn get_highest_usage<'t>(
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
) -> Result<(u64, &'t String, &QuotaResource)> {
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
for (name, resources) in unique_quota_roots {

View File

@@ -346,6 +346,10 @@ impl Context {
<body>"#
.to_string();
// =============================================================================================
// Get the states from the RwLock
// =============================================================================================
let lock = self.scheduler.read().await;
let (folders_states, smtp) = match &*lock {
Scheduler::Running {
@@ -380,6 +384,13 @@ impl Context {
};
drop(lock);
// =============================================================================================
// Add e.g.
// Incoming messages
// - "Inbox": Connected
// - "Sent": Connected
// =============================================================================================
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
for (folder, watch, state) in &folders_states {
let w = self.get_config(*watch).await.ok_or_log(self);
@@ -417,6 +428,12 @@ impl Context {
}
ret += "</ul>";
// =============================================================================================
// Add e.g.
// Outgoing messages
// Your last message was sent successfully
// =============================================================================================
ret += &format!(
"<h3>{}</h3><ul><li>",
stock_str::outgoing_messages(self).await
@@ -427,6 +444,13 @@ impl Context {
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
ret += "</li></ul>";
// =============================================================================================
// Add e.g.
// Storage on testrun.org
// 1.34 GiB of 2 GiB used
// [======67%===== ]
// =============================================================================================
let domain = dc_tools::EmailAddress::new(
&self
.get_config(Config::ConfiguredAddr)
@@ -527,6 +551,8 @@ impl Context {
}
ret += "</ul>";
// =============================================================================================
ret += "</body></html>\n";
Ok(ret)
}

View File

@@ -499,6 +499,12 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 300);
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
ChatId::create_for_contact(context, contact_id).await?;
}
// Alice -> Bob
send_alice_handshake_msg(
context,
@@ -527,9 +533,18 @@ pub(crate) async fn handle_securejoin_handshake(
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(context, bobstate.chat_id(context).await?, msg, time())
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)
}
@@ -858,7 +873,7 @@ async fn could_not_establish_secure_connection(
)
.await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
error!(
warn!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
);
@@ -936,6 +951,8 @@ mod tests {
async fn test_setup_contact() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().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);
// Setup JoinerProgress sinks.
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
@@ -954,6 +971,7 @@ mod tests {
// 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);
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
@@ -965,6 +983,7 @@ 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);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -1041,6 +1060,11 @@ mod tests {
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);
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
@@ -1302,6 +1326,8 @@ mod tests {
async fn test_secure_join() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().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);
// Setup JoinerProgress sinks.
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
@@ -1323,12 +1349,9 @@ mod tests {
.await
.unwrap();
// Step 2: Bob scans QR-code, sends vg-request; blocks on ongoing process
let joiner = {
let qr = qr.clone();
let ctx = bob.ctx.clone();
async_std::task::spawn(async move { dc_join_securejoin(&ctx, &qr).await.unwrap() })
};
// Step 2: Bob scans QR-code, sends vg-request
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
@@ -1444,9 +1467,9 @@ mod tests {
"vg-member-added-received"
);
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);
assert!(!bob.ctx.has_ongoing().await);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
@@ -1454,6 +1477,14 @@ mod tests {
// however, should not be visible in the UIs.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
let bobs_chat_with_alice = bob.create_chat(&alice).await;
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
Ok(())
}
}

View File

@@ -1,7 +1,5 @@
//! # Simplify incoming plaintext.
use itertools::Itertools;
// protect lines starting with `--` against being treated as a footer.
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
// this should be invisible on most systems and there is no need to unescape it again
@@ -158,6 +156,7 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
s.strip_prefix('>')
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
})
.collect::<Vec<&str>>()
.join("\n");
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
@@ -204,6 +203,7 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
s.strip_prefix('>')
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
})
.collect::<Vec<&str>>()
.join("\n"),
),
)

View File

@@ -6,7 +6,6 @@ use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport};
use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO;
use crate::context::Context;
use crate::events::EventType;
use itertools::Itertools;
use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
@@ -43,10 +42,14 @@ impl Smtp {
}
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
let recipients = recipients_chunk.to_vec();
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
let recipients_display = recipients_chunk
.iter()
.map(|x| x.as_ref())
.collect::<Vec<&str>>()
.join(",");
let envelope = Envelope::new(self.from.clone(), recipients).map_err(Error::Envelope)?;
let envelope = Envelope::new(self.from.clone(), recipients_chunk.to_vec())
.map_err(Error::Envelope)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging

View File

@@ -8,7 +8,7 @@ use strum::EnumProperty;
use strum_macros::EnumProperty;
use crate::blob::BlobObject;
use crate::chat::{self, ChatId, ProtectionStatus};
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::{Contact, Origin};
@@ -56,9 +56,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))]
StatusLine = 13,
#[strum(props(fallback = "Hello, I\'ve just created the group \"%1$s\" for us."))]
NewGroupDraft = 14,
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))]
MsgGrpName = 15,
@@ -333,6 +330,12 @@ pub enum StockMessage {
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
SecureJoinReplies = 118,
#[strum(props(fallback = "Scan to chat with %1$s"))]
SetupContactQRDescription = 119,
#[strum(props(fallback = "Scan to join group %1$s"))]
SecureJoinGroupQRDescription = 120,
}
impl StockMessage {
@@ -457,13 +460,6 @@ pub(crate) async fn status_line(context: &Context) -> String {
translated(context, StockMessage::StatusLine).await
}
/// Stock string: `Hello, I've just created the group "%1$s" for us.`.
pub(crate) async fn new_group_draft(context: &Context, group_name: impl AsRef<str>) -> String {
translated(context, StockMessage::NewGroupDraft)
.await
.replace1(group_name)
}
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
pub(crate) async fn msg_grp_name(
context: &Context,
@@ -624,6 +620,29 @@ pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> S
}
}
/// Stock string: `Scan to chat with %1$s`.
pub(crate) async fn setup_contact_qr_description(
context: &Context,
display_name: &str,
addr: &str,
) -> String {
let name = if display_name == addr {
addr.to_owned()
} else {
format!("{} ({})", display_name, addr)
};
translated(context, StockMessage::SetupContactQRDescription)
.await
.replace1(name)
}
/// Stock string: `Scan to join %1$s`.
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
translated(context, StockMessage::SecureJoinGroupQRDescription)
.await
.replace1(chat.get_name())
}
/// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
translated(context, StockMessage::ContactVerified)

View File

@@ -9,7 +9,6 @@ use crate::message::{Message, MessageState};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;
use itertools::Itertools;
use std::borrow::Cow;
use std::fmt;
@@ -174,7 +173,7 @@ impl Message {
summary_content
};
summary.split_whitespace().join(" ")
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
}
}

View File

@@ -12,7 +12,6 @@ use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
use crate::token::Namespace;
use crate::{chat, stock_str, token};
use anyhow::Result;
use itertools::Itertools;
use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
@@ -177,7 +176,10 @@ impl Context {
} else {
Ok(Some((
format!("{{\"items\":[\n{}\n]}}", serialized),
ids.iter().map(|x| x.to_string()).join(","),
ids.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(","),
)))
}
}