Compare commits

...

140 Commits

Author SHA1 Message Date
link2xt
9d3b2d4844 chore(release): prepare for 1.118.0 2023-07-07 17:01:03 +00:00
link2xt
c312280ab3 build(git-cliff): do not fail if commit.footers is undefined 2023-07-07 16:41:10 +00:00
link2xt
572b99a2e1 Rewrite member added/removed messages even if the change is not allowed
PR https://github.com/deltachat/deltachat-core-rust/pull/4529
2023-07-06 13:47:56 +00:00
link2xt
3992b5a063 refactor: move handle_mdn and handle_ndn to mimeparser and make them private
Previously handle_mdn was erroneously exposed in the public API.
2023-07-06 12:34:49 +00:00
link2xt
b97cb4b55e chore(cargo): update Cargo.lock 2023-07-06 12:33:58 +00:00
link2xt
64c218f1ea fix: rewrite "member added" message even if change is not allowed 2023-07-06 01:36:42 +00:00
link2xt
deed790950 test: test that "member added" message is rewritten even if self is not in chat 2023-07-06 01:36:23 +00:00
link2xt
b33ae3cd0f fix: rewrite "member removed" message even if change is not allowed 2023-07-06 01:36:23 +00:00
link2xt
9480699362 test: test that "member removed" message is rewritten if self is not in chat 2023-07-06 01:36:08 +00:00
link2xt
94c190e844 fix: use different member added/removal messages locally and on the network
This commit adds new stock strings
"I added member ...",
"I removed member ..." and
"I left the group" that are sent over the network
and are visible in classic MUAs like Thunderbird.

Member name in these messages uses authname
instead of the display name,
so the name set locally does not get leaked when
a member is added or removed.
2023-07-05 16:16:30 +00:00
link2xt
578e47666f api!: replace message::get_msg_info() with MsgId.get_info() 2023-07-05 14:22:37 +00:00
link2xt
7eeced50d1 chore(cargo): bump backtrace from 0.3.67 to 0.3.68 2023-07-04 19:19:27 +00:00
link2xt
46e127ad27 chore(cargo): bump hermit-abi from 0.3.1 to 0.3.2
0.3.1 is yanked
2023-07-04 16:56:19 +00:00
link2xt
4891849e28 chore: add LICENSE file to deltachat-rpc-client
This gets packaged into the wheel when you run `python -m build`.
2023-07-03 23:01:46 +00:00
link2xt
e0dd83d538 chore: update MPL 2.0 license text
New text was downloaded from https://www.mozilla.org/en-US/MPL/,
specifically https://www.mozilla.org/media/MPL/2.0/index.f75d2927d3c1.txt

The difference is that the URL is changed from http:// to https://
2023-07-04 00:52:31 +02:00
link2xt
aac8bb950c chore(python): add "Topic :: Communications :: Chat" classifier 2023-07-03 22:49:14 +00:00
link2xt
bf21796bc0 chore(python): change bindings status to production/stable 2023-07-03 22:48:51 +00:00
link2xt
9cbf413064 chore(deltachat-rpc-client): add Trove classifiers 2023-07-03 21:22:10 +00:00
dependabot[bot]
1b57eb4d8d chore(cargo): bump serde from 1.0.164 to 1.0.166
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.164 to 1.0.166.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.164...v1.0.166)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 20:31:10 +00:00
link2xt
5152e702bd chore(cargo): bump num-derive from 0.3.3 to 0.4.0 2023-07-03 19:49:58 +00:00
link2xt
c80f1a1997 chore(cargo): bump toml from 0.7.4 to 0.7.5 2023-07-03 19:41:31 +00:00
link2xt
88759c815b refactor(python): flatten the API of deltachat module 2023-07-03 16:19:31 +00:00
link2xt
9c68fac4b6 api!: make Message.text non-optional
Message.set_text() and Message.get_text() are modified accordingly
to accept String and return String.

Messages which previously contained None text
are now represented as messages with empty text.
Use Message.set_text("".to_string())
instead of Message.set_text(None).
2023-07-03 15:36:32 +00:00
dependabot[bot]
8e17e400b3 chore(cargo): bump uuid from 1.3.3 to 1.4.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.3.3 to 1.4.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.3.3...1.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 00:20:16 +00:00
dependabot[bot]
dae3857db8 chore(cargo): bump syn from 2.0.18 to 2.0.23
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.18 to 2.0.23.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.18...2.0.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 23:45:42 +00:00
dependabot[bot]
695f71e124 chore(cargo): bump strum from 0.24.1 to 0.25.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.24.1 to 0.25.0.
- [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>
2023-07-02 23:45:32 +00:00
link2xt
2d30afd212 fix: do not run simplify() on dehtml() output
simplify() is written to process incoming plaintext messages
and extract footers and quotes from them.
Incoming messages contain various quote styles
and simplify() implements heuristics to detects them.

If dehtml() output is processed by simplify(),
simplify() heuristics may erroneously detect
footers and quotes in produced plaintext.

dehtml() should directly detect quotes
instead of converting them to plaintext quotes
for parsing with simplify().
2023-07-02 23:12:13 +00:00
link2xt
5fe94e8bce docs(dehtml): document AddText variants 2023-07-02 23:12:13 +00:00
link2xt
1351f71632 fix(remove_contact_from_chat): do not emit event if the contact was not removed 2023-07-02 23:00:49 +00:00
link2xt
d42322b38b fix(remove_contact_from_chat): bubble up chat loading errors 2023-07-02 23:00:49 +00:00
link2xt
ce6876c418 fix(remove_contact_from_chat): do not ignore database errors when loading the contact 2023-07-02 23:00:49 +00:00
link2xt
2a6b7d9766 api(contact): add Contact::get_by_id_optional() API 2023-07-02 23:00:49 +00:00
link2xt
fa1924da2b api!(contact): remove Contact::load_from_db() in favor of Contact::get_by_id() 2023-07-02 23:00:49 +00:00
link2xt
d5214eb192 refactor: check if the contact is blocked with a dedicated SQL query
Avoid loading unnecessary fields from the database.
2023-07-02 23:00:49 +00:00
dependabot[bot]
c47324d671 chore(cargo): bump regex from 1.8.3 to 1.8.4
Bumps [regex](https://github.com/rust-lang/regex) from 1.8.3 to 1.8.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.8.3...1.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 21:49:32 +00:00
dependabot[bot]
3f8ec5ec56 chore(cargo): bump testdir from 0.7.3 to 0.8.0
Bumps [testdir](https://github.com/flub/testdir) from 0.7.3 to 0.8.0.
- [Changelog](https://github.com/flub/testdir/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flub/testdir/compare/v0.7.3...v0.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 21:49:23 +00:00
dependabot[bot]
fab504b54c chore(cargo): bump url from 2.3.1 to 2.4.0
Bumps [url](https://github.com/servo/rust-url) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.3.1...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 20:25:59 +00:00
dependabot[bot]
dd32430ade chore(cargo): bump strum_macros from 0.24.3 to 0.25.0
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.24.3 to 0.25.0.
- [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>
2023-07-02 20:23:28 +00:00
link2xt
eb943625a6 chore(cargo): bump num_cpus from 1.15.0 to 1.16.0 2023-07-02 20:19:40 +00:00
dependabot[bot]
32ac4a01ca chore(cargo): bump tempfile from 3.5.0 to 3.6.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.5.0 to 3.6.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.5.0...v3.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 19:36:50 +00:00
dependabot[bot]
f01a9d7d5c chore(cargo): bump rustyline from 11.0.0 to 12.0.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 11.0.0 to 12.0.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v11.0.0...v12.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 18:33:04 +00:00
dependabot[bot]
a5db7104c2 chore(cargo): bump quote from 1.0.28 to 1.0.29
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.28 to 1.0.29.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.28...1.0.29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 18:03:20 +00:00
dependabot[bot]
18aeb14003 chore(cargo): bump quick-xml from 0.28.2 to 0.29.0
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.28.2 to 0.29.0.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.28.2...v0.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 18:02:37 +00:00
dependabot[bot]
4ad2d6e340 chore(cargo): bump sha2 from 0.10.6 to 0.10.7
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.6 to 0.10.7.
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.6...sha2-v0.10.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 17:40:20 +00:00
dependabot[bot]
ce9cd54993 chore(cargo): bump serde_json from 1.0.96 to 1.0.99
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.96 to 1.0.99.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.96...v1.0.99)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 15:37:55 +00:00
dependabot[bot]
23f540f9f9 chore(cargo): bump serde from 1.0.163 to 1.0.164
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.163 to 1.0.164.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.163...v1.0.164)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 15:37:23 +00:00
dependabot[bot]
f994b2d8e4 chore(cargo): bump log from 0.4.18 to 0.4.19
Bumps [log](https://github.com/rust-lang/log) from 0.4.18 to 0.4.19.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.18...0.4.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 15:37:05 +00:00
dependabot[bot]
6e42b85a36 Merge pull request #4495 from deltachat/dependabot/cargo/tokio-1.29.1 2023-07-02 04:17:26 +00:00
link2xt
d69e42377d chore(deps): update human-panic from 1.1.4 to 1.1.5 2023-07-02 01:20:35 +00:00
link2xt
de9330b52f chore(deps): update libc from 0.2.146 to 0.2.147 2023-07-02 01:19:13 +00:00
dependabot[bot]
01d1c4c04b chore(cargo): bump tokio from 1.29.0 to 1.29.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.0 to 1.29.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.0...tokio-1.29.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-02 01:18:16 +00:00
link2xt
7d98978269 chore: rustfmt 2023-07-02 01:17:12 +00:00
link2xt
5024f48609 refactor: add error context to Message::load_from_db() 2023-07-01 19:40:23 +00:00
link2xt
e975568122 docs: fix a typo in get_for_contact documentation 2023-07-01 19:39:50 +00:00
link2xt
1f71c69325 fix: update tokio to 1.29.0
This fixes panic after sending 29 offline messages.
2023-06-28 09:11:57 +00:00
Hocuri
b80ec8507c test: reproduce tokio panic 2023-06-28 08:40:23 +00:00
link2xt
3a3f3542d9 chore: remove libm entry from deny.toml 2023-06-27 14:34:26 +00:00
link2xt
657c5fa947 chore(deps): update iana-time-zone-haiku to 0.1.2
This removes `cxx` dependency.
2023-06-27 14:33:34 +00:00
link2xt
7d0b25c209 chore(deps): update ed25519-dalek 2023-06-27 14:30:27 +00:00
link2xt
8d26303cad refactor(simplify): remove local variable empty_body 2023-06-25 12:55:21 +00:00
Simon Laux
0d8a76593a fix: make avatar image work on more platforms (use xlink:href)
Without it delta touch (qt) can not render the avatar image
and also inkscape does not show it either.
2023-06-22 15:42:08 +00:00
dependabot[bot]
7b49fb2eb6 chore(deps): bump openssl from 0.10.48 to 0.10.55 in /fuzz
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.48 to 0.10.55.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.48...openssl-v0.10.55)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-22 15:32:38 +00:00
link2xt
efa37dd283 fix: preserve indentation when converting plaintext to HTML 2023-06-22 13:24:40 +00:00
link2xt
323e44da04 test: make plaintext to HTML conversion tests non-async 2023-06-22 13:24:40 +00:00
link2xt
70efd0f10a refactor: use with statement with multiple contexts
Otherwise `ruff` check SIM117 fails.
2023-06-21 00:30:48 +00:00
link2xt
fcec81b4c1 chore(cargo): update openssl to 0.10.55
This fixes https://rustsec.org/advisories/RUSTSEC-2023-0044
2023-06-20 23:37:41 +00:00
link2xt
dd806b2d88 test: add make-python-testenv.sh script
This scripts makes it easy to (re)create python testing environment.
2023-06-19 16:24:03 +00:00
link2xt
5659c1b9c2 refactor: do not treat messages without headers as a special case 2023-06-19 12:00:03 +00:00
link2xt
d538d29b94 docs: document how to regenerate Node.js constants before the release 2023-06-17 14:30:11 +00:00
link2xt
b4209fac2e ci(concourse): install venv before trying to use it
This is a follow-up to cbe1671104

I changed `apt-get` arguments for x86_64 glibc builds,
but forgot to change it for x86_64 musl, aarch64 glibc and aarch64 musl.

Because of this, `upload-wheels` task failed with a message:
```
The virtual environment was not created successfully because ensurepip is not
available.  On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.

    apt install python3.11-venv

You may need to use sudo with that command.  After installing the python3-venv
package, recreate your virtual environment.
```
2023-06-16 19:15:05 +00:00
link2xt
4d6dfa120e docs: add missing links for 1.116.0 and 1.117.0 to the changelog 2023-06-16 17:18:04 +00:00
link2xt
f92108be1d chore(release): prepare for 1.117.0 2023-06-16 16:42:17 +00:00
link2xt
00cb72f04d fix(dehtml): do not insert unnecessary newlines when parsing <p> tags
Previously, parsing of `<p>Foo</p><p>Bar</p>`
resulted in `\n\nFoo\n\n\n\nBar\n\n`.

Now it results in `Foo\n\nBar`.
2023-06-16 16:27:14 +00:00
link2xt
92e34d67e6 chore: add openrpc.json to .gitignore 2023-06-16 14:32:09 +00:00
link2xt
65bff8339f chore: update generated node files 2023-06-16 14:27:45 +00:00
Sebastian Klähn
768f8175e6 api(rust): Add api endpoint get_status_update (#4468)
* start

* derive default

* make some webxdc file public

* shorten code

* Add from<u32> for chatid

* reduce changes to a bare minimum

* fix nested errors

* @hocuris fixes

* fix @link2xt changes

---------

Co-authored-by: septias <xxsebastian.kleahnxx@gmail.com>
2023-06-15 15:35:57 +02:00
link2xt
c3f352aff1 fix(dehtml): skip links with empty text 2023-06-14 15:41:38 +00:00
link2xt
5ac2d1b8cb ci: remove mergeable configuration
Mergeable is disabled because it was requiring
that PR title follows conventional commit notation
even when PR consisted of multiple commits
and was not planned to be squash merged.
2023-06-14 14:59:33 +00:00
link2xt
8214b2b8c1 docs: document how conventional commits interact with squash merges 2023-06-14 14:59:33 +00:00
link2xt
53ab8a3b35 fix: update to async-imap 0.9.0 to remove deprecated ouroboros dependency
`ouroboros` is deprecated with a security advisory recommending
switch to `self_cell` crate:
https://rustsec.org/advisories/RUSTSEC-2023-0042

async-imap 0.9.0 depends on `self_cell` instead of `ouroboros`.
2023-06-14 15:46:42 +02:00
link2xt
cbe1671104 ci(concourse): install devpi in a virtual environment
This commit solves the "error: externally-managed-environment"
which started appearing since Debian 12 release.
`debian` is used as an Docker image to run devpi.
2023-06-13 16:51:53 +00:00
link2xt
0d0e223238 test(python): replace legacy tmpdir fixture with tmp_path 2023-06-12 15:14:25 +00:00
Simon Laux
4767f1ce74 docs: readme remove legacy from dc-node, mark napi as experimental 2023-06-10 00:29:59 +02:00
link2xt
1a62b6d77f refactor: rename MimeMessage.header into MimeMessage.headers 2023-06-09 22:20:46 +00:00
link2xt
915008d474 build: use 1 codegen-units for release builds 2023-06-08 16:25:14 +00:00
link2xt
9646766793 build: disable unused brotli feature "ffi-api" 2023-06-08 00:02:11 +00:00
link2xt
e948ec3256 test: regression test for case-sensitive comparison of gossip header to contact address 2023-06-07 19:50:42 +00:00
link2xt
9ab9d2eb7b fix: ignore address case when comparing the To: field to Autocrypt-Gossip: 2023-06-07 19:50:42 +00:00
link2xt
437f8c48c4 api(python): make Contact.is_verified() return bool 2023-06-07 19:50:42 +00:00
link2xt
e6d9a49187 api: emit DC_EVENT_MSGS_CHANGED per chat when messages are deleted 2023-06-07 09:07:21 +00:00
link2xt
33a014eea4 feat: add MsgDeleted event 2023-06-07 09:07:21 +00:00
link2xt
9be871ccf6 fix: emit DC_EVENT_MSGS_CHANGED without IDs when the message expires
Specifying msg IDs that cannot be loaded in the event payload
results in an error when the UI tries to load the message.
Instead, emit an event without IDs
to make the UI reload the whole messagelist.
2023-06-07 09:07:21 +00:00
Sebastian Klähn
6eb8abe535 feat: new group membership update algorithm
New algorithm improves group consistency
in cases of missing messages,
restored old backups and replies from classic MUAs.

Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: link2xt <link2xt@testrun.org>
2023-06-06 23:49:55 +00:00
link2xt
91bf87fa80 fix: update from yanked libc 0.2.145 to 0.2.146
https://github.com/rust-lang/libc/issues/3264
2023-06-06 22:45:17 +00:00
link2xt
a2599ef08a ci: run cargo check with musl libc 2023-06-06 22:45:17 +00:00
link2xt
22d0a4bb32 build: use Rust 1.70.0 to compile deltachat-rpc-server releases 2023-06-06 22:45:17 +00:00
link2xt
7a160033b6 chore(release): prepare for 1.116.0 2023-06-05 19:11:07 +00:00
link2xt
3442748be7 ci: update clippy to 1.70.0 2023-06-05 18:52:30 +00:00
dependabot[bot]
d451bcfbe3 Merge pull request #4452 from deltachat/dependabot/cargo/percent-encoding-2.3.0 2023-06-05 18:51:57 +00:00
link2xt
b2993242e4 docs(python): document pytest fixtures
These docstrings are displayed by `pytest --fixtures`
when `deltachat` package is installed from PyPI.
2023-06-05 18:34:27 +00:00
dependabot[bot]
5eaa9eeed2 chore(cargo): bump percent-encoding from 2.2.0 to 2.3.0
Bumps [percent-encoding](https://github.com/servo/rust-url) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.2.0...v2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 11:04:55 +00:00
dependabot[bot]
3ed2ac8f0c Merge pull request #4429 from deltachat/dependabot/cargo/uuid-1.3.3 2023-06-05 11:00:45 +00:00
dependabot[bot]
0145203f7b Merge pull request #4432 from deltachat/dependabot/cargo/proptest-1.2.0 2023-06-05 10:29:23 +00:00
dependabot[bot]
59588b319e cargo: bump uuid from 1.3.2 to 1.3.3
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.3.2 to 1.3.3.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.3.2...1.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 10:14:10 +00:00
link2xt
f917c7de6b chore(cargo): bump criterion from 0.4.0 to 0.5.1 2023-06-05 10:12:27 +00:00
dependabot[bot]
84888fa4c4 cargo: bump proptest from 1.1.0 to 1.2.0
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/compare/v1.1.0...v1.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-04 22:42:06 +00:00
link2xt
e0b1644488 ci: run node.js lint on Windows 2023-06-04 22:39:15 +00:00
link2xt
4beba8ce3c chore(gitattributes): configure LF line endings for JavaScript files 2023-06-04 22:39:15 +00:00
dependabot[bot]
bc521a685d chore(cargo): bump once_cell from 1.17.1 to 1.18.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.17.1 to 1.18.0.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.17.1...v1.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-04 18:54:12 -03:00
dependabot[bot]
33caa0f499 Merge pull request #4441 from deltachat/dependabot/cargo/tokio-1.28.2 2023-06-04 18:10:06 +00:00
dependabot[bot]
033ce41c0f Merge pull request #4450 from deltachat/dependabot/cargo/libc-0.2.145 2023-06-04 18:09:43 +00:00
dependabot[bot]
88a62e1f6e chore(cargo): bump libc from 0.2.142 to 0.2.145
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.142 to 0.2.145.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.142...0.2.145)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-04 14:24:50 +00:00
dependabot[bot]
dd30f6ab7d cargo: bump tokio from 1.28.0 to 1.28.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.28.0 to 1.28.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.28.0...tokio-1.28.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-04 14:24:16 +00:00
dependabot[bot]
140d116d98 Merge pull request #4438 from deltachat/dependabot/cargo/chrono-0.4.26 2023-06-04 14:23:24 +00:00
dependabot[bot]
d96b783909 cargo: bump chrono from 0.4.24 to 0.4.26
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.24 to 0.4.26.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.24...v0.4.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-04 13:41:48 +00:00
dependabot[bot]
572c7f2efb Merge pull request #4434 from deltachat/dependabot/cargo/base64-0.21.2 2023-06-04 13:40:51 +00:00
link2xt
bcd6c226f6 ci: document why node prebuilds are built in a container 2023-06-04 13:30:24 +00:00
link2xt
bae61746f8 fix: build deltachat-node prebuilds on Debian 10
This reduces glibc version requirement
and makes sure it does not increase
as Ubuntu version on CI runners is updated.
2023-06-04 12:35:12 +00:00
dependabot[bot]
31f2766074 cargo: bump base64 from 0.21.0 to 0.21.2
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.21.0 to 0.21.2.
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.21.0...v0.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-04 10:33:01 +00:00
link2xt
b06c8baa9c chore(cargo): bump pretty_env_logger from 0.4.0 to 0.5.0 2023-06-04 10:31:29 +00:00
dependabot[bot]
1e479fe4a3 Merge pull request #4431 from deltachat/dependabot/cargo/serde-1.0.163 2023-06-04 09:13:42 +00:00
link2xt
8ea8ee02ed ci: add top comments to GH Actions workflows 2023-06-02 20:19:57 +00:00
link2xt
55bc556bcf ci: use working-directory instead of cd command 2023-06-02 17:00:27 +00:00
link2xt
3b6d21301b ci: remove broken node-delete-preview.yml workflow
Old previews should be deleted by the cronjob
running on the `download.delta.chat` server.
2023-06-02 16:50:15 +00:00
dependabot[bot]
472195c7d9 cargo: bump serde from 1.0.160 to 1.0.163
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.160 to 1.0.163.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.160...v1.0.163)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 15:26:34 +00:00
dependabot[bot]
afb8b5ce55 Merge pull request #4435 from deltachat/dependabot/cargo/toml-0.7.4 2023-06-02 15:25:40 +00:00
dependabot[bot]
de3c82ef43 cargo: bump toml from 0.7.3 to 0.7.4
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.3 to 0.7.4.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.3...toml-v0.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 13:49:25 +00:00
dependabot[bot]
4255ae4c2d cargo: bump log from 0.4.17 to 0.4.18
Bumps [log](https://github.com/rust-lang/log) from 0.4.17 to 0.4.18.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.17...0.4.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 12:28:21 +00:00
dependabot[bot]
4b4e2f700e Merge pull request #4437 from deltachat/dependabot/cargo/syn-2.0.18 2023-06-02 12:27:32 +00:00
dependabot[bot]
81fde5c680 Merge pull request #4436 from deltachat/dependabot/cargo/reqwest-0.11.18 2023-06-02 12:26:56 +00:00
dependabot[bot]
5340a7d033 cargo: bump syn from 2.0.15 to 2.0.18
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.15 to 2.0.18.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.15...2.0.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 10:31:52 +00:00
dependabot[bot]
fc82d728fc cargo: bump reqwest from 0.11.17 to 0.11.18
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.17 to 0.11.18.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.17...v0.11.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 10:31:34 +00:00
link2xt
136e9179e9 ci(dependabot): use chore prefix for dependency updates 2023-06-02 10:30:34 +00:00
link2xt
31e19ca56c chore(cargo): bump regex from 1.8.1 to 1.8.3 2023-06-02 10:28:58 +00:00
link2xt
f2b02b7bb0 docs: document how to get Concourse CI secrets from pass 2023-06-01 19:50:42 +00:00
link2xt
646ace8e7a chore: add link to the changelog 2023-06-01 12:54:12 +00:00
link2xt
a2495716b6 Merge tag 'v1.112.10' 2023-06-01 12:53:10 +00:00
link2xt
0f579c6415 chore(release): prepare for 1.112.10 2023-06-01 12:49:20 +00:00
link2xt
3eddc9164c fix: disable fetch_existing_msgs setting by default
This caused too many problems after switching
the default setting for `show_emails`
from DC_SHOW_EMAILS_OFF to DC_SHOW_EMAILS_ALL
in <https://github.com/deltachat/deltachat-core-rust/pull/4019>

There is a topic <https://support.delta.chat/t/setting-no-chats-only-for-show-classic-e-mails-showing-classic-emails/2481>
on the forum with multiple requests to revert this setting
due to old emails being downloaded.
2023-06-01 11:11:36 +00:00
link2xt
dd29fae49b fix: update h2 to fix RUSTSEC-2023-0034 2023-05-31 19:00:20 +00:00
107 changed files with 3848 additions and 2306 deletions

8
.gitattributes vendored
View File

@@ -2,6 +2,14 @@
# ensures this even if the user has not set core.autocrlf.
* text=auto
# Checkout JavaScript files with LF line endings
# to prevent `prettier` from reporting errors on Windows.
*.js eol=lf
*.jsx eol=lf
*.ts eol=lf
*.tsx eol=lf
*.json eol=lf
# This directory contains email messages verbatim, and changing CRLF to
# LF will corrupt them.
test-data/** text=false

View File

@@ -5,5 +5,5 @@ updates:
schedule:
interval: "monthly"
commit-message:
prefix: "cargo"
prefix: "chore(cargo)"
open-pull-requests-limit: 50

15
.github/mergeable.yml vendored
View File

@@ -1,15 +0,0 @@
version: 2
mergeable:
- when: pull_request.*
name: "Conventional Commits"
validate:
- do: title
begins_with:
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore', 'cargo', 'build', 'ci', 'docs']
fail:
- do: checks
status: "action_required"
payload:
title: PR title should follow conventional commits
summary: "PR title should follow https://conventionalcommits.org. See https://github.com/deltachat/deltachat-core-rust/blob/master/CONTRIBUTING.md for details."

View File

@@ -1,3 +1,7 @@
# GitHub Actions workflow to
# lint Rust and Python code
# and run Rust tests, Python tests and async Python tests.
name: Rust CI
# Cancel previously started workflow runs
@@ -20,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.68.2
RUSTUP_TOOLCHAIN: 1.70.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -34,6 +38,10 @@ jobs:
- name: Check
run: cargo check --workspace --all-targets --all-features
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
- name: Check musl
run: scripts/zig-musl-check.sh
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest

View File

@@ -1,4 +1,10 @@
# Manually triggered action to build deltachat-rpc-server binaries.
# GitHub Actions workflow
# to build `deltachat-rpc-server` binaries
# and upload them to the release.
#
# The workflow is automatically triggered on releases.
# It can also be triggered manually
# to produce binary artifacts for testing.
name: Build deltachat-rpc-server binaries

View File

@@ -1,3 +1,6 @@
# GitHub Actions workflow
# to automatically approve PRs made by Dependabot.
name: Dependabot auto-approve
on: pull_request

View File

@@ -38,13 +38,12 @@ jobs:
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: Install dependencies without running scripts
run: |
cd deltachat-jsonrpc/typescript
npm install --ignore-scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
shell: bash
working-directory: deltachat-jsonrpc/typescript
run: |
cd deltachat-jsonrpc/typescript
npm run build
npm pack .
ls -lah

View File

@@ -22,24 +22,19 @@ jobs:
- name: Add Rust cache
uses: Swatinem/rust-cache@v2
- name: npm install
run: |
cd deltachat-jsonrpc/typescript
npm install
working-directory: deltachat-jsonrpc/typescript
run: npm install
- name: Build TypeScript, run Rust tests, generate bindings
run: |
cd deltachat-jsonrpc/typescript
npm run build
working-directory: deltachat-jsonrpc/typescript
run: npm run build
- name: Run integration tests
run: |
cd deltachat-jsonrpc/typescript
npm run test
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: make sure websocket server version still builds
run: |
cd deltachat-jsonrpc
cargo build --bin deltachat-jsonrpc-server --features webserver
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver
- name: Run linter
run: |
cd deltachat-jsonrpc/typescript
npm run prettier:check
working-directory: deltachat-jsonrpc/typescript
run: npm run prettier:check

View File

@@ -1,31 +0,0 @@
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
name: Delete node PR previews
on:
pull_request:
types: [closed]
jobs:
delete:
runs-on: ubuntu-latest
steps:
- name: Get Pull Request ID
id: getid
run: |
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
- name: Renaming
run: |
# create empty file to copy it over the outdated deliverable on download.delta.chat
echo "This preview build is outdated and has been removed." > empty
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
- name: Replace builds with dummy files
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
host: "download.delta.chat"
port: 22
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
remote: "/var/www/html/download/node/preview/"

View File

@@ -1,3 +1,8 @@
# GitHub Actions workflow to build
# Node.js bindings documentation
# and upload it to the web server.
# Built documentation is available at <https://js.delta.chat/>
name: Generate & upload node.js documentation
on:
@@ -17,8 +22,8 @@ jobs:
node-version: 16.x
- name: npm install and generate documentation
working-directory: node
run: |
cd node
npm i --ignore-scripts
npx typedoc
mv docs js

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
os: [macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -46,13 +46,12 @@ jobs:
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd node
npm install --verbose
working-directory: node
run: npm install --verbose
- name: Build Prebuild
working-directory: node
run: |
cd node
npm run prebuildify
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
@@ -62,10 +61,79 @@ jobs:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
prebuild-linux:
name: Prebuild Linux
runs-on: ubuntu-latest
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
container: debian:10
steps:
# Working directory is owned by 1001:1001 by default.
# Change it to our user.
- name: Change working directory owner
run: chown root:root .
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
- run: apt-get update
# Python is needed for node-gyp
- name: Install curl, python and compilers
run: apt-get install -y curl build-essential python3
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: System info
run: |
rustc -vV
rustup -vV
cargo -vV
npm --version
node --version
- name: Cache node modules
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
~/.npm
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
working-directory: node
run: npm install --verbose
- name: Build Prebuild
working-directory: node
run: |
npm run prebuildify
tar -zcvf "linux.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v3
with:
name: linux
path: node/linux.tar.gz
pack-module:
needs: prebuild
needs: [prebuild, prebuild-linux]
name: Package deltachat-node and upload to download.delta.chat
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Install tree
run: sudo apt install tree
@@ -96,10 +164,10 @@ jobs:
npm --version
node --version
echo $DELTACHAT_NODE_TAR_GZ
- name: Download Ubuntu prebuild
- name: Download Linux prebuild
uses: actions/download-artifact@v1
with:
name: ubuntu-20.04
name: linux
- name: Download macOS prebuild
uses: actions/download-artifact@v1
with:
@@ -111,11 +179,11 @@ jobs:
- shell: bash
run: |
mkdir node/prebuilds
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
tar -xvzf linux/linux.tar.gz -C node/prebuilds
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -rf ubuntu-20.04 macos-latest windows-latest
rm -rf linux macos-latest windows-latest
- name: Install dependencies without running scripts
run: |
npm install --ignore-scripts

View File

@@ -1,3 +1,6 @@
# GitHub Actions workflow
# to test Node.js bindings.
name: "node.js tests"
# Cancel previously started workflow runs
@@ -52,25 +55,13 @@ jobs:
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd node
npm install --verbose
working-directory: node
run: npm install --verbose
- name: Test
timeout-minutes: 10
if: runner.os != 'Windows'
run: |
cd node
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
- name: Run tests on Windows, except lint
timeout-minutes: 10
if: runner.os == 'Windows'
run: |
cd node
npm run test:mocha
working-directory: node
run: npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,4 +1,5 @@
# Manually triggered action to build a Windows repl.exe which users can
# Manually triggered GitHub Actions workflow
# to build a Windows repl.exe which users can
# download to debug complex bugs.
name: Build Windows REPL .exe

View File

@@ -1,3 +1,7 @@
# GitHub Actions workflow
# to build `deltachat_fii` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat
on:

View File

@@ -1,5 +1,184 @@
# Changelog
## [1.118.0] - 2023-07-07
### API-Changes
- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
- Add `Contact::get_by_id_optional()` API.
- [**breaking**] Make `Message.text` non-optional.
- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
Previously `handle_mdn` was erroneously exposed in the public API.
- python: flatten the API of `deltachat` module.
### Fixes
- Use different member added/removal messages locally and on the network.
- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
- Make SVG avatar image work on more platforms (use `xlink:href`).
- Preserve indentation when converting plaintext to HTML.
- Do not run simplify() on dehtml() output.
- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
### Documentation
- Document how to regenerate Node.js constants before the release.
### Build system
- git-cliff: Do not fail if commit.footers is undefined.
### Other
- Dependency updates.
- Update MPL 2.0 license text.
- Add LICENSE file to deltachat-rpc-client.
- deltachat-rpc-client: Add Trove classifiers.
- python: Change bindings status to production/stable.
### Tests
- Add `make-python-testenv.sh` script.
## [1.117.0] - 2023-06-15
### Features
- New group membership update algorithm.
New algorithm improves group consistency
in cases of missing messages,
restored old backups and replies from classic MUAs.
- Add `DC_EVENT_MSG_DELETED` event.
This event notifies the UI about the message
being deleted from the messagelist, e.g. when the message expires
or the user deletes it.
### Fixes
- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires.
Specifying msg IDs that cannot be loaded in the event payload
results in an error when the UI tries to load the message.
Instead, emit an event without IDs
to make the UI reload the whole messagelist.
- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`.
This bug resulted in failure to propagate verification
if the contact list already contained a new verified group member
with a non-lowercase address.
- dehtml: skip links with empty text.
Links like `<a href="https://delta.chat/"></a>` in HTML mails are now skipped
instead of being converted to a link without a label like `[](https://delta.chat/)`.
- dehtml: Do not insert unnecessary newlines when parsing `<p>` tags.
- Update from yanked `libc` 0.2.145 to 0.2.146.
- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency.
### API-Changes
- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted.
Previously a single event with zero chat ID was emitted.
- python: make `Contact.is_verified()` return bool.
- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)).
- rust: make `WebxdcManifest` type public.
### Build system
- Use Rust 1.70.0 to compile deltachat-rpc-server releases.
- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries.
### CI
- Run `cargo check` with musl libc.
- concourse: Install devpi in a virtual environment.
- Remove [mergeable](https://mergeable.us/) configuration.
### Documentation
- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently.
- CONTRIBUTING: document how conventional commits interact with squash merges.
### Refactor
- Rename `MimeMessage.header` into `MimeMessage.headers`.
- Derive `Default` trait for `WebxdcManifest`.
### Tests
- Regression test for case-sensitive comparison of gossip header to contact address.
- Multiple new group consistency tests in Rust.
- python: Replace legacy `tmpdir` fixture with `tmp_path`.
## [1.116.0] - 2023-06-05
### API-Changes
- Add `dc_jsonrpc_blocking_call()`.
### Changes
- Generate OpenRPC definitions for JSON-RPC.
- Add more context to message loading errors.
### Fixes
- Build deltachat-node prebuilds on Debian 10.
### Documentation
- Document release process in `RELEASE.md`.
- Add contributing guidelines `CONTRIBUTING.md`.
- Update instructions for python devenv.
- python: Document pytest fixtures.
### Tests
- python: Make `test_mdn_asymmetric` less flaky.
- Make `test_group_with_removed_message_id` less flaky.
- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)).
### Build system
- git-cliff: Changelog generation improvements.
- `set_core_version.py`: Expect release date in the changelog.
### CI
- Require Python 3.8 for deltachat-rpc-client.
- mergeable: Allow PR titles to start with "ci" and "build".
- Remove incorrect comment.
- dependabot: Use `chore` prefix for dependency updates.
- Remove broken `node-delete-preview.yml` workflow.
- Add top comments to GH Actions workflows.
- Run node.js lint on Windows.
- Update clippy to 1.70.0.
### Miscellaneous Tasks
- Remove release.toml.
- gitattributes: Configure LF line endings for JavaScript files.
- Update dependencies
## [1.112.10] - 2023-06-01
### Fixes
- Disable `fetch_existing_msgs` setting by default.
- Update `h2` to fix RUSTSEC-2023-0034.
## [1.115.0] - 2023-05-12
### JSON-RPC API Changes
@@ -2486,6 +2665,10 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0

View File

@@ -15,7 +15,13 @@ If you have a feature request, create a new topic on the [forum](https://support
## Contributing code
If you want to contribute a code, [open a pull request](https://github.com/deltachat/deltachat-core-rust/pulls).
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
If you have write access to the repository,
push a branch named `<username>/<feature>`
so it is clear who is responsible for the branch,
and open a PR proposing to merge the change.
Otherwise fork the repository and create a branch in your fork.
You can find the list of good first issues
and a link to this guide
@@ -45,6 +51,11 @@ The following prefix types are used:
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
If you intend to squash merge the PR from the web interface,
make sure the PR title follows the conventional commits notation
as it will end up being a commit title.
Otherwise make sure each commit title follows the conventional commit notation.
#### Breaking Changes
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
@@ -65,6 +76,17 @@ If you have multiple changes in one PR, create multiple conventional commits, an
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Reviewing
Once a PR has an approval and passes CI, it can be merged.
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
This is to ensure that PRs are merged as intended by the author,
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
If you do not have access to the repository and created a PR from a fork,
ask the maintainers to merge the PR and say how it should be merged.
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).

1452
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.115.0"
version = "1.118.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
@@ -23,6 +23,7 @@ opt-level = "z"
lto = true
panic = 'abort'
opt-level = "z"
codegen-units = 1
[patch.crates-io]
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
@@ -35,13 +36,13 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
async-imap = { version = "0.8.0", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.9.0", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
brotli = "3.3"
brotli = { version = "3.3", default-features=false, features = ["std"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
@@ -58,19 +59,19 @@ lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master"
libc = "0.2"
mailparse = "0.14"
mime = "0.3.17"
num_cpus = "1.15"
num-derive = "0.3"
num_cpus = "1.16"
num-derive = "0.4"
num-traits = "0.2"
once_cell = "1.17.0"
percent-encoding = "2.2"
once_cell = "1.18.0"
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.28"
quick-xml = "0.29"
rand = "0.8"
regex = "1.8"
reqwest = { version = "0.11.17", features = ["json"] }
reqwest = { version = "0.11.18", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.4"
@@ -79,8 +80,8 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
strum = "0.24"
strum_macros = "0.24"
strum = "0.25"
strum_macros = "0.25"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
@@ -96,13 +97,13 @@ uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.4.0", features = ["async_tokio"] }
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = "1.13"
log = "0.4"
pretty_env_logger = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.7.3"
testdir = "0.8.0"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"

View File

@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE

View File

@@ -167,8 +167,8 @@ Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js**
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]

View File

@@ -4,15 +4,18 @@ For example, to release version 1.116.0 of the core, do the following steps.
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
2. Run `npm run build:core:constants` in the root of the repository
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
3. Update the version by running `scripts/set_core_version.py 1.116.0`.
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
4. Commit the changes as `chore(release): prepare for 1.116.0`.
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
5. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
5. Tag the release: `git tag -a v1.116.0`.
6. Tag the release: `git tag -a v1.116.0`.
6. Push the release tag: `git push origin v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
7. Create a GitHub release: `gh release create v1.116.0 -n ''`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -67,9 +67,11 @@ body = """
- {% if commit.breaking %}[**breaking**] {% endif %}\
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
{{ commit.message | upper_first }}.\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
{% if commit.footers is defined %}\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
{% endif%}\
{% endfor %}
{% endfor %}\n
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.115.0"
version = "1.118.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -24,7 +24,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.8"
once_cell = "1.17.0"
once_cell = "1.18.0"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
[features]

View File

@@ -6079,6 +6079,15 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSG_READ 2015
/**
* A single message is deleted.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/
#define DC_EVENT_MSG_DELETED 2016
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.

View File

@@ -527,6 +527,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
EventType::MsgRead { .. } => 2015,
EventType::MsgDeleted { .. } => 2016,
EventType::ChatModified(_) => 2020,
EventType::ChatEphemeralTimerModified { .. } => 2021,
EventType::ContactsChanged(_) => 2030,
@@ -574,6 +575,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::MsgDelivered { chat_id, .. }
| EventType::MsgFailed { chat_id, .. }
| EventType::MsgRead { chat_id, .. }
| EventType::MsgDeleted { chat_id, .. }
| EventType::ChatModified(chat_id)
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
@@ -631,7 +633,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
| EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
| EventType::MsgRead { msg_id, .. }
| EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::SecurejoinInviterProgress { progress, .. }
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
@@ -674,6 +677,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::MsgDelivered { .. }
| EventType::MsgFailed { .. }
| EventType::MsgRead { .. }
| EventType::MsgDeleted { .. }
| EventType::ChatModified(_)
| EventType::ContactsChanged(_)
| EventType::LocationChanged(_)
@@ -1873,13 +1877,10 @@ pub unsafe extern "C" fn dc_get_msg_info(
return "".strdup();
}
let ctx = &*context;
block_on(async move {
message::get_msg_info(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
})
let msg_id = MsgId::new(msg_id);
block_on(msg_id.get_info(ctx))
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
}
#[no_mangle]
@@ -3304,7 +3305,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_text().unwrap_or_default().strdup()
ffi_msg.message.get_text().strdup()
}
#[no_mangle]
@@ -3689,7 +3690,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
return;
}
let ffi_msg = &mut *msg;
ffi_msg.message.set_text(to_opt_string_lossy(text))
ffi_msg.message.set_text(to_string_lossy(text))
}
#[no_mangle]

View File

@@ -1,3 +1,4 @@
openrpc/openrpc.json
accounts/
.cargo

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.115.0"
version = "1.118.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -17,14 +17,14 @@ deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.11"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
tempfile = "3.6.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.96"
serde_json = "1.0.99"
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.28.0" }
tokio = { version = "1.29.1" }
sanitize-filename = "0.4"
walkdir = "2.3.3"
base64 = "0.21"
@@ -34,7 +34,7 @@ axum = { version = "0.6.18", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.28.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -19,9 +19,7 @@ use deltachat::{
context::get_info,
ephemeral::Timer,
imex, location,
message::{
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
},
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
@@ -901,7 +899,7 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
msg.set_text(text);
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
@@ -1119,7 +1117,7 @@ impl CommandApi {
/// max. text returned by dc_msg_get_text() (about 30000 characters).
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
get_msg_info(&ctx, MsgId::new(message_id)).await
MsgId::new(message_id).get_info(&ctx).await
}
/// Returns contacts that sent read receipts and the time of reading.
@@ -1340,7 +1338,7 @@ impl CommandApi {
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
let contact = Contact::load_from_db(&ctx, contact_id).await?;
let contact = Contact::get_by_id(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
@@ -1767,9 +1765,7 @@ impl CommandApi {
} else {
Viewtype::Text
});
if data.text.is_some() {
message.set_text(data.text);
}
message.set_text(data.text.unwrap_or_default());
if data.html.is_some() {
message.set_html(data.html);
}
@@ -1950,7 +1946,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
msg.set_text(text);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
@@ -1973,9 +1969,7 @@ impl CommandApi {
} else {
Viewtype::Text
});
if text.is_some() {
message.set_text(text);
}
message.set_text(text.unwrap_or_default());
if let Some(file) = file {
message.set_file(file, None);
}
@@ -2019,9 +2013,7 @@ impl CommandApi {
} else {
Viewtype::Text
});
if text.is_some() {
draft.set_text(text);
}
draft.set_text(text.unwrap_or_default());
if let Some(file) = file {
draft.set_file(file, None);
}

View File

@@ -53,7 +53,7 @@ impl FullChat {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::load_from_db(context, *contact_id)
Contact::get_by_id(context, *contact_id)
.await
.context("failed to load contact")?,
)
@@ -74,7 +74,7 @@ impl FullChat {
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::load_from_db(context, *contact)
Some(contact) => Contact::get_by_id(context, *contact)
.await
.context("failed to load contact for was_seen_recently")?
.was_seen_recently(),

View File

@@ -107,7 +107,7 @@ pub(crate) async fn get_chat_list_item_by_id(
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
Some(contact) => Contact::load_from_db(ctx, *contact)
Some(contact) => Contact::get_by_id(ctx, *contact)
.await
.context("contact")?
.was_seen_recently(),

View File

@@ -174,6 +174,13 @@ pub enum EventType {
msg_id: u32,
},
/// A single message is deleted.
#[serde(rename_all = "camelCase")]
MsgDeleted {
chat_id: u32,
msg_id: u32,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
@@ -347,6 +354,10 @@ impl From<CoreEventType> for EventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
},

View File

@@ -113,7 +113,7 @@ impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::load_from_db(context, message.get_from_id())
let sender_contact = Contact::get_by_id(context, message.get_from_id())
.await
.context("failed to load sender contact")?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
@@ -135,7 +135,7 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::load_from_db(context, quote.get_from_id())
let quote_author = Contact::get_by_id(context, quote.get_from_id())
.await
.context("failed to load quote author contact")?;
Some(MessageQuote::WithMessage {
@@ -180,7 +180,7 @@ impl MessageObject {
from_id: message.get_from_id().to_u32(),
quote,
parent_id,
text: message.get_text(),
text: Some(message.get_text()).filter(|s| !s.is_empty()),
has_location: message.has_location(),
has_html: message.has_html(),
view_type: message.get_viewtype().into(),
@@ -469,7 +469,7 @@ impl MessageSearchResult {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
let profile_image = match sender.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
@@ -500,7 +500,7 @@ impl MessageSearchResult {
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text().unwrap_or_default(),
message: message.get_text(),
timestamp: message.get_timestamp(),
})
}

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.115.0"
"version": "1.118.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.115.0"
version = "1.118.0"
license = "MPL-2.0"
edition = "2021"
@@ -9,10 +9,10 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.16"
pretty_env_logger = "0.4"
log = "0.4.19"
pretty_env_logger = "0.5"
rusqlite = "0.29"
rustyline = "11"
rustyline = "12"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]

View File

@@ -199,7 +199,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
if msg.has_location() { "📍" } else { "" },
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
msgtext,
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == ContactId::SELF {
""
@@ -912,9 +912,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::File
});
msg.set_file(arg1, None);
if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string()));
}
msg.set_text(arg2.to_string());
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendhtml" => {
@@ -926,11 +924,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string()));
msg.set_text(Some(if arg2.is_empty() {
msg.set_text(if arg2.is_empty() {
path.file_name().unwrap().to_string_lossy().to_string()
} else {
arg2.to_string()
}));
});
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendsyncmsg" => match context.send_sync_msg().await? {
@@ -979,7 +977,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(Some(arg1.to_string()));
draft.set_text(arg1.to_string());
sel_chat
.as_ref()
.unwrap()
@@ -1003,7 +1001,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(arg1.to_string()));
msg.set_text(arg1.to_string());
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1090,7 +1088,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(&context, id).await?;
let res = id.get_info(&context).await?;
println!("{res}");
}
"download" => {

View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -9,6 +9,21 @@ dependencies = [
"aiohttp",
"aiodns"
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]
dynamic = [
"version"
]

View File

@@ -16,9 +16,8 @@ async def get_temp_credentials() -> dict:
# Replace default 5 minute timeout with a 1 minute timeout.
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession() as session:
async with session.post(url, timeout=timeout) as response:
return json.loads(await response.text())
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response:
return json.loads(await response.text())
class ACFactory:

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.115.0"
version = "1.118.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -17,9 +17,9 @@ anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
log = "0.4"
serde_json = "1.0.96"
serde_json = "1.0.99"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.28.0", features = ["io-std"] }
tokio = { version = "1.29.1", features = ["io-std"] }
tokio-util = "0.7.8"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }

View File

@@ -1,7 +1,7 @@
//! Delta Chat core RPC server.
//!
//! It speaks JSON Lines over stdio.
use std::env;
///! Delta Chat core RPC server.
///!
///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use std::sync::Arc;

View File

@@ -2,9 +2,6 @@
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
# Only affects windows if using non-default allocator (and unmaintained).
"RUSTSEC-2021-0145",
]
[bans]
@@ -13,12 +10,11 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "ahash", version = "0.7.6" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling_core", version = "<0.14" },
@@ -28,12 +24,12 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "env_logger", version = "<0.10" },
{ name = "getrandom", version = "<0.2" },
{ name = "hermit-abi", version = "<0.3" },
{ name = "humantime", version = "<2.1" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "idna", version = "<0.3" },
{ name = "libm", version = "0.1.4" },
{ name = "indexmap", version = "<2.0.0" },
{ name = "linux-raw-sys", version = "0.3.8" },
{ name = "num-derive", version = "0.3.3" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
@@ -42,9 +38,11 @@ skip = [
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.37.21" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "socket2", version = "0.4.9" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
@@ -57,8 +55,10 @@ skip = [
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "winreg", version = "0.10.1" },
]

128
fuzz/Cargo.lock generated
View File

@@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "abao"
version = "0.2.0"
@@ -60,19 +54,13 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "0.7.20"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@@ -189,12 +177,11 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.7.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25"
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
dependencies = [
"async-channel",
"async-native-tls 0.4.0",
"base64 0.21.0",
"byte-pool",
"chrono",
@@ -203,25 +190,13 @@ dependencies = [
"log",
"nom",
"once_cell",
"ouroboros",
"pin-utils",
"self_cell",
"stop-token",
"thiserror",
"tokio",
]
[[package]]
name = "async-native-tls"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
dependencies = [
"native-tls",
"thiserror",
"tokio",
"url",
]
[[package]]
name = "async-native-tls"
version = "0.5.0"
@@ -556,9 +531,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "byte-pool"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
dependencies = [
"crossbeam-queue",
"stable_deref_trait",
@@ -951,12 +926,12 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.112.6"
version = "1.117.0"
dependencies = [
"anyhow",
"async-channel",
"async-imap",
"async-native-tls 0.5.0",
"async-native-tls",
"async-smtp",
"async_zip",
"backtrace",
@@ -979,6 +954,7 @@ dependencies = [
"lettre_email",
"libc",
"mailparse 0.14.0",
"mime",
"num-derive",
"num-traits",
"num_cpus",
@@ -1244,7 +1220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
dependencies = [
"serde",
"signature 1.6.4",
"signature 2.1.0",
]
[[package]]
@@ -1701,9 +1677,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand",
"futures-core",
@@ -2397,9 +2373,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mime"
version = "0.3.16"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
@@ -2651,9 +2627,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.17.0"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
@@ -2663,9 +2639,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.48"
version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
@@ -2704,11 +2680,10 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.83"
version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"autocfg",
"cc",
"libc",
"openssl-src",
@@ -2716,29 +2691,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ouroboros"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
dependencies = [
"aliasable",
"ouroboros_macro",
]
[[package]]
name = "ouroboros_macro"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.107",
]
[[package]]
name = "overload"
version = "0.1.1"
@@ -2865,9 +2817,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pgp"
@@ -3316,13 +3268,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.0"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.7.2",
]
[[package]]
@@ -3331,7 +3283,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
"regex-syntax 0.6.28",
]
[[package]]
@@ -3341,10 +3293,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "reqwest"
version = "0.11.16"
name = "regex-syntax"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "reqwest"
version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -3690,6 +3648,12 @@ dependencies = [
"libc",
]
[[package]]
name = "self_cell"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
[[package]]
name = "semver"
version = "1.0.17"
@@ -4249,9 +4213,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.11"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -4275,9 +4239,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.7"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [
"bytes",
"futures-core",

View File

@@ -48,6 +48,7 @@ module.exports = {
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
DC_EVENT_MSGS_NOTICED: 2008,
DC_EVENT_MSG_DELETED: 2016,
DC_EVENT_MSG_DELIVERED: 2010,
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,

View File

@@ -22,6 +22,7 @@ module.exports = {
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
2016: 'DC_EVENT_MSG_DELETED',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',

View File

@@ -48,6 +48,7 @@ export enum C {
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
DC_EVENT_MSGS_NOTICED = 2008,
DC_EVENT_MSG_DELETED = 2016,
DC_EVENT_MSG_DELIVERED = 2010,
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
@@ -307,6 +308,7 @@ export const EventId2EventName: { [key: number]: string } = {
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
2016: 'DC_EVENT_MSG_DELETED',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.115.0"
"version": "1.118.0"
}

View File

@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE

View File

@@ -2,34 +2,34 @@
high level API reference
========================
- :class:`deltachat.account.Account` (your main entry point, creates the
- :class:`deltachat.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.contact.Contact`
- :class:`deltachat.chat.Chat`
- :class:`deltachat.message.Message`
- :class:`deltachat.Contact`
- :class:`deltachat.Chat`
- :class:`deltachat.Message`
Account
-------
.. autoclass:: deltachat.account.Account
.. autoclass:: deltachat.Account
:members:
Contact
-------
.. autoclass:: deltachat.contact.Contact
.. autoclass:: deltachat.Contact
:members:
Chat
----
.. autoclass:: deltachat.chat.Chat
.. autoclass:: deltachat.Chat
:members:
Message
-------
.. autoclass:: deltachat.message.Message
.. autoclass:: deltachat.Message
:members:

View File

@@ -11,10 +11,11 @@ authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email",
"Topic :: Software Development :: Libraries",
]

View File

@@ -6,7 +6,7 @@ from array import array
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
from . import const, hookspec
from .capi import ffi, lib

View File

@@ -71,9 +71,9 @@ class Contact:
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self):
def is_verified(self) -> bool:
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact)
return lib.dc_contact_is_verified(self._dc_contact) == 2
def get_verifier(self, contact):
"""Return the address of the contact that verified the contact."""

View File

@@ -9,11 +9,11 @@ from contextlib import contextmanager
from queue import Empty, Queue
from . import const
from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
from .account import Account
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):

View File

@@ -9,7 +9,7 @@ import threading
import time
import weakref
from queue import Queue
from typing import Callable, List, Optional, Dict, Set
from typing import Callable, Dict, List, Optional, Set
import pytest
import requests
@@ -137,6 +137,9 @@ def pytest_report_header(config, startdir):
@pytest.fixture(scope="session")
def testprocess(request):
"""Return live account configuration manager.
The returned object is a :class:`TestProcess` object."""
return TestProcess(pytestconfig=request.config)
@@ -231,6 +234,8 @@ def write_dict_to_dir(dic, target_dir):
@pytest.fixture()
def data(request):
"""Test data."""
class Data:
def __init__(self) -> None:
# trying to find test data heuristically
@@ -614,6 +619,7 @@ class ACFactory:
@pytest.fixture()
def acfactory(request, tmpdir, testprocess, data):
"""Account factory."""
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
@@ -686,11 +692,14 @@ class BotProcess:
@pytest.fixture()
def tmp_db_path(tmpdir):
"""Return a path inside the temporary directory where the database can be created."""
return tmpdir.join("test.db").strpath
@pytest.fixture()
def lp():
"""Log printer fixture."""
class Printer:
def sec(self, msg: str) -> None:
print()

View File

@@ -1,6 +1,6 @@
from queue import Queue
from threading import Event
from typing import List, TYPE_CHECKING
from typing import TYPE_CHECKING, List
from .hookspec import Global, account_hookimpl

View File

@@ -8,7 +8,7 @@ import pytest
import deltachat
def test_db_busy_error(acfactory, tmpdir):
def test_db_busy_error(acfactory):
starttime = time.time()
log_lock = threading.RLock()

View File

@@ -494,7 +494,7 @@ def test_multidevice_sync_seen(acfactory, lp):
assert "Expires: " in ac1_clone_message.get_message_info()
def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
@@ -507,9 +507,10 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
for ac in [ac1, ac1_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
dir = tmpdir.mkdir("exportdir")
ac1.export_self_keys(dir.strpath)
ac1_offl.import_self_keys(dir.strpath)
dir = tmp_path / "exportdir"
dir.mkdir()
ac1.export_self_keys(str(dir))
ac1_offl.import_self_keys(str(dir))
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
@@ -541,7 +542,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
ac1.set_config("bcc_self", "0")
def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
@@ -556,9 +557,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
dir = tmpdir.mkdir("exportdir")
ac2.export_self_keys(dir.strpath)
ac2_offl.import_self_keys(dir.strpath)
dir = tmp_path / "exportdir"
dir.mkdir()
ac2.export_self_keys(str(dir))
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")

View File

@@ -7,13 +7,12 @@ from datetime import datetime, timezone
import pytest
from imap_tools import AND, U
from deltachat import const
from deltachat.hookspec import account_hookimpl
from deltachat.message import Message
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
def test_basic_imap_api(acfactory, tmpdir):
def test_basic_imap_api(acfactory, tmp_path):
ac1, ac2 = acfactory.get_online_accounts(2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
@@ -28,7 +27,7 @@ def test_basic_imap_api(acfactory, tmpdir):
imap2.mark_all_read()
assert imap2.get_unread_cnt() == 0
imap2.dump_imap_structures(tmpdir, logfile=sys.stdout)
imap2.dump_imap_structures(tmp_path, logfile=sys.stdout)
imap2.shutdown()
@@ -36,8 +35,8 @@ def test_basic_imap_api(acfactory, tmpdir):
def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys.
acfactory.remove_preconfigured_keys()
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519))
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -72,35 +71,37 @@ def test_configure_canceled(acfactory):
pass
def test_configure_unref(tmpdir):
def test_configure_unref(tmp_path):
"""Test that removing the last reference to the context during ongoing configuration
does not result in use-after-free."""
from deltachat.capi import ffi, lib
path = tmpdir.mkdir("test_configure_unref").join("dc.db").strpath
dc_context = lib.dc_context_new(ffi.NULL, path.encode("utf8"), ffi.NULL)
path = tmp_path / "test_configure_unref"
path.mkdir()
dc_context = lib.dc_context_new(ffi.NULL, str(path / "dc.db").encode("utf8"), ffi.NULL)
lib.dc_set_config(dc_context, "addr".encode("utf8"), "foo@x.testrun.org".encode("utf8"))
lib.dc_set_config(dc_context, "mail_pw".encode("utf8"), "abc".encode("utf8"))
lib.dc_configure(dc_context)
lib.dc_context_unref(dc_context)
def test_export_import_self_keys(acfactory, tmpdir, lp):
def test_export_import_self_keys(acfactory, tmp_path, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
dir = tmpdir.mkdir("exportdir")
export_files = ac1.export_self_keys(dir.strpath)
dir = tmp_path / "exportdir"
dir.mkdir()
export_files = ac1.export_self_keys(str(dir))
assert len(export_files) == 2
for x in export_files:
assert x.startswith(dir.strpath)
assert x.startswith(str(dir))
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)")
for name in os.listdir(dir.strpath):
lp.indent(dir.strpath + os.sep + name)
for name in dir.iterdir():
lp.indent(str(dir / name))
lp.sec("importing into existing account")
ac2.import_self_keys(dir.strpath)
ac2.import_self_keys(str(dir))
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
assert key_id2 == key_id
@@ -156,25 +157,24 @@ def test_one_account_send_bcc_setting(acfactory, lp):
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "somedäüta.html.zip"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
f.write("some data")
p = tmp_path / basename
p.write_text("some data")
def send_and_receive_message():
lp.sec("ac1: prepare and send attachment + text to ac2")
msg1 = Message.new_empty(ac1, "file")
msg1.set_text("withfile")
msg1.set_file(p)
msg1.set_file(str(p))
chat.send_msg(msg1)
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
return ac2.get_message_by_id(ev.data2)
msg = send_and_receive_message()
@@ -189,25 +189,24 @@ def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
assert msg.filename != msg2.filename
def test_send_file_html_attachment(tmpdir, acfactory, lp):
def test_send_file_html_attachment(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "test.html"
content = "<html><body>text</body>data"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
f.write(content)
p = tmp_path / basename
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
p.write_text(content)
lp.sec("ac1: prepare and send attachment + text to ac2")
chat.send_file(p, mime_type="text/html")
chat.send_file(str(p), mime_type="text/html")
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
msg = ac2.get_message_by_id(ev.data2)
assert open(msg.filename).read() == content
@@ -351,7 +350,7 @@ def test_move_works(acfactory):
# Message is downloaded
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
def test_move_works_on_self_sent(acfactory):
@@ -396,9 +395,12 @@ def test_forward_messages(acfactory, lp):
lp.sec("ac2: check new chat has a forwarded message")
assert chat3.is_promoted()
messages = chat3.get_messages()
assert len(messages) == 1
msg = messages[-1]
assert msg.is_forwarded()
ac2.delete_messages(messages)
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_DELETED")
assert ev.data2 == messages[0].id
assert not chat3.get_messages()
@@ -531,8 +533,8 @@ def test_send_and_receive_message_markseen(acfactory, lp):
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
# Check that ac1 marks the read receipt as read.
@@ -1204,7 +1206,7 @@ def test_quote_encrypted(acfactory, lp):
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
def test_quote_attachment(tmpdir, acfactory, lp):
def test_quote_attachment(tmp_path, acfactory, lp):
"""Test that replies with an attachment and a quote are received correctly."""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1219,15 +1221,14 @@ def test_quote_attachment(tmpdir, acfactory, lp):
assert received_message.text == "hi"
basename = "attachment.txt"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
f.write("data to send")
p = tmp_path / basename
p.write_text("data to send")
lp.sec("ac2 sends a reply to ac1")
chat2 = received_message.create_chat()
reply = Message.new_empty(ac2, "file")
reply.set_text("message reply")
reply.set_file(p)
reply.set_file(str(p))
reply.quote = received_message
chat2.send_msg(reply)
@@ -1334,7 +1335,7 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
@@ -1369,10 +1370,9 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmpdir.join("large")
with open(path, "wb") as fout:
fout.write(os.urandom(download_limit + 1))
msgs.append(chat.send_file(path.strpath))
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
lp.sec("sending a reaction to the large message from ac1 to ac2")
react_str = "\N{THUMBS UP SIGN}"
@@ -1385,7 +1385,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
@@ -1431,7 +1431,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
assert reactions.get_by_contact(contacts[0]) == react_str
def test_import_export_online_all(acfactory, tmpdir, data, lp):
def test_import_export_online_all(acfactory, tmp_path, data, lp):
(ac1,) = acfactory.get_online_accounts(1)
lp.sec("create some chat content")
@@ -1443,10 +1443,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
path = tmpdir.join("attachment.txt")
with open(path, "w") as file:
path = tmp_path / "attachment.txt"
with path.open("w") as file:
file.truncate(100000)
chat1.send_file(path.strpath)
chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts(query="some1")
@@ -1464,12 +1464,13 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
assert_account_is_proper(ac1)
backupdir = tmpdir.mkdir("backup")
backupdir = tmp_path / "backup"
backupdir.mkdir()
lp.sec(f"export all to {backupdir}")
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
@@ -1487,7 +1488,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
ac2 = acfactory.get_unconfigured_account()
lp.sec("get latest backup file")
path2 = ac2.get_latest_backupfile(backupdir.strpath)
path2 = ac2.get_latest_backupfile(str(backupdir))
assert path2 == path
lp.sec("import backup and check it's proper")
@@ -1505,10 +1506,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
lp.sec(f"Second-time export all to {backupdir}")
ac1.stop_io()
path2 = ac1.export_all(backupdir.strpath)
path2 = ac1.export_all(str(backupdir))
assert os.path.exists(path2)
assert path2 != path
assert ac2.get_latest_backupfile(backupdir.strpath) == path2
assert ac2.get_latest_backupfile(str(backupdir)) == path2
def test_ac_setup_message(acfactory, lp):
@@ -1584,6 +1585,39 @@ def test_qr_join_chat(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
when the database already contained the contact with a different email address capitalization.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac1 adds ac2 as a contact with an email address in uppercase.
ac2_addr_uppercase = ac2.get_config("addr").upper()
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
ac1.create_contact(ac2_addr_uppercase)
lp.sec("ac3 creates a verified group with a QR code")
chat = ac3.create_group_chat("hello", verified=True)
qr = chat.get_join_qr()
lp.sec("ac1 joins a verified group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code")
ac2.qr_join_chat(qr)
ac1._evtracker.wait_next_incoming_message()
# ac1 should see both ac3 and ac2 as verified.
assert len(ac1_chat.get_contacts()) == 3
for contact in ac1_chat.get_contacts():
assert contact.is_verified()
def test_set_get_contact_avatar(acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1800,15 +1834,15 @@ def test_connectivity(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test stop_io() and start_io()")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
@@ -1829,8 +1863,8 @@ def test_connectivity(acfactory, lp):
ac2.create_chat(ac1).send_text("Hi 2")
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
@@ -1840,7 +1874,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -1855,7 +1889,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -1864,10 +1898,10 @@ def test_connectivity(acfactory, lp):
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(acfactory, lp):
@@ -2350,9 +2384,9 @@ def test_archived_muted_chat(acfactory, lp):
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
while 1:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK:
assert ev.data2 == 0
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK)
assert archive.count_fresh_messages() == 1
assert chat2.count_fresh_messages() == 1
break

View File

@@ -30,25 +30,26 @@ def wait_msgs_changed(account, msgs_list):
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
def test_increation_not_blobdir(self, tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join("file.txt").ensure(file=1)
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.touch()
with pytest.raises(Exception):
chat.prepare_message_file(src.strpath)
chat.prepare_message_file(str(src))
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join("file.txt")
src.write("hello there\n")
chat.send_file(src.strpath)
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.write_text("hello there\n")
chat.send_file(str(src))
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
assert os.path.exists(blob_src), "file.txt not copied to blobdir"

View File

@@ -4,12 +4,11 @@ from datetime import datetime, timedelta, timezone
import pytest
from deltachat import Account, const
import deltachat as dc
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from deltachat.hookspec import account_hookimpl
from deltachat.message import Message
from deltachat.tracker import ImexFailed
from deltachat import Account, account_hookimpl, Message
@pytest.mark.parametrize(
@@ -52,18 +51,18 @@ def test_parse_system_add_remove(msgtext, res):
class TestOfflineAccountBasic:
def test_wrong_db(self, tmpdir):
p = tmpdir.join("hello.db")
p.write("123")
account = Account(p.strpath)
def test_wrong_db(self, tmp_path):
p = tmp_path / "hello.db"
p.write_text("123")
account = Account(str(p))
assert not account.is_open()
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
def test_os_name(self, tmp_path):
p = tmp_path / "hello.db"
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
ac1 = Account(p.strpath, os_name="solarpunk")
ac1 = Account(str(p), os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, data):
@@ -299,13 +298,13 @@ class TestOfflineChat:
assert not 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_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
ac1.set_stock_translation(dc.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.set_stock_translation(dc.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")
@@ -481,6 +480,19 @@ class TestOfflineChat:
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
assert contact2.name == "real"
def test_send_lots_of_offline_msgs(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac1.set_config("configured_mail_server", "example.org")
ac1.set_config("configured_mail_user", "example.org")
ac1.set_config("configured_mail_pw", "example.org")
ac1.set_config("configured_send_server", "example.org")
ac1.set_config("configured_send_user", "example.org")
ac1.set_config("configured_send_pw", "example.org")
ac1.start_io()
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
for i in range(50):
chat.send_text("hello")
def test_create_chat_simple(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
@@ -496,22 +508,22 @@ class TestOfflineChat:
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
backupdir = tmpdir.mkdir("backup")
def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path):
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not backupdir.listdir()
assert not list(backupdir.iterdir())
ac1.stop_io()
path = ac1.export_all(backupdir.strpath)
path = ac1.export_all(str(backupdir))
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
@@ -525,27 +537,27 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_on_encrypted_acct(self, acfactory, tmpdir):
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
passphrase1 = "passphrase1"
passphrase2 = "passphrase2"
backupdir = tmpdir.mkdir("backup")
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not backupdir.listdir()
assert not list(backupdir.iterdir())
ac1.stop_io()
path = ac1.export_all(backupdir.strpath)
path = ac1.export_all(str(backupdir))
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account(closed=True)
@@ -580,27 +592,27 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_with_passphrase(self, acfactory, tmpdir):
def test_import_export_with_passphrase(self, acfactory, tmp_path):
passphrase = "test_passphrase"
wrong_passphrase = "wrong_passprase"
backupdir = tmpdir.mkdir("backup")
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not backupdir.listdir()
assert not list(backupdir.iterdir())
ac1.stop_io()
path = ac1.export_all(backupdir.strpath, passphrase)
path = ac1.export_all(str(backupdir), passphrase)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
@@ -619,7 +631,7 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir):
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
"""
Test that account passphrase isn't lost if backup failed to be imported.
See https://github.com/deltachat/deltachat-core-rust/issues/3379
@@ -627,24 +639,24 @@ class TestOfflineChat:
acct_passphrase = "passphrase1"
bak_passphrase = "passphrase2"
wrong_passphrase = "wrong_passprase"
backupdir = tmpdir.mkdir("backup")
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not backupdir.listdir()
assert not list(backupdir.iterdir())
ac1.stop_io()
path = ac1.export_all(backupdir.strpath, bak_passphrase)
path = ac1.export_all(str(backupdir), bak_passphrase)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account(closed=True)
@@ -805,7 +817,7 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref,
)
assert len(list(iter_array(dc_array, lambda x: x))) == 2

View File

@@ -1,7 +1,7 @@
import os
from queue import Queue
from deltachat import capi, const, cutil, register_global_plugin
import deltachat as dc
from deltachat import capi, cutil, register_global_plugin
from deltachat.capi import ffi, lib
from deltachat.hookspec import global_hookimpl
from deltachat.testplugin import (
@@ -65,16 +65,17 @@ class TestACSetup:
assert pc._account2state[ac1] == pc.IDLEREADY
assert pc._account2state[ac2] == pc.IDLEREADY
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path):
ac1 = acfactory.get_pseudo_configured_account()
holder = acfactory._acsetup.testprocess
assert holder.cache_maybe_store_configured_db_files(ac1)
assert not holder.cache_maybe_store_configured_db_files(ac1)
acdir = tmpdir.mkdir("newaccount")
acdir = tmp_path / "newaccount"
acdir.mkdir()
addr = ac1.get_config("addr")
target_db_path = acdir.join("db").strpath
assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
assert len(os.listdir(acdir)) >= 2
target_db_path = acdir / "db"
assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path))
assert sum(1 for _ in acdir.iterdir()) >= 2
def test_liveconfig_caching(acfactory, monkeypatch):
@@ -112,40 +113,40 @@ def test_dc_close_events(acfactory):
shutdowns.get(timeout=2)
def test_wrong_db(tmpdir):
p = tmpdir.join("hello.db")
def test_wrong_db(tmp_path):
p = tmp_path / "hello.db"
# write an invalid database file
p.write("x123" * 10)
p.write_bytes(b"x123" * 10)
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
context = lib.dc_context_new(ffi.NULL, str(p).encode("ascii"), ffi.NULL)
assert not lib.dc_context_is_open(context)
def test_empty_blobdir(tmpdir):
db_fname = tmpdir.join("hello.db")
def test_empty_blobdir(tmp_path):
db_fname = tmp_path / "hello.db"
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), b""),
lib.dc_context_unref,
)
assert ctx != ffi.NULL
def test_event_defines():
assert const.DC_EVENT_INFO == 100
assert const.DC_CONTACT_ID_SELF
assert dc.const.DC_EVENT_INFO == 100
assert dc.const.DC_CONTACT_ID_SELF
def test_sig():
sig = capi.lib.dc_event_has_string_data
assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
assert not sig(dc.const.DC_EVENT_MSGS_CHANGED)
assert sig(dc.const.DC_EVENT_INFO)
assert sig(dc.const.DC_EVENT_WARNING)
assert sig(dc.const.DC_EVENT_ERROR)
assert sig(dc.const.DC_EVENT_SMTP_CONNECTED)
assert sig(dc.const.DC_EVENT_IMAP_CONNECTED)
assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
@@ -174,10 +175,10 @@ def test_provider_info_none():
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_open(tmpdir):
db_fname = tmpdir.join("test.db")
def test_get_info_open(tmp_path):
db_fname = tmp_path / "test.db"
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
@@ -218,10 +219,10 @@ def test_logged_ac_process_ffi_failure(acfactory):
assert "Traceback" in res
def test_jsonrpc_blocking_call(tmpdir):
accounts_fname = tmpdir.join("accounts")
def test_jsonrpc_blocking_call(tmp_path):
accounts_fname = tmp_path / "accounts"
accounts = ffi.gc(
lib.dc_accounts_new(ffi.NULL, accounts_fname.strpath.encode("ascii")),
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)

View File

@@ -1 +1 @@
2023-05-12
2023-07-07

View File

@@ -18,6 +18,10 @@ and an own build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
- `make-python-testenv.sh` creates or updates local python test development environment.
Reusing the same environment is faster than running `run-python-test.sh` which always
recreates environment from scratch and runs additional lints.
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
- `run_all.sh` builds Python wheels

View File

@@ -12,10 +12,15 @@ where `secret.yml` contains the following secrets:
```
c.delta.chat:
private_key: |
-----BEGIN RSA PRIVATE KEY-----
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
-----END OPENSSH PRIVATE KEY-----
devpi:
login: dc
password: ...
```
Secrets can be read from the password manager:
```
fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml)
```

View File

@@ -153,11 +153,13 @@ jobs:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*manylinux201*
- name: python-aarch64
plan:
@@ -223,11 +225,13 @@ jobs:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*manylinux201*
- name: python-musl-x86_64
plan:
@@ -293,11 +297,13 @@ jobs:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*musllinux_1_1_x86_64*
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64*
- name: python-musl-aarch64
plan:
@@ -363,8 +369,10 @@ jobs:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*musllinux_1_1_aarch64*
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64*

21
scripts/make-python-testenv.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
#
# Script to create or update a python development environment.
# It rebuilds the core and bindings as needed.
#
# After running the script, you can either
# run `pytest` directly with `env/bin/pytest python/`
# or activate the environment with `. env/bin/activacte`
# and run `pytest` from there.
set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
if test -d env; then
env/bin/pip install -e python --force-reinstall
else
tox -e py --devenv env
env/bin/pip install --upgrade pip
fi

28
scripts/zig-musl-check.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
#
# Run `cargo check` with musl libc.
# This requires `zig` to compile vendored openssl.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.70.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo check --release --target x86_64-unknown-linux-musl -p deltachat_ffi --features jsonrpc

View File

@@ -8,7 +8,7 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.68.1
export RUSTUP_TOOLCHAIN=1.70.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e

View File

@@ -357,7 +357,6 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::message;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
@@ -705,7 +704,7 @@ Authentication-Results: dkim=";
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.as_ref().unwrap().contains("1234"));
assert!(!received.text.contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
@@ -786,7 +785,7 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())
}
@@ -825,7 +824,9 @@ Authentication-Results: dkim=";
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
assert!(message::get_msg_info(&bob, rcvd.id)
assert!(rcvd
.id
.get_info(&bob)
.await
.unwrap()
.contains("KEYCHANGES NOT ALLOWED"));

View File

@@ -461,7 +461,7 @@ impl ChatId {
promote: bool,
from_id: ContactId,
) -> Result<()> {
let msg_text = context.stock_protection_msg(protect, from_id).await;
let text = context.stock_protection_msg(protect, from_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
@@ -470,7 +470,7 @@ impl ChatId {
if promote {
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(msg_text),
text,
..Default::default()
};
msg.param.set_cmd(cmd);
@@ -479,7 +479,7 @@ impl ChatId {
add_info_msg_with_cmd(
context,
self,
&msg_text,
&text,
cmd,
create_smeared_timestamp(context),
None,
@@ -642,7 +642,7 @@ impl ChatId {
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::self_deleted_msg_body(context).await);
msg.text = stock_str::self_deleted_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
@@ -724,7 +724,7 @@ impl ChatId {
match msg.viewtype {
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
Viewtype::Text => {
if msg.text.is_none_or_empty() && msg.in_reply_to.is_none_or_empty() {
if msg.text.is_empty() && msg.in_reply_to.is_none_or_empty() {
bail!("No text and no quote in draft");
}
}
@@ -770,7 +770,7 @@ impl ChatId {
(
time(),
msg.viewtype,
msg.text.as_deref().unwrap_or(""),
&msg.text,
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
@@ -804,7 +804,7 @@ impl ChatId {
time(),
msg.viewtype,
MessageState::OutDraft,
msg.text.as_deref().unwrap_or(""),
&msg.text,
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
@@ -983,7 +983,7 @@ impl ChatId {
.iter()
.filter(|&contact_id| !contact_id.is_special())
{
let contact = Contact::load_from_db(context, *contact_id).await?;
let contact = Contact::get_by_id(context, *contact_id).await?;
let addr = contact.get_addr();
let peerstate = Peerstate::from_addr(context, addr).await?;
@@ -1358,7 +1358,7 @@ impl Chat {
/// deltachat, and the data returned is still subject to change.
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
let draft = match self.id.get_draft(context).await? {
Some(message) => message.text.unwrap_or_default(),
Some(message) => message.text,
_ => String::new(),
};
Ok(ChatInfo {
@@ -1400,6 +1400,7 @@ impl Chat {
}
/// Returns true if the chat is promoted.
/// This means a message has been sent to it and it _not_ only exists on the users device.
pub fn is_promoted(&self) -> bool {
!self.is_unpromoted()
}
@@ -1603,7 +1604,7 @@ impl Chat {
timestamp,
msg.viewtype,
msg.state,
msg.text.as_ref().cloned().unwrap_or_default(),
msg.text,
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -1652,7 +1653,7 @@ impl Chat {
timestamp,
msg.viewtype,
msg.state,
msg.text.as_ref().cloned().unwrap_or_default(),
msg.text,
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -1808,7 +1809,7 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
chat.param.set(Param::ProfileImage, &icon);
chat.update_param(context).await?;
let mut contact = Contact::load_from_db(context, ContactId::DEVICE).await?;
let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
contact.param.set(Param::ProfileImage, icon);
contact.update_param(context).await?;
}
@@ -1932,7 +1933,7 @@ impl ChatIdBlocked {
/// Returns the chat for the 1:1 chat with this contact.
///
/// I the chat does not yet exist a new one is created, using the provided [`Blocked`]
/// If the chat does not yet exist a new one is created, using the provided [`Blocked`]
/// state.
pub async fn get_for_contact(
context: &Context,
@@ -1950,7 +1951,7 @@ impl ChatIdBlocked {
return Ok(res);
}
let contact = Contact::load_from_db(context, contact_id).await?;
let contact = Contact::get_by_id(context, contact_id).await?;
let chat_name = contact.get_display_name().to_string();
let mut params = Params::new();
match contact_id {
@@ -2203,9 +2204,7 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages againts RTLO attacks
if msg.is_system_message() {
if let Some(text) = &msg.text {
msg.text = Some(strip_rtlo_characters(text.as_ref()));
}
msg.text = strip_rtlo_characters(&msg.text);
}
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
@@ -2396,7 +2395,7 @@ pub async fn send_text_msg(
);
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(text_to_send);
msg.text = text_to_send;
send_msg(context, chat_id, &mut msg).await
}
@@ -2422,10 +2421,9 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
msg.text = Some(
msg.text =
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
.await,
);
.await;
send_msg(context, chat_id, &mut msg).await
}
@@ -2969,6 +2967,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
}
/// Adds a contact to the chat.
/// If the group is promoted, also sends out a system message to all group members
pub async fn add_contact_to_chat(
context: &Context,
chat_id: ChatId,
@@ -2990,7 +2989,7 @@ pub(crate) async fn add_contact_to_chat_ex(
chat_id.reset_gossiped_timestamp(context).await?;
/*this also makes sure, not contacts are added to special or normal chats*/
// this also makes sure, no contacts are added to special or normal chats
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
@@ -3012,7 +3011,7 @@ pub(crate) async fn add_contact_to_chat_ex(
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot add contact to group; self not in group.".into(),
));
bail!("can not add contact because our account is not part of it");
bail!("can not add contact because the account is not part of the group/broadcast");
}
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
@@ -3055,10 +3054,10 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
msg.text =
Some(stock_str::msg_add_member(context, contact.get_addr(), ContactId::SELF).await);
let contact_addr = contact.get_addr();
msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
msg.id = send_msg(context, chat_id, &mut msg).await?;
}
@@ -3178,59 +3177,53 @@ pub async fn remove_contact_from_chat(
);
let mut msg = Message::default();
let mut success = false;
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot remove contact from chat; self not in group.".into(),
));
} else {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
if contact.id == ContactId::SELF {
set_group_explicitly_left(context, &chat.grpid).await?;
msg.text =
Some(stock_str::msg_group_left(context, ContactId::SELF).await);
} else {
msg.text = Some(
stock_str::msg_del_member(
context,
contact.get_addr(),
ContactId::SELF,
)
.await,
);
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.id = send_msg(context, chat_id, &mut msg).await?;
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
if !chat.is_self_in_chat(context).await? {
let err_msg = format!(
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
);
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{}", err_msg);
} else {
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
if contact.id == ContactId::SELF {
set_group_explicitly_left(context, &chat.grpid).await?;
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text = stock_str::msg_del_member_local(
context,
contact.get_addr(),
ContactId::SELF,
)
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.id = send_msg(context, chat_id, &mut msg).await?;
}
// we remove the member from the chat after constructing the
// to-be-send message. If between send_msg() and here the
// process dies the user will have to re-do the action. It's
// better than the other way round: you removed
// someone from DB but no peer or device gets to know about it and
// group membership is thus different on different devices.
// Note also that sending a message needs all recipients
// in order to correctly determine encryption so if we
// removed it first, it would complicate the
// check/encryption logic.
success = remove_from_chat_contacts_table(context, chat_id, contact_id)
.await
.is_ok();
context.emit_event(EventType::ChatModified(chat_id));
}
// we remove the member from the chat after constructing the
// to-be-send message. If between send_msg() and here the
// process dies the user will have to re-do the action. It's
// better than the other way round: you removed
// someone from DB but no peer or device gets to know about it and
// group membership is thus different on different devices.
// Note also that sending a message needs all recipients
// in order to correctly determine encryption so if we
// removed it first, it would complicate the
// check/encryption logic.
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}
if !success {
bail!("Failed to remove contact");
} else {
bail!("Cannot remove members from non-group chats.");
}
Ok(())
@@ -3291,9 +3284,8 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
&& improve_single_line_input(&chat.name) != new_name
{
msg.viewtype = Viewtype::Text;
msg.text = Some(
stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await,
);
msg.text =
stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::GroupNameChanged);
if !chat.name.is_empty() {
msg.param.set(Param::Arg, &chat.name);
@@ -3342,13 +3334,13 @@ pub async fn set_chat_profile_image(
if new_image.is_empty() {
chat.param.remove(Param::ProfileImage);
msg.param.remove(Param::Arg);
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
msg.text = stock_str::msg_grp_img_deleted(context, ContactId::SELF).await;
} else {
let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
msg.text = Some(stock_str::msg_grp_img_changed(context, ContactId::SELF).await);
msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
}
chat.update_param(context).await?;
if chat.is_promoted() && !chat.is_mailing_list() {
@@ -3623,7 +3615,7 @@ pub async fn add_device_msg_with_importance(
timestamp_sent, // timestamp_sent equals timestamp_rcvd
msg.viewtype,
state,
msg.text.as_ref().cloned().unwrap_or_default(),
&msg.text,
msg.param.to_string(),
rfc724_mid,
),
@@ -3797,7 +3789,7 @@ mod tests {
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::test_utils::{TestContext, TestContextManager};
use tokio::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3860,7 +3852,7 @@ mod tests {
let t = TestContext::new().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let draft = chat_id.get_draft(&t).await.unwrap().unwrap();
@@ -3875,12 +3867,12 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hi!".to_string()));
msg.set_text("hi!".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("another".to_string()));
msg.set_text("another".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
@@ -3895,7 +3887,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
@@ -3909,7 +3901,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
msg.set_text("hello".to_string());
assert_eq!(msg.id, MsgId::new_unset());
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
@@ -3922,7 +3914,7 @@ mod tests {
);
assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id);
msg.set_text(Some("hello2".to_string()));
msg.set_text("hello2".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
let id_after_2nd_set = msg.id;
@@ -3934,7 +3926,7 @@ mod tests {
let test = chat_id.get_draft(&t).await?.unwrap();
assert_eq!(id_after_2nd_set, test.id);
assert_eq!(id_after_2nd_set, msg.id);
assert_eq!(test.text, Some("hello2".to_string()));
assert_eq!(test.text, "hello2".to_string());
assert_eq!(test.state, MessageState::OutDraft);
let id_after_prepare = prepare_msg(&t, *chat_id, &mut msg).await?;
@@ -3962,11 +3954,11 @@ mod tests {
// save a draft
let mut draft = Message::new(Viewtype::Text);
draft.set_text(Some("draft text".to_string()));
draft.set_text("draft text".to_string());
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("draft text".to_string()));
assert_eq!(test.text, "draft text".to_string());
assert!(test.quoted_text().is_none());
assert!(test.quoted_message(&t).await?.is_none());
@@ -3975,17 +3967,17 @@ mod tests {
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("draft text".to_string()));
assert_eq!(test.text, "draft text".to_string());
assert_eq!(test.quoted_text(), Some("quote1".to_string()));
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id);
// change quote on same message object
draft.set_text(Some("another draft text".to_string()));
draft.set_text("another draft text".to_string());
draft.set_quote(&t, Some(&quote2)).await?;
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("another draft text".to_string()));
assert_eq!(test.text, "another draft text".to_string());
assert_eq!(test.quoted_text(), Some("quote2".to_string()));
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id);
@@ -3994,7 +3986,7 @@ mod tests {
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
assert_eq!(test.text, Some("another draft text".to_string()));
assert_eq!(test.text, "another draft text".to_string());
assert!(test.quoted_text().is_none());
assert!(test.quoted_message(&t).await?.is_none());
@@ -4014,6 +4006,144 @@ mod tests {
assert_eq!(added, false);
}
/// Test adding and removing members in a group chat.
///
/// Make sure messages sent outside contain authname
/// and displayed messages contain locally set name.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_member_add_remove() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Disable encryption so we can inspect raw message contents.
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
// Create contact for Bob on the Alice side with name "robert".
let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
// Set Bob authname to "Bob" and send it to Alice.
bob.set_config(Config::Displayname, Some("Bob")).await?;
tcm.send_recv(&bob, &alice, "Hello!").await;
// Check that Alice has Bob's name set to "robert" and authname set to "Bob".
{
let alice_bob_contact = Contact::get_by_id(&alice, alice_bob_contact_id).await?;
assert_eq!(alice_bob_contact.get_name(), "robert");
// This is the name that will be sent outside.
assert_eq!(alice_bob_contact.get_authname(), "Bob");
assert_eq!(alice_bob_contact.get_display_name(), "robert");
}
// Create and promote a group.
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let sent = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
assert!(sent.payload.contains("Hi! I created a group."));
// Alice adds Bob to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
assert!(sent
.payload
.contains("I added member Bob (bob@example.net)."));
// Locally set name "robert" should not leak.
assert!(!sent.payload.contains("robert"));
assert_eq!(
sent.load_from_db().await.get_text(),
"You added member robert (bob@example.net)."
);
// Alice removes Bob from the chat.
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
assert!(sent
.payload
.contains("I removed member Bob (bob@example.net)."));
assert!(!sent.payload.contains("robert"));
assert_eq!(
sent.load_from_db().await.get_text(),
"You removed member robert (bob@example.net)."
);
// Alice leaves the chat.
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
let sent = alice.pop_sent_msg().await;
assert!(sent.payload.contains("I left the group."));
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
Ok(())
}
/// Test simultaneous removal of user from the chat and leaving the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_simultaneous_member_remove() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
let alice_claire_contact_id =
Contact::create(&alice, "Claire", "claire@example.net").await?;
// Create and promote a group.
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let alice_sent_msg = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
let bob_received_msg = bob.recv_msg(&alice_sent_msg).await;
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(&bob).await?;
// Alice adds Claire to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
let alice_sent_add_msg = alice.pop_sent_msg().await;
// Alice removes Bob from the chat.
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let alice_sent_remove_msg = alice.pop_sent_msg().await;
// Bob leaves the chat.
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
// Bob receives a msg about Alice adding Claire to the group.
let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await;
// Test that add message is rewritten.
assert_eq!(
bob_received_add_msg.get_text(),
"Member claire@example.net added by alice@example.org."
);
// Bob receives a msg about Alice removing him from the group.
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
// Test that remove message is rewritten.
assert_eq!(
bob_received_remove_msg.get_text(),
"Member Me (bob@example.net) removed by alice@example.org."
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_multi_device() -> Result<()> {
let a1 = TestContext::new_alice().await;
@@ -4210,6 +4340,7 @@ mod tests {
Ok(())
}
/// Test that adding or removing contacts in 1:1 chat is not allowed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_remove_contact_for_single() {
let ctx = TestContext::new_alice().await;
@@ -4257,7 +4388,7 @@ mod tests {
t2.recv_msg(&sent_msg).await;
let chat = &t2.get_self_chat().await;
let msg = t2.get_last_msg_in(chat.id).await;
assert_eq!(msg.text, Some("foo self".to_string()));
assert_eq!(msg.text, "foo self".to_string());
assert_eq!(msg.from_id, ContactId::SELF);
assert_eq!(msg.to_id, ContactId::SELF);
assert!(msg.get_showpadlock());
@@ -4271,12 +4402,12 @@ mod tests {
// add two device-messages
let mut msg1 = Message::new(Viewtype::Text);
msg1.text = Some("first message".to_string());
msg1.set_text("first message".to_string());
let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
let mut msg2 = Message::new(Viewtype::Text);
msg2.text = Some("second message".to_string());
msg2.set_text("second message".to_string());
let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
@@ -4285,7 +4416,7 @@ mod tests {
let msg1 = message::Message::load_from_db(&t, msg1_id.unwrap()).await;
assert!(msg1.is_ok());
let msg1 = msg1.unwrap();
assert_eq!(msg1.text.as_ref().unwrap(), "first message");
assert_eq!(msg1.text, "first message");
assert_eq!(msg1.from_id, ContactId::DEVICE);
assert_eq!(msg1.to_id, ContactId::SELF);
assert!(!msg1.is_info());
@@ -4294,7 +4425,7 @@ mod tests {
let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await;
assert!(msg2.is_ok());
let msg2 = msg2.unwrap();
assert_eq!(msg2.text.as_ref().unwrap(), "second message");
assert_eq!(msg2.text, "second message");
// check device chat
assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2);
@@ -4306,13 +4437,13 @@ mod tests {
// add two device-messages with the same label (second attempt is not added)
let mut msg1 = Message::new(Viewtype::Text);
msg1.text = Some("first message".to_string());
msg1.text = "first message".to_string();
let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
assert!(!msg1_id.as_ref().unwrap().is_unset());
let mut msg2 = Message::new(Viewtype::Text);
msg2.text = Some("second message".to_string());
msg2.text = "second message".to_string();
let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert!(msg2_id.as_ref().unwrap().is_unset());
@@ -4320,7 +4451,7 @@ mod tests {
// check added message
let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await?;
assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id);
assert_eq!(msg1.text.as_ref().unwrap(), "first message");
assert_eq!(msg1.text, "first message");
assert_eq!(msg1.from_id, ContactId::DEVICE);
assert_eq!(msg1.to_id, ContactId::SELF);
assert!(!msg1.is_info());
@@ -4360,7 +4491,7 @@ mod tests {
assert!(res.is_ok());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
msg.set_text("message text".to_string());
let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await;
assert!(msg_id.is_ok());
@@ -4378,7 +4509,7 @@ mod tests {
assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
msg.set_text("message text".to_string());
add_device_msg(&t, Some("another-label"), Some(&mut msg))
.await
.ok();
@@ -4396,7 +4527,7 @@ mod tests {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
msg.set_text("message text".to_string());
add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.ok();
@@ -4420,7 +4551,7 @@ mod tests {
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
msg.set_text("message text".to_string());
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
@@ -4432,7 +4563,7 @@ mod tests {
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
msg.set_text("message text".to_string());
let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.unwrap();
@@ -4465,7 +4596,7 @@ mod tests {
// create two chats
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("foo".to_string());
msg.set_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -4746,7 +4877,7 @@ mod tests {
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("foo".to_string());
msg.set_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -4824,7 +4955,7 @@ mod tests {
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hi!".into()));
msg.set_text("hi!".into());
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, alice_chat_id);
@@ -4962,7 +5093,7 @@ mod tests {
let msg = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert_eq!(msg.get_text().unwrap(), "foo info");
assert_eq!(msg.get_text(), "foo info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::Unknown);
assert!(msg.parent(&t).await?.is_none());
@@ -4989,7 +5120,7 @@ mod tests {
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert_eq!(msg.get_text().unwrap(), "foo bar info");
assert_eq!(msg.get_text(), "foo bar info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged);
assert!(msg.parent(&t).await?.is_none());
@@ -5176,7 +5307,7 @@ mod tests {
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, alice_chat_id);
assert_eq!(msg.text, Some("ho!".to_string()));
assert_eq!(msg.text, "ho!".to_string());
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 2);
Ok(())
}
@@ -5470,7 +5601,7 @@ mod tests {
let bob_chat = bob.create_chat(&alice).await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Hi Bob".to_owned()));
msg.set_text("Hi Bob".to_owned());
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
@@ -5478,7 +5609,7 @@ mod tests {
let forwarded_msg = bob.pop_sent_msg().await;
let msg = alice.recv_msg(&forwarded_msg).await;
assert!(msg.get_text().unwrap() == "Hi Bob");
assert_eq!(msg.get_text(), "Hi Bob");
assert!(msg.is_forwarded());
Ok(())
}
@@ -5493,7 +5624,7 @@ mod tests {
add_contact_to_chat(&t, chat_id1, bob_id).await?;
let msg1 = t.get_last_msg_in(chat_id1).await;
assert!(msg1.is_info());
assert!(msg1.get_text().unwrap().contains("bob@example.net"));
assert!(msg1.get_text().contains("bob@example.net"));
let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0);
@@ -5503,7 +5634,7 @@ mod tests {
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
assert_ne!(msg2.from_id, ContactId::INFO);
assert_ne!(msg2.to_id, ContactId::INFO);
assert_eq!(msg2.get_text().unwrap(), msg1.get_text().unwrap());
assert_eq!(msg2.get_text(), msg1.get_text());
assert!(msg2.is_forwarded());
Ok(())
@@ -5522,7 +5653,7 @@ mod tests {
// Bob quotes received message and sends a reply to Alice.
let mut reply = Message::new(Viewtype::Text);
reply.set_text(Some("Reply".to_owned()));
reply.set_text("Reply".to_owned());
reply.set_quote(&bob, Some(&received_msg)).await?;
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
let received_reply = alice.recv_msg(&sent_reply).await;
@@ -5573,13 +5704,13 @@ mod tests {
// Alice sends a message to Bob.
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
let received_msg = bob.recv_msg(&sent_msg).await;
assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
assert_eq!(received_msg.get_text(), "Hi Bob");
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;
let received_msg = bob.recv_msg(&sent_msg).await;
assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
assert_eq!(received_msg.get_text(), "Hello Bob");
assert_eq!(received_msg.chat_id, bob_chat.id);
// Bob forwards message to a group chat with Alice.
@@ -5606,7 +5737,7 @@ mod tests {
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
add_contact_to_chat(&alice, group_id, bob_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("bla foo".to_owned()));
msg.set_text("bla foo".to_owned());
let sent_msg = alice.send_msg(group_id, &mut msg).await;
assert!(sent_msg.payload().contains("secretgrpname"));
assert!(sent_msg.payload().contains("secretname"));
@@ -5663,7 +5794,7 @@ mod tests {
// Bob receives all messages
let bob = TestContext::new_bob().await;
let msg = bob.recv_msg(&sent1).await;
assert_eq!(msg.get_text().unwrap(), "alice->bob");
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
bob.recv_msg(&sent2).await;
@@ -5680,7 +5811,7 @@ mod tests {
claire.configure_addr("claire@example.org").await;
claire.recv_msg(&sent2).await;
let msg = claire.recv_msg(&sent3).await;
assert_eq!(msg.get_text().unwrap(), "alice->bob");
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
@@ -5828,7 +5959,7 @@ mod tests {
assert_eq!(msg.chat_id, chat.id);
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), Some("ola!".to_string()));
assert_eq!(msg.get_text(), "ola!");
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Single);

View File

@@ -319,7 +319,7 @@ impl Chatlist {
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::load_from_db(context, lastmsg.from_id)
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
@@ -424,7 +424,7 @@ mod tests {
// 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()));
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
@@ -636,7 +636,7 @@ mod tests {
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
msg.set_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

View File

@@ -145,7 +145,7 @@ pub enum Config {
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
#[strum(props(default = "1"))]
#[strum(props(default = "0"))]
FetchExistingMsgs,
/// If set to "1", then existing messages are considered to be already fetched.

View File

@@ -146,7 +146,7 @@ async fn on_configure_completed(
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
msg.text = provider.after_login_hint.to_string();
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
@@ -161,7 +161,7 @@ async fn on_configure_completed(
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")

View File

@@ -338,11 +338,33 @@ impl Default for VerifiedStatus {
}
impl Contact {
/// Loads a contact snapshot from the database.
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
let mut contact = context
/// Loads a single contact object from the database.
///
/// Returns an error if the contact does not exist.
///
/// For contact ContactId::SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by set_config().
///
/// For contact ContactId::DEVICE, the function overrides
/// the contact name and status with localized address.
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Self> {
let contact = Self::get_by_id_optional(context, contact_id)
.await?
.with_context(|| format!("contact {contact_id} not found"))?;
Ok(contact)
}
/// Loads a single contact object from the database.
///
/// Similar to [`Contact::get_by_id()`] but returns `None` if the contact does not exist.
pub async fn get_by_id_optional(
context: &Context,
contact_id: ContactId,
) -> Result<Option<Self>> {
if let Some(mut contact) = context
.sql
.query_row(
.query_row_optional(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
c.authname, c.param, c.status
FROM contacts c
@@ -371,23 +393,27 @@ impl Contact {
Ok(contact)
},
)
.await?;
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
contact.status = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
} else if contact_id == ContactId::DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = ContactId::DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
.await?
{
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
contact.status = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
} else if contact_id == ContactId::DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = ContactId::DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(Some(contact))
} else {
Ok(None)
}
Ok(contact)
}
/// Returns `true` if this contact is blocked.
@@ -407,7 +433,13 @@ impl Contact {
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
let blocked = context
.sql
.query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| {
let blocked: bool = row.get(0)?;
Ok(blocked)
})
.await?;
Ok(blocked)
}
@@ -959,7 +991,7 @@ impl Contact {
);
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
@@ -1046,17 +1078,6 @@ impl Contact {
Ok(())
}
/// Get a single contact object. For a list, see eg. get_contacts().
///
/// For contact ContactId::SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by set_config().
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
let contact = Contact::load_from_db(context, contact_id).await?;
Ok(contact)
}
/// Updates `param` column in the database.
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
@@ -1120,11 +1141,29 @@ impl Contact {
&self.addr
}
/// Get a summary of authorized name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
/// This string is suitable for sending over email
/// as it does not leak the locally set name.
pub fn get_authname_n_addr(&self) -> String {
if !self.authname.is_empty() {
format!("{} ({})", self.authname, self.addr)
} else {
(&self.addr).into()
}
}
/// Get a summary of name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
/// The result should only be used locally and never sent over the network
/// as it leaks the local contact name.
///
/// The summary is typically used when asking the user something about the contact.
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
pub fn get_name_n_addr(&self) -> String {
@@ -1317,7 +1356,7 @@ async fn set_block_contact(
contact_id
);
let contact = Contact::load_from_db(context, contact_id).await?;
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.blocked != new_blocking {
context
@@ -1379,7 +1418,7 @@ pub(crate) async fn set_profile_image(
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
let mut contact = Contact::load_from_db(context, contact_id).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == ContactId::SELF {
@@ -1434,7 +1473,7 @@ pub(crate) async fn set_status(
.await?;
}
} else {
let mut contact = Contact::load_from_db(context, contact_id).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.status != status {
contact.status = status;
@@ -1752,7 +1791,7 @@ mod tests {
.await?;
assert_ne!(id, ContactId::UNDEFINED);
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "bob");
@@ -1780,7 +1819,7 @@ mod tests {
.await?;
assert_eq!(contact_bob_id, id);
assert_eq!(modified, Modifier::Modified);
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "someone");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "someone");
@@ -1846,7 +1885,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_id(), contact_id);
assert_eq!(contact.get_name(), "Name one");
assert_eq!(contact.get_authname(), "bla foo");
@@ -1865,7 +1904,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Real one");
assert_eq!(contact.get_addr(), "one@eins.org");
assert!(!contact.is_blocked());
@@ -1881,7 +1920,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "three@drei.sam");
assert_eq!(contact.get_addr(), "three@drei.sam");
@@ -1898,7 +1937,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
assert!(!contact.is_blocked());
@@ -1913,7 +1952,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "m. serious");
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
assert!(!contact.is_blocked());
@@ -1929,14 +1968,14 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Wonderland, Alice");
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
assert_eq!(contact.get_addr(), "alice@w.de");
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
// check SELF
let contact = Contact::load_from_db(&t, ContactId::SELF).await.unwrap();
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());
@@ -1967,7 +2006,7 @@ mod tests {
assert_eq!(chatlist.len(), 1);
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = contacts.first().unwrap();
let contact = Contact::load_from_db(&t, *contact_id).await?;
let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "f@example.org");
@@ -1993,7 +2032,7 @@ mod tests {
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo");
let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?;
assert_eq!(chatlist.len(), 1);
let contact = Contact::load_from_db(&t, *contact_id).await?;
let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Flobbyfoo");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Flobbyfoo");
@@ -2023,7 +2062,7 @@ mod tests {
assert_eq!(chatlist.len(), 0);
let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?;
assert_eq!(chatlist.len(), 1);
let contact = Contact::load_from_db(&t, *contact_id).await?;
let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Foo Flobby");
@@ -2041,7 +2080,7 @@ mod tests {
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk");
let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?;
assert_eq!(chatlist.len(), 1);
let contact = Contact::load_from_db(&t, *contact_id).await?;
let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "Falk");
assert_eq!(contact.get_display_name(), "Falk");
@@ -2080,7 +2119,7 @@ mod tests {
// If a contact has ongoing chats, contact is only hidden on deletion
Contact::delete(&alice, contact_id).await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
@@ -2094,7 +2133,7 @@ mod tests {
// Can delete contact physically now
Contact::delete(&alice, contact_id).await?;
assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
assert!(Contact::get_by_id(&alice, contact_id).await.is_err());
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
@@ -2113,7 +2152,7 @@ mod tests {
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Contact::delete(&t, contact_id1).await?;
assert!(Contact::load_from_db(&t, contact_id1).await.is_err());
assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_ne!(contact_id2, contact_id1);
@@ -2122,12 +2161,12 @@ mod tests {
// test recreation after hiding
t.create_chat_with_contact("Foo", "foo@bar.de").await;
Contact::delete(&t, contact_id2).await?;
let contact = Contact::load_from_db(&t, contact_id2).await?;
let contact = Contact::get_by_id(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
let contact = Contact::load_from_db(&t, contact_id3).await?;
let contact = Contact::get_by_id(&t, contact_id3).await?;
assert_eq!(contact.origin, Origin::ManuallyCreated);
assert_eq!(contact_id3, contact_id2);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
@@ -2150,7 +2189,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob1");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "bob1");
@@ -2166,7 +2205,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "bob2");
@@ -2176,7 +2215,7 @@ mod tests {
.await
.unwrap();
assert!(!contact_id.is_special());
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
@@ -2192,7 +2231,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob4");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
@@ -2205,7 +2244,7 @@ mod tests {
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap();
assert!(!contact_id.is_special());
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
@@ -2221,7 +2260,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire1");
@@ -2237,7 +2276,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire2");
@@ -2260,7 +2299,7 @@ mod tests {
)
.await?;
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t, contact_id).await?;
let contact = Contact::get_by_id(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
// Incoming message from someone else with "Not Bob" <bob@example.org> in the "To:" field.
@@ -2273,7 +2312,7 @@ mod tests {
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await?;
let contact = Contact::get_by_id(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Not Bob");
// Incoming message from Bob, changing the name back.
@@ -2286,7 +2325,7 @@ mod tests {
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix
let contact = Contact::load_from_db(&t, contact_id).await?;
let contact = Contact::get_by_id(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
Ok(())
@@ -2300,7 +2339,7 @@ mod tests {
let contact_id = Contact::create(&t, "dave1", "dave@example.org")
.await
.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
@@ -2314,14 +2353,14 @@ mod tests {
)
.await
.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t, "", "dave@example.org").await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "dave2");
@@ -2339,21 +2378,21 @@ mod tests {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "", "<dave@example.org>").await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t, "", "Mueller, Dave <dave@example.org>")
.await
.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Mueller, Dave");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t, "name1", "name2 <dave@example.org>")
.await
.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "name1");
assert_eq!(contact.get_addr(), "dave@example.org");
@@ -2597,7 +2636,7 @@ CCCB 5AA9 F6E1 141C 9431
Origin::ManuallyCreated,
)
.await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), 0);
let mime = br#"Subject: Hello
@@ -2614,7 +2653,7 @@ Hi."#;
let timestamp = msg.get_timestamp();
assert!(timestamp > 0);
let contact = Contact::load_from_db(&alice, contact_id).await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), timestamp);
Ok(())

View File

@@ -1042,7 +1042,7 @@ mod tests {
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await.unwrap();
let contact = Contact::load_from_db(t, *members.first().unwrap())
let contact = Contact::get_by_id(t, *members.first().unwrap())
.await
.unwrap();
let msg = format!(
@@ -1304,11 +1304,11 @@ mod tests {
// Add messages to chat with Bob.
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text(Some("foobar".to_string()));
msg1.set_text("foobar".to_string());
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text(Some("barbaz".to_string()));
msg2.set_text("barbaz".to_string());
send_msg(&alice, chat.id, &mut msg2).await?;
// Global search with a part of text finds the message.
@@ -1404,7 +1404,7 @@ mod tests {
// Add 999 messages
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foobar".to_string()));
msg.set_text("foobar".to_string());
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}

View File

@@ -399,7 +399,7 @@ mod tests {
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
assert_eq!(msg.text, "Hello from Thunderbird!");
Ok(())
}

View File

@@ -10,10 +10,11 @@ use quick_xml::{
Reader,
};
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
use crate::simplify::{simplify_quote, SimplifiedText};
struct Dehtml {
strbuilder: String,
quote: String,
add_text: AddText,
last_href: Option<String>,
/// GMX wraps a quote in `<div name="quote">`. After a `<div name="quote">`, this count is
@@ -29,17 +30,22 @@ struct Dehtml {
}
impl Dehtml {
fn line_prefix(&self) -> &str {
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
"> "
/// Returns true if HTML parser is currently inside the quote.
fn is_quote(&self) -> bool {
self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0
}
/// Returns the buffer where the text should be written.
///
/// If the parser is inside the quote, returns the quote buffer.
fn get_buf(&mut self) -> &mut String {
if self.is_quote() {
&mut self.quote
} else {
""
&mut self.strbuilder
}
}
fn append_prefix(&self, line_end: &str) -> String {
// line_end is e.g. "\n\n". We add "> " if necessary.
line_end.to_string() + self.line_prefix()
}
fn get_add_text(&self) -> AddText {
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
AddText::No // Everything between `<div name="quoted">` and `<div name="quoted_content">` is metadata which we don't want
@@ -51,30 +57,70 @@ impl Dehtml {
#[derive(Debug, PartialEq, Clone, Copy)]
enum AddText {
/// Inside `<script>`, `<style>` and similar tags
/// which contents should not be displayed.
No,
YesRemoveLineEnds,
/// Inside `<pre>`.
YesPreserveLineEnds,
}
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller
pub fn dehtml(buf: &str) -> Option<String> {
let s = dehtml_quick_xml(buf);
pub(crate) fn dehtml(buf: &str) -> Option<SimplifiedText> {
let (s, quote) = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
return Some(s);
let text = dehtml_cleanup(s);
let top_quote = if !quote.trim().is_empty() {
Some(dehtml_cleanup(simplify_quote(&quote).0))
} else {
None
};
return Some(SimplifiedText {
text,
top_quote,
..Default::default()
});
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
return Some(s);
let text = dehtml_cleanup(s);
return Some(SimplifiedText {
text,
..Default::default()
});
}
None
}
fn dehtml_quick_xml(buf: &str) -> String {
fn dehtml_cleanup(mut text: String) -> String {
text.retain(|c| c != '\r');
let lines = text.trim().split('\n');
let mut text = String::new();
let mut linebreak = false;
for line in lines {
if line.chars().all(char::is_whitespace) {
linebreak = true;
} else {
if !text.is_empty() {
text += "\n";
if linebreak {
text += "\n";
}
}
text += line.trim_end();
linebreak = false;
}
}
text
}
fn dehtml_quick_xml(buf: &str) -> (String, String) {
let buf = buf.trim().trim_start_matches("<!doctype html>");
let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf.len()),
quote: String::new(),
add_text: AddText::YesRemoveLineEnds,
last_href: None,
divs_since_quote_div: 0,
@@ -126,22 +172,33 @@ fn dehtml_quick_xml(buf: &str) -> String {
buf.clear();
}
dehtml.strbuilder
(dehtml.strbuilder, dehtml.quote)
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
{
let last_added = escaper::decode_html_buf_sloppy(event as &[_]).unwrap_or_default();
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
} else if !dehtml.line_prefix().is_empty() {
let l = dehtml.append_prefix("\n");
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
// Replace all line ends with spaces.
// E.g. `\r\n\r\n` is replaced with one space.
let last_added = LINE_RE.replace_all(&last_added, " ");
// Add a space if `last_added` starts with a space
// and there is no whitespace at the end of the buffer yet.
// Trim the rest of leading whitespace from `last_added`.
let buf = dehtml.get_buf();
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
*buf += " ";
}
*buf += last_added.trim_start();
} else {
dehtml.strbuilder += &last_added;
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
}
}
}
@@ -152,32 +209,37 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
.to_lowercase();
match tag.as_str() {
"p" | "table" | "td" | "style" | "script" | "title" | "pre" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
"style" | "script" | "title" | "pre" => {
*dehtml.get_buf() += "\n\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"div" => {
pop_tag(&mut dehtml.divs_since_quote_div);
pop_tag(&mut dehtml.divs_since_quoted_content_div);
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
*dehtml.get_buf() += "\n\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"a" => {
if let Some(ref last_href) = dehtml.last_href.take() {
dehtml.strbuilder += "](";
dehtml.strbuilder += last_href;
dehtml.strbuilder += ")";
let buf = dehtml.get_buf();
if buf.ends_with('[') {
buf.truncate(buf.len() - 1);
} else {
*buf += "](";
*buf += last_href;
*buf += ")";
}
}
}
"b" | "strong" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "*";
*dehtml.get_buf() += "*";
}
}
"i" | "em" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "_";
*dehtml.get_buf() += "_";
}
}
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
@@ -196,7 +258,9 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
match tag.as_str() {
"p" | "table" | "td" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
if !dehtml.strbuilder.is_empty() {
*dehtml.get_buf() += "\n\n";
}
dehtml.add_text = AddText::YesRemoveLineEnds;
}
#[rustfmt::skip]
@@ -204,18 +268,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
*dehtml.get_buf() += "\n\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"br" => {
dehtml.strbuilder += &dehtml.append_prefix("\n");
*dehtml.get_buf() += "\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"style" | "script" | "title" => {
dehtml.add_text = AddText::No;
}
"pre" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
*dehtml.get_buf() += "\n\n";
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
@@ -236,18 +300,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
if !href.is_empty() {
dehtml.last_href = Some(href);
dehtml.strbuilder += "[";
*dehtml.get_buf() += "[";
}
}
}
"b" | "strong" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "*";
*dehtml.get_buf() += "*";
}
}
"i" | "em" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "_";
*dehtml.get_buf() += "_";
}
}
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
@@ -308,7 +372,6 @@ pub fn dehtml_manually(buf: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::simplify::{simplify, SimplifiedText};
#[test]
fn test_dehtml() {
@@ -322,17 +385,18 @@ mod tests {
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Despite missing ', this should be shown:
("<a href='/foo.png>Hi</a> ", "Hi "),
("<a href='/foo.png>Hi</a> ", "Hi"),
("No link: <a href='https://get.delta.chat/'/>", "No link:"),
(
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
"No link: <a href='https://get.delta.chat/'></a>",
"No link:",
),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
];
for (input, output) in cases {
assert_eq!(simplify(dehtml(input).unwrap(), true).text, output);
assert_eq!(dehtml(input).unwrap().text, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
@@ -342,16 +406,54 @@ mod tests {
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html).unwrap();
let html = "line1<br>line2";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\nline2");
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
let html = "line1<br> line2";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\nline2");
let html = "line1 <br><br> line2";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\n\nline2");
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\nline2\nline3");
}
#[test]
fn test_dehtml_parse_span() {
assert_eq!(dehtml("<span>Foo</span>bar").unwrap().text, "Foobar");
assert_eq!(dehtml("<span>Foo</span> bar").unwrap().text, "Foo bar");
assert_eq!(dehtml("<span>Foo </span>bar").unwrap().text, "Foo bar");
assert_eq!(dehtml("<span>Foo</span>\nbar").unwrap().text, "Foo bar");
assert_eq!(dehtml("\n<span>Foo</span> bar").unwrap().text, "Foo bar");
assert_eq!(dehtml("<span>Foo</span>\n\nbar").unwrap().text, "Foo bar");
assert_eq!(dehtml("Foo\n<span>bar</span>").unwrap().text, "Foo bar");
assert_eq!(dehtml("Foo<span>\nbar</span>").unwrap().text, "Foo bar");
}
#[test]
fn test_dehtml_parse_p() {
let html = "<p>Foo</p><p>Bar</p>";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "Foo\n\nBar");
let html = "<p>Foo<p>Bar";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "Foo\n\nBar");
let html = "<p>Foo</p><p>Bar<p>Baz";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "Foo\n\nBar\n\nBaz");
}
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html).unwrap();
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "[text](url)");
}
@@ -359,7 +461,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html).unwrap();
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "text *bold*<>");
}
@@ -369,7 +471,7 @@ mod tests {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html).unwrap();
let plain = dehtml(html).unwrap().text;
assert_eq!(
plain,
@@ -393,32 +495,38 @@ mod tests {
</html>
"##;
let txt = dehtml(input).unwrap();
assert_eq!(txt.trim(), "lots of text");
assert_eq!(txt.text.trim(), "lots of text");
}
#[test]
fn test_pre_tag() {
let input = "<html><pre>\ntwo\nlines\n</pre></html>";
let txt = dehtml(input).unwrap();
assert_eq!(txt.trim(), "two\nlines");
assert_eq!(txt.text.trim(), "two\nlines");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quote_div() {
let input = include_str!("../test-data/message/gmx-quote-body.eml");
let dehtml = dehtml(input).unwrap();
println!("{dehtml}");
let SimplifiedText {
text,
is_forwarded,
is_cut,
top_quote,
footer,
} = simplify(dehtml, false);
} = dehtml;
assert_eq!(text, "Test");
assert_eq!(is_forwarded, false);
assert_eq!(is_cut, false);
assert_eq!(top_quote.as_deref(), Some("test"));
assert_eq!(footer, None);
}
#[test]
fn test_spaces() {
let input = include_str!("../test-data/spaces.html");
let txt = dehtml(input).unwrap();
assert_eq!(txt.text, "Welcome back to Strolling!\n\nHey there,\n\nWelcome back! Use this link to securely sign in to your Strolling account:\n\nSign in to Strolling\n\nFor your security, the link will expire in 24 hours time.\n\nSee you soon!\n\nYou can also copy\n\nhttps://strolling.rosano.ca/members/?token=XXX\n\nIf you did not make this request, you can safely ignore this email.\n\nThis message was sent from [strolling.rosano.ca](https://strolling.rosano.ca/) to [alice@example.org](mailto:alice@example.org)");
}
}

View File

@@ -308,7 +308,7 @@ mod tests {
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Hi Bob".to_owned()));
msg.set_text("Hi Bob".to_owned());
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
@@ -355,7 +355,6 @@ mod tests {
assert_eq!(msg.get_subject(), "foo");
assert!(msg
.get_text()
.unwrap()
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
receive_imf_inner(
@@ -370,7 +369,7 @@ mod tests {
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Done);
assert_eq!(msg.get_subject(), "foo");
assert_eq!(msg.get_text(), Some("100k text...".to_string()));
assert_eq!(msg.get_text(), "100k text...");
Ok(())
}

View File

@@ -63,6 +63,7 @@
//! ephemeral message timers or global `delete_server_after` setting.
use std::cmp::max;
use std::collections::BTreeSet;
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
@@ -218,7 +219,7 @@ impl ChatId {
if self.is_promoted(context).await? {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await);
msg.text = stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
@@ -455,8 +456,15 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
})
.await?;
let mut modified_chat_ids = BTreeSet::new();
for (chat_id, msg_id) in msgs_changed {
context.emit_msgs_changed(chat_id, msg_id);
context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
modified_chat_ids.insert(chat_id);
}
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
}
for msg_id in webxdc_deleted {
@@ -1054,7 +1062,7 @@ mod tests {
delete_expired_messages(t, not_deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "Message text");
assert_eq!(loaded.text, "Message text");
assert_eq!(loaded.chat_id, chat.id);
assert!(next_expiration < deleted_at);
@@ -1074,7 +1082,7 @@ mod tests {
.await;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "");
assert_eq!(loaded.text, "");
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
// Check that the msg was deleted locally.
@@ -1097,7 +1105,7 @@ mod tests {
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, ContactId::UNDEFINED);
assert_eq!(msg.to_id, ContactId::UNDEFINED);
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
assert_eq!(msg.text, "");
let rawtxt: Option<String> = t
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))

View File

@@ -145,6 +145,27 @@ pub enum EventType {
msg_id: MsgId,
},
/// A single message was deleted.
///
/// This event means that the message will no longer appear in the messagelist.
/// UI should remove the message from the messagelist
/// in response to this event if the message is currently displayed.
///
/// The message may have been explicitly deleted by the user or expired.
/// Internally the message may have been removed from the database,
/// moved to the trash chat or hidden.
///
/// This event does not indicate the message
/// deletion from the server.
MsgDeleted {
/// ID of the chat where the message was prior to deletion.
/// Never 0 or trash chat.
chat_id: ChatId,
/// ID of the deleted message. Never 0.
msg_id: MsgId,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()

View File

@@ -456,7 +456,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert!(!msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -470,7 +470,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -483,7 +483,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -526,7 +526,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.get_showpadlock());
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -540,14 +540,14 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("plain text".to_string()));
msg.set_text("plain text".to_string());
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
// check the message is written correctly to alice's db
let msg = alice.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert_eq!(msg.get_text(), "plain text");
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
@@ -557,7 +557,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let chat_id = bob.create_chat(&alice).await.id;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert_eq!(msg.get_text(), "plain text");
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
@@ -575,7 +575,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.viewtype, Viewtype::Text);
assert!(msg.text.as_ref().unwrap().contains("foo bar ä ö ü ß"));
assert!(msg.text.contains("foo bar ä ö ü ß"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&t).await?.unwrap();
println!("{html}");

View File

@@ -412,7 +412,7 @@ impl Imap {
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(message.clone());
msg.text = message.clone();
if let Err(e) =
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
.await

View File

@@ -253,12 +253,10 @@ async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool("bcc_self").await? {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
"It seems you are using multiple devices with Delta Chat. Great!\n\n\
msg.text = "It seems you are using multiple devices with Delta Chat. Great!\n\n\
If you also want to synchronize outgoing messages across all devices, \
go to \"Settings → Advanced\" and enable \"Send Copy to Self\"."
.to_string(),
);
.to_string();
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
}
Ok(())
@@ -1030,10 +1028,7 @@ mod tests {
// not synchronized yet.
let sent = alice2.send_text(msg.chat_id, "Test").await;
alice.recv_msg(&sent).await;
assert_ne!(
alice.get_last_msg().await.get_text(),
Some("Test".to_string())
);
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
// Transfer the key.
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
@@ -1041,10 +1036,7 @@ mod tests {
// Alice sends a message to self from the new device.
let sent = alice2.send_text(msg.chat_id, "Test").await;
alice.recv_msg(&sent).await;
assert_eq!(
alice.get_last_msg().await.get_text(),
Some("Test".to_string())
);
assert_eq!(alice.get_last_msg().await.get_text(), "Test");
Ok(())
}

View File

@@ -275,7 +275,7 @@ impl BackupProvider {
Ok(_) => {
context.emit_event(SendProgress::Completed.into());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(backup_transfer_msg_body(context).await);
msg.text = backup_transfer_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
Err(err) => {
@@ -611,7 +611,7 @@ mod tests {
// Write a message in the self chat
let self_chat = ctx0.get_self_chat().await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hi there".to_string()));
msg.set_text("hi there".to_string());
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
// Send an attachment in the self chat
@@ -643,7 +643,7 @@ mod tests {
_ => panic!("wrong chat item"),
};
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let text = msg.get_text().unwrap();
let text = msg.get_text();
assert_eq!(text, "hi there");
let msgid = match msgs.get(1).unwrap() {
ChatItem::Message { msg_id } => msg_id,

View File

@@ -280,7 +280,7 @@ pub async fn send_locations_to_chat(
.await?;
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::msg_location_enabled(context).await);
msg.text = stock_str::msg_location_enabled(context).await;
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await
@@ -885,7 +885,7 @@ Text message."#,
)
.await?;
let received_msg = alice.get_last_msg().await;
assert_eq!(received_msg.text.unwrap(), "Text message.");
assert_eq!(received_msg.text, "Text message.");
receive_imf(
&alice,

View File

@@ -7,29 +7,28 @@ use anyhow::{ensure, format_err, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::chat::{Chat, ChatId};
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{Contact, ContactId, Origin};
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::imap::markseen_on_imap_table;
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
use crate::mimeparser::{parse_message_id, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
use crate::summary::Summary;
use crate::tools::{
buf_compress, buf_decompress, create_smeared_timestamp, get_filebytes, get_filemeta,
gm2local_offset, read_file, time, timestamp_to_str, truncate,
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file, time,
timestamp_to_str, truncate,
};
/// Message ID, including reserved IDs.
@@ -157,6 +156,171 @@ WHERE id=?;
pub fn to_u32(self) -> u32 {
self.0
}
/// Returns detailed message information in a multi-line text form.
pub async fn get_info(self, context: &Context) -> Result<String> {
let msg = Message::load_from_db(context, self).await?;
let rawtxt: Option<String> = context
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,))
.await?;
let mut ret = String::new();
if rawtxt.is_none() {
ret += &format!("Cannot load message {self}.");
return Ok(ret);
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let fts = timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {fts}");
let name = Contact::get_by_id(context, msg.from_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {name}");
ret += "\n";
if msg.from_id != ContactId::SELF {
let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
msg.timestamp_rcvd
} else {
msg.timestamp_sort
});
ret += &format!("Received: {}", &s);
ret += "\n";
}
if let EphemeralTimer::Enabled { duration } = msg.ephemeral_timer {
ret += &format!("Ephemeral timer: {duration}\n");
}
if msg.ephemeral_timestamp != 0 {
ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
}
if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
// device-internal message, no further details needed
return Ok(ret);
}
if let Ok(rows) = context
.sql
.query_map(
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
(self,),
|row| {
let contact_id: ContactId = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
{
for (contact_id, ts) in rows {
let fts = timestamp_to_str(ts);
ret += &format!("Read: {fts}");
let name = Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {name}");
ret += "\n";
}
}
ret += &format!("State: {}", msg.state);
if msg.has_location() {
ret += ", Location sent";
}
let e2ee_errors = msg.param.get_int(Param::ErroneousE2ee).unwrap_or_default();
if 0 != e2ee_errors {
if 0 != e2ee_errors & 0x2 {
ret += ", Encrypted, no valid signature";
}
} else if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
ret += ", Encrypted";
}
ret += "\n";
let reactions = get_msg_reactions(context, self).await?;
if !reactions.is_empty() {
ret += &format!("Reactions: {reactions}\n");
}
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {error}");
}
if let Some(path) = msg.get_file(context) {
let bytes = get_filebytes(context, &path).await?;
ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes);
}
if msg.viewtype != Viewtype::Text {
ret += "Type: ";
ret += &format!("{}", msg.viewtype);
ret += "\n";
ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
}
let w = msg.param.get_int(Param::Width).unwrap_or_default();
let h = msg.param.get_int(Param::Height).unwrap_or_default();
if w != 0 || h != 0 {
ret += &format!("Dimension: {w} x {h}\n",);
}
let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
if duration != 0 {
ret += &format!("Duration: {duration} ms\n",);
}
if !rawtxt.is_empty() {
ret += &format!("\n{rawtxt}\n");
}
if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
let server_uids = context
.sql
.query_map(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
(msg.rfc724_mid,),
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok((folder, uid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (folder, uid) in server_uids {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\n</{folder}/;UID={uid}>");
}
}
let hop_info: Option<String> = context
.sql
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", (self,))
.await?;
ret += "\n\n";
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
Ok(ret)
}
}
impl std::fmt::Display for MsgId {
@@ -258,7 +422,7 @@ pub struct Message {
pub(crate) timestamp_rcvd: i64,
pub(crate) ephemeral_timer: EphemeralTimer,
pub(crate) ephemeral_timestamp: i64,
pub(crate) text: Option<String>,
pub(crate) text: String,
/// Message subject.
///
@@ -367,7 +531,7 @@ impl Message {
.filter(|error| !error.is_empty()),
is_dc_message: row.get("msgrmsg")?,
mime_modified: row.get("mime_modified")?,
text: Some(text),
text,
subject: row.get("subject")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
hidden: row.get("hidden")?,
@@ -379,7 +543,8 @@ impl Message {
Ok(msg)
},
)
.await?;
.await
.with_context(|| format!("failed to load message {id} from the database"))?;
Ok(msg)
}
@@ -514,8 +679,8 @@ impl Message {
}
/// Returns the text of the message.
pub fn get_text(&self) -> Option<String> {
self.text.as_ref().map(|s| s.to_string())
pub fn get_text(&self) -> String {
self.text.clone()
}
/// Returns message subject.
@@ -791,7 +956,7 @@ impl Message {
}
/// Sets or unsets message text.
pub fn set_text(&mut self, text: Option<String>) {
pub fn set_text(&mut self, text: String) {
self.text = text;
}
@@ -883,7 +1048,7 @@ impl Message {
self.param.set(Param::GuaranteeE2ee, "1");
}
let text = quote.get_text().unwrap_or_default();
let text = quote.get_text();
self.param.set(
Param::Quote,
if text.is_empty() {
@@ -1108,171 +1273,6 @@ pub async fn get_msg_read_receipts(
.await
}
/// Returns detailed message information in a multi-line text form.
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let msg = Message::load_from_db(context, msg_id).await?;
let rawtxt: Option<String> = context
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
.await?;
let mut ret = String::new();
if rawtxt.is_none() {
ret += &format!("Cannot load message {msg_id}.");
return Ok(ret);
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let fts = timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {fts}");
let name = Contact::load_from_db(context, msg.from_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {name}");
ret += "\n";
if msg.from_id != ContactId::SELF {
let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
msg.timestamp_rcvd
} else {
msg.timestamp_sort
});
ret += &format!("Received: {}", &s);
ret += "\n";
}
if let EphemeralTimer::Enabled { duration } = msg.ephemeral_timer {
ret += &format!("Ephemeral timer: {duration}\n");
}
if msg.ephemeral_timestamp != 0 {
ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
}
if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
// device-internal message, no further details needed
return Ok(ret);
}
if let Ok(rows) = context
.sql
.query_map(
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
(msg_id,),
|row| {
let contact_id: ContactId = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
{
for (contact_id, ts) in rows {
let fts = timestamp_to_str(ts);
ret += &format!("Read: {fts}");
let name = Contact::load_from_db(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {name}");
ret += "\n";
}
}
ret += &format!("State: {}", msg.state);
if msg.has_location() {
ret += ", Location sent";
}
let e2ee_errors = msg.param.get_int(Param::ErroneousE2ee).unwrap_or_default();
if 0 != e2ee_errors {
if 0 != e2ee_errors & 0x2 {
ret += ", Encrypted, no valid signature";
}
} else if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
ret += ", Encrypted";
}
ret += "\n";
let reactions = get_msg_reactions(context, msg_id).await?;
if !reactions.is_empty() {
ret += &format!("Reactions: {reactions}\n");
}
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {error}");
}
if let Some(path) = msg.get_file(context) {
let bytes = get_filebytes(context, &path).await?;
ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes);
}
if msg.viewtype != Viewtype::Text {
ret += "Type: ";
ret += &format!("{}", msg.viewtype);
ret += "\n";
ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
}
let w = msg.param.get_int(Param::Width).unwrap_or_default();
let h = msg.param.get_int(Param::Height).unwrap_or_default();
if w != 0 || h != 0 {
ret += &format!("Dimension: {w} x {h}\n",);
}
let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
if duration != 0 {
ret += &format!("Duration: {duration} ms\n",);
}
if !rawtxt.is_empty() {
ret += &format!("\n{rawtxt}\n");
}
if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
let server_uids = context
.sql
.query_map(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
(msg.rfc724_mid,),
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok((folder, uid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (folder, uid) in server_uids {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\n</{folder}/;UID={uid}>");
}
}
let hop_info: Option<String> = context
.sql
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", (msg_id,))
.await?;
ret += "\n\n";
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
Ok(ret)
}
pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
let info = match extension {
@@ -1421,6 +1421,8 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
/// by moving them to the trash chat
/// and scheduling for deletion on IMAP.
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
let mut modified_chat_ids = BTreeSet::new();
for &msg_id in msg_ids {
let msg = Message::load_from_db(context, msg_id).await?;
if msg.location_id > 0 {
@@ -1431,10 +1433,17 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
.await
.with_context(|| format!("Unable to trash message {msg_id}"))?;
context.emit_event(EventType::MsgDeleted {
chat_id: msg.chat_id,
msg_id,
});
if msg.viewtype == Viewtype::Webxdc {
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
}
modified_chat_ids.insert(msg.chat_id);
let target = context.get_delete_msgs_target().await?;
context
.sql
@@ -1458,9 +1467,11 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
}
if !msg_ids.is_empty() {
context.emit_msgs_changed_without_ids();
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
}
if !msg_ids.is_empty() {
// Run housekeeping to delete unused blobs.
context.set_config(Config::LastHousekeeping, None).await?;
}
@@ -1672,176 +1683,6 @@ pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str
}
}
/// returns Some if an event should be send
pub async fn handle_mdn(
context: &Context,
from_id: ContactId,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Result<Option<(ChatId, MsgId)>> {
if from_id == ContactId::SELF {
warn!(
context,
"ignoring MDN sent to self, this is a bug on the sender device"
);
// This is not an error on our side,
// we successfully ignored an invalid MDN and return `Ok`.
return Ok(None);
}
let res = context
.sql
.query_row_optional(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" m.state AS state",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
" ORDER BY m.id;"
),
(&rfc724_mid,),
|row| {
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, MessageState>("state")?,
))
},
)
.await?;
let (msg_id, chat_id, msg_state) = if let Some(res) = res {
res
} else {
info!(
context,
"handle_mdn found no message with Message-ID {:?} sent by us in the database",
rfc724_mid
);
return Ok(None);
};
if !context
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
(msg_id, from_id),
)
.await?
{
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
(msg_id, from_id, timestamp_sent),
)
.await?;
}
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
Ok(Some((chat_id, msg_id)))
} else {
Ok(None)
}
}
/// Marks a message as failed after an ndn (non-delivery-notification) arrived.
/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
pub(crate) async fn handle_ndn(
context: &Context,
failed: &DeliveryReport,
error: Option<String>,
) -> Result<()> {
if failed.rfc724_mid.is_empty() {
return Ok(());
}
// The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client.
// In this case we need to mark multiple "msgids" as failed that all refer to the same message-id.
let msgs: Vec<_> = context
.sql
.query_map(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
),
(&failed.rfc724_mid,),
|row| {
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, Chattype>("type")?,
))
},
|rows| Ok(rows.collect::<Vec<_>>()),
)
.await?;
let error = if let Some(error) = error {
error
} else if let Some(failed_recipient) = &failed.failed_recipient {
format!("Delivery to {failed_recipient} failed.").clone()
} else {
"Delivery to at least one recipient failed.".to_string()
};
let mut first = true;
for msg in msgs {
let (msg_id, chat_id, chat_type) = msg?;
set_msg_failed(context, msg_id, &error).await;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
}
first = false;
}
Ok(())
}
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &DeliveryReport,
chat_id: ChatId,
chat_type: Chattype,
) -> Result<()> {
match chat_type {
Chattype::Group | Chattype::Broadcast => {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
.await?
.context("contact ID not found")?;
let contact = Contact::load_from_db(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
chat::add_info_msg(context, chat_id, &text, create_smeared_timestamp(context))
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}
Chattype::Mailinglist => {
// ndn_maybe_add_info_msg() is about the case when delivery to the group failed.
// If we get an NDN for the mailing list, just issue a warning.
warn!(context, "ignoring NDN for mailing list.");
}
Chattype::Single | Chattype::Undefined => {}
}
Ok(())
}
/// The number of messages assigned to unblocked chats
pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
match context
@@ -2064,7 +1905,7 @@ mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::{marknoticed_chat, ChatItem};
use crate::chat::{self, marknoticed_chat, ChatItem};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils as test;
@@ -2240,7 +2081,7 @@ mod tests {
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Quoted message".to_string()));
msg.set_text("Quoted message".to_string());
// Prepare message for sending, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
@@ -2252,14 +2093,14 @@ mod tests {
msg2.set_quote(ctx, Some(&msg))
.await
.expect("can't set quote");
assert!(msg2.quoted_text() == msg.get_text());
assert_eq!(msg2.quoted_text().unwrap(), msg.get_text());
let quoted_msg = msg2
.quoted_message(ctx)
.await
.expect("error while retrieving quoted message")
.expect("quoted message not found");
assert!(quoted_msg.get_text() == msg2.quoted_text());
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2283,7 +2124,7 @@ mod tests {
// check chat-id of this message
let msg = alice.get_last_msg().await;
assert!(!msg.get_chat_id().is_special());
assert_eq!(msg.get_text().unwrap(), "hello".to_string());
assert_eq!(msg.get_text(), "hello".to_string());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2297,10 +2138,10 @@ mod tests {
.unwrap()
.first()
.unwrap();
let contact = Contact::load_from_db(&alice, contact_id).await.unwrap();
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("bla blubb".to_string()));
msg.set_text("bla blubb".to_string());
msg.set_override_sender_name(Some("over ride".to_string()));
assert_eq!(
msg.get_override_sender_name(),
@@ -2317,10 +2158,10 @@ mod tests {
.unwrap()
.first()
.unwrap();
let contact = Contact::load_from_db(&bob, contact_id).await.unwrap();
let contact = Contact::get_by_id(&bob, contact_id).await.unwrap();
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.chat_id, chat.id);
assert_eq!(msg.text, Some("bla blubb".to_string()));
assert_eq!(msg.text, "bla blubb");
assert_eq!(
msg.get_override_sender_name(),
Some("over ride".to_string())
@@ -2340,7 +2181,7 @@ mod tests {
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
msg.set_text("this is the text!".to_string());
// alice sends to bob,
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
@@ -2426,7 +2267,7 @@ mod tests {
// check outgoing messages states on sender side
let mut alice_msg = Message::new(Viewtype::Text);
alice_msg.set_text(Some("hi!".to_string()));
alice_msg.set_text("hi!".to_string());
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
alice_chat
@@ -2482,7 +2323,7 @@ mod tests {
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "hello".to_string());
assert_eq!(msg.get_text(), "hello".to_string());
assert!(msg.is_bot());
// Alice receives a message from Bob who is not the bot anymore.
@@ -2499,7 +2340,7 @@ mod tests {
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "hello again".to_string());
assert_eq!(msg.get_text(), "hello again".to_string());
assert!(!msg.is_bot());
Ok(())
@@ -2538,13 +2379,13 @@ mod tests {
let sent = alice.send_text(chat.id, "> First quote").await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some("> First quote"));
assert_eq!(received.text, "> First quote");
assert!(received.quoted_text().is_none());
assert!(received.quoted_message(&bob).await?.is_none());
let sent = alice.send_text(chat.id, "> Second quote").await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some("> Second quote"));
assert_eq!(received.text, "> Second quote");
assert!(received.quoted_text().is_none());
assert!(received.quoted_message(&bob).await?.is_none());
@@ -2561,24 +2402,24 @@ mod tests {
let text = " Foo bar";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
assert_eq!(received.text, text);
let text = "Foo bar baz";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
assert_eq!(received.text, text);
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
assert_eq!(received.text, text);
let python_program = "\
def hello():
return 'Hello, world!'";
let sent = alice.send_text(chat.id, python_program).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(python_program));
assert_eq!(received.text, python_program);
Ok(())
}

View File

@@ -237,7 +237,7 @@ impl<'a> MimeFactory<'a> {
) -> Result<MimeFactory<'a>> {
ensure!(!msg.chat_id.is_special(), "Invalid chat id");
let contact = Contact::load_from_db(context, msg.from_id).await?;
let contact = Contact::get_by_id(context, msg.from_id).await?;
let from_addr = context.get_primary_self_addr().await?;
let from_displayname = context
.get_config(Config::Displayname)
@@ -918,6 +918,19 @@ impl<'a> MimeFactory<'a> {
match command {
SystemMessage::MemberRemovedFromGroup => {
let email_to_remove = self.msg.param.get(Param::Arg).unwrap_or_default();
if email_to_remove
== context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default()
{
placeholdertext = Some(stock_str::msg_group_left_remote(context).await);
} else {
placeholdertext =
Some(stock_str::msg_del_member_remote(context, email_to_remove).await);
};
if !email_to_remove.is_empty() {
headers.protected.push(Header::new(
"Chat-Group-Member-Removed".into(),
@@ -927,6 +940,9 @@ impl<'a> MimeFactory<'a> {
}
SystemMessage::MemberAddedToGroup => {
let email_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
placeholdertext =
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
if !email_to_add.is_empty() {
headers.protected.push(Header::new(
"Chat-Group-Member-Added".into(),
@@ -1138,15 +1154,8 @@ impl<'a> MimeFactory<'a> {
} else {
None
};
let final_text = {
if let Some(ref text) = placeholdertext {
text
} else if let Some(ref text) = self.msg.text {
text
} else {
""
}
};
let final_text = placeholdertext.as_deref().unwrap_or(&self.msg.text);
let mut quoted_text = self
.msg
@@ -1792,7 +1801,7 @@ mod tests {
quote: Option<&Message>,
) -> Result<String> {
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
new_msg.set_text("Hi".to_string());
if let Some(q) = quote {
new_msg.set_quote(t, Some(q)).await?;
}
@@ -1879,7 +1888,7 @@ mod tests {
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
new_msg.set_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
@@ -1987,7 +1996,7 @@ mod tests {
chat_id.accept(context).await.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
new_msg.set_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
.await
@@ -2105,7 +2114,7 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
msg.set_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
@@ -2165,7 +2174,7 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
msg.set_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
@@ -2196,7 +2205,7 @@ mod tests {
.await
.unwrap()
.unwrap();
let alice_contact = Contact::load_from_db(&bob.ctx, alice_id).await.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert!(alice_contact
.get_profile_image(&bob.ctx)
.await
@@ -2228,7 +2237,7 @@ mod tests {
assert_eq!(body.match_indices("Subject:").count(), 0);
bob.recv_msg(&sent_msg).await;
let alice_contact = Contact::load_from_db(&bob.ctx, alice_id).await.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert!(alice_contact
.get_profile_image(&bob.ctx)
.await
@@ -2277,7 +2286,7 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
msg.set_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let payload = sent_msg.payload();

View File

@@ -15,9 +15,10 @@ use once_cell::sync::Lazy;
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::contact::{addr_cmp, addr_normalize, ContactId};
use crate::constants::{Chattype, DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::contact::{addr_cmp, addr_normalize, Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
@@ -28,13 +29,16 @@ use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::message::{self, Viewtype};
use crate::message::{self, set_msg_failed, update_msg_state, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::stock_str;
use crate::sync::SyncItems;
use crate::tools::{get_filemeta, parse_receive_headers, strip_rtlo_characters, truncate_by_lines};
use crate::tools::{
create_smeared_timestamp, get_filemeta, parse_receive_headers, strip_rtlo_characters,
truncate_by_lines,
};
use crate::{location, tools};
/// A parsed MIME message.
@@ -52,7 +56,7 @@ pub(crate) struct MimeMessage {
pub parts: Vec<Part>,
/// Message headers.
header: HashMap<String, String>,
headers: HashMap<String, String>,
/// Addresses are normalized and lowercased:
pub recipients: Vec<SingleInfo>,
@@ -63,6 +67,8 @@ pub(crate) struct MimeMessage {
/// Whether the From address was repeated in the signed part
/// (and we know that the signer intended to send from this address)
pub from_is_signed: bool,
/// The List-Post address is only set for mailing lists. Users can send
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decryption_info: DecryptionInfo,
@@ -384,7 +390,7 @@ impl MimeMessage {
let mut parser = MimeMessage {
parts: Vec::new(),
header: headers,
headers,
recipients,
list_post,
from,
@@ -690,7 +696,7 @@ impl MimeMessage {
self.parts.push(part);
}
if self.header.contains_key("auto-submitted") {
if self.headers.contains_key("auto-submitted") {
for part in &mut self.parts {
part.param.set(Param::Bot, "1");
}
@@ -768,12 +774,10 @@ impl MimeMessage {
!self.signatures.is_empty()
}
/// Returns whether the email contains a `chat-version` header.
/// This indicates that the email is a DC-email.
pub(crate) fn has_chat_version(&self) -> bool {
self.header.contains_key("chat-version")
}
pub(crate) fn has_headers(&self) -> bool {
!self.header.is_empty()
self.headers.contains_key("chat-version")
}
pub(crate) fn get_subject(&self) -> Option<String> {
@@ -783,7 +787,7 @@ impl MimeMessage {
}
pub fn get_header(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(headerdef.get_headername())
self.headers.get(headerdef.get_headername())
}
fn parse_mime_recursive<'a>(
@@ -1076,16 +1080,20 @@ impl MimeMessage {
Default::default()
} else {
let is_html = mime_type == mime::TEXT_HTML;
let out = if is_html {
if is_html {
self.is_mime_modified = true;
dehtml(&decoded_data).unwrap_or_else(|| {
if let Some(text) = dehtml(&decoded_data) {
text
} else {
dehtml_failed = true;
decoded_data.clone()
})
SimplifiedText {
text: decoded_data.clone(),
..Default::default()
}
}
} else {
decoded_data.clone()
};
simplify(out, self.has_chat_version())
simplify(decoded_data.clone(), self.has_chat_version())
}
};
self.is_mime_modified = self.is_mime_modified
@@ -1644,16 +1652,10 @@ impl MimeMessage {
.iter()
.chain(&report.additional_message_ids)
{
match message::handle_mdn(context, from_id, original_message_id, sent_timestamp)
.await
if let Err(err) =
handle_mdn(context, from_id, original_message_id, sent_timestamp).await
{
Ok(Some((chat_id, msg_id))) => {
context.emit_event(EventType::MsgRead { chat_id, msg_id });
}
Ok(None) => {}
Err(err) => {
warn!(context, "failed to handle_mdn: {:#}", err);
}
warn!(context, "Could not handle MDN: {err:#}.");
}
}
}
@@ -1664,8 +1666,8 @@ impl MimeMessage {
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(e) = message::handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle ndn: {}", e);
if let Err(err) = handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle NDN: {err:#}.");
}
}
}
@@ -1748,7 +1750,7 @@ async fn update_gossip_peerstates(
.handle_fingerprint_change(context, message_time)
.await?;
gossiped_addr.insert(header.addr.clone());
gossiped_addr.insert(header.addr.to_lowercase());
}
Ok(gossiped_addr)
@@ -2023,6 +2025,166 @@ fn get_all_addresses_from_header(
result
}
async fn handle_mdn(
context: &Context,
from_id: ContactId,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Result<()> {
if from_id == ContactId::SELF {
warn!(
context,
"Ignoring MDN sent to self, this is a bug on the sender device."
);
// This is not an error on our side,
// we successfully ignored an invalid MDN and return `Ok`.
return Ok(());
}
let Some((msg_id, chat_id, msg_state)) = context
.sql
.query_row_optional(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" m.state AS state",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
" ORDER BY m.id"
),
(&rfc724_mid,),
|row| {
let msg_id: MsgId = row.get("msg_id")?;
let chat_id: ChatId = row.get("chat_id")?;
let msg_state: MessageState = row.get("state")?;
Ok((msg_id, chat_id, msg_state))
},
)
.await? else {
info!(
context,
"Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
);
return Ok(());
};
if !context
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?",
(msg_id, from_id),
)
.await?
{
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
(msg_id, from_id, timestamp_sent),
)
.await?;
}
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
context.emit_event(EventType::MsgRead { chat_id, msg_id });
}
Ok(())
}
/// Marks a message as failed after an ndn (non-delivery-notification) arrived.
/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
async fn handle_ndn(
context: &Context,
failed: &DeliveryReport,
error: Option<String>,
) -> Result<()> {
if failed.rfc724_mid.is_empty() {
return Ok(());
}
// The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client.
// In this case we need to mark multiple "msgids" as failed that all refer to the same message-id.
let msgs: Vec<_> = context
.sql
.query_map(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
),
(&failed.rfc724_mid,),
|row| {
let msg_id: MsgId = row.get("msg_id")?;
let chat_id: ChatId = row.get("chat_id")?;
let chat_type: Chattype = row.get("type")?;
Ok((msg_id, chat_id, chat_type))
},
|rows| Ok(rows.collect::<Vec<_>>()),
)
.await?;
let error = if let Some(error) = error {
error
} else if let Some(failed_recipient) = &failed.failed_recipient {
format!("Delivery to {failed_recipient} failed.").clone()
} else {
"Delivery to at least one recipient failed.".to_string()
};
let mut first = true;
for msg in msgs {
let (msg_id, chat_id, chat_type) = msg?;
set_msg_failed(context, msg_id, &error).await;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
}
first = false;
}
Ok(())
}
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &DeliveryReport,
chat_id: ChatId,
chat_type: Chattype,
) -> Result<()> {
match chat_type {
Chattype::Group | Chattype::Broadcast => {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
.await?
.context("contact ID not found")?;
let contact = Contact::get_by_id(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}
Chattype::Mailinglist => {
// ndn_maybe_add_info_msg() is about the case when delivery to the group failed.
// If we get an NDN for the mailing list, just issue a warning.
warn!(context, "ignoring NDN for mailing list.");
}
Chattype::Single | Chattype::Undefined => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
@@ -3217,10 +3379,7 @@ On 2020-10-25, Bob wrote:
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(
msg.text.as_ref().unwrap(),
"subj with important info body text"
);
assert_eq!(msg.text, "subj with important info body text");
assert_eq!(msg.viewtype, Viewtype::Image);
assert_eq!(msg.error(), None);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
@@ -3389,9 +3548,9 @@ Some reply
receive_imf(&t, raw, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply");
assert_eq!(msg.get_text(), "Some reply");
let quoted_message = msg.quoted_message(&t).await?.unwrap();
assert_eq!(quoted_message.get_text().unwrap(), "Some quote.");
assert_eq!(quoted_message.get_text(), "Some quote.");
Ok(())
}

View File

@@ -535,7 +535,7 @@ impl Peerstate {
stock_str::contact_setup_changed(context, &self.addr).await
}
PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?;
let old_contact = Contact::get_by_id(context, contact_id).await?;
stock_str::aeap_addr_changed(
context,
old_contact.get_display_name(),

View File

@@ -96,7 +96,12 @@ impl PlainText {
line += "<br/>\n";
}
ret += &*line;
let len_with_indentation = line.len();
let line = line.trim_start_matches(' ');
for _ in line.len()..len_with_indentation {
ret += "&nbsp;";
}
ret += line;
}
ret += "</body></html>\n";
ret
@@ -107,8 +112,8 @@ impl PlainText {
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html() {
#[test]
fn test_plain_to_html() {
let html = PlainText {
text: r##"line 1
line 2
@@ -137,8 +142,8 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html_encapsulated() {
#[test]
fn test_plain_to_html_encapsulated() {
let html = PlainText {
text: r#"line with <http://encapsulated.link/?foo=_bar> here!"#.to_string(),
flowed: false,
@@ -158,8 +163,8 @@ line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.l
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html_nolink() {
#[test]
fn test_plain_to_html_nolink() {
let html = PlainText {
text: r#"line with nohttp://no.link here"#.to_string(),
flowed: false,
@@ -179,8 +184,8 @@ line with nohttp://no.link here<br/>
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html_mailto() {
#[test]
fn test_plain_to_html_mailto() {
let html = PlainText {
text: r#"just an address: foo@bar.org another@one.de"#.to_string(),
flowed: false,
@@ -200,8 +205,8 @@ just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:an
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html_flowed() {
#[test]
fn test_plain_to_html_flowed() {
let html = PlainText {
text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
flowed: true,
@@ -224,8 +229,8 @@ line still line<br/>
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html_flowed_delsp() {
#[test]
fn test_plain_to_html_flowed_delsp() {
let html = PlainText {
text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
flowed: true,
@@ -248,8 +253,8 @@ linestill line<br/>
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_plain_to_html_fixed() {
#[test]
fn test_plain_to_html_fixed() {
let html = PlainText {
text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
flowed: false,
@@ -267,7 +272,32 @@ line <br/>
still line<br/>
<em>&gt;quote </em><br/>
<em>&gt;still quote</em><br/>
&gt;no quote<br/>
&nbsp;&gt;no quote<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_indentation() {
let html = PlainText {
text: "def foo():\n pass\n\ndef bar(x):\n return x + 5".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
def foo():<br/>
&nbsp;&nbsp;&nbsp;&nbsp;pass<br/>
<br/>
def bar(x):<br/>
&nbsp;&nbsp;&nbsp;&nbsp;return x + 5<br/>
</body></html>
"#
);

View File

@@ -121,6 +121,7 @@ fn inner_generate_secure_join_qr_code(
w.elem("svg", |d| {
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
d.attr("viewBox", format_args!("0 0 {width} {height}"))?;
d.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?; // required for enabling xlink:href on browsers
Ok(())
})?
.build(|w| {
@@ -240,7 +241,7 @@ fn inner_generate_secure_join_qr_code(
d.attr("preserveAspectRatio", "none")?;
d.attr("clip-path", "url(#avatar-cut)")?;
d.attr(
"href", /*might need xlink:href instead if it doesn't work on older devices?*/
"xlink:href", /* xlink:href is needed otherwise it won't even display in inkscape not to mention qt's QSvgHandler */
format!(
"data:image/jpeg;base64,{}",
base64::engine::general_purpose::STANDARD.encode(img)

View File

@@ -156,7 +156,7 @@ impl Context {
self.set_config(Config::QuotaExceeding, Some(&highest.to_string()))
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::quota_exceeding(self, highest).await);
msg.text = stock_str::quota_exceeding(self, highest).await;
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
self.set_config(Config::QuotaExceeding, None).await?;

View File

@@ -214,7 +214,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
let reaction: Reaction = reaction.into();
let mut reaction_msg = Message::new(Viewtype::Text);
reaction_msg.text = Some(reaction.as_str().to_string());
reaction_msg.text = reaction.as_str().to_string();
reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
reaction_msg.hidden = true;

View File

@@ -138,12 +138,6 @@ pub(crate) async fn receive_imf_inner(
Ok(mime_parser) => mime_parser,
};
// we can not add even an empty record if we have no info whatsoever
if !mime_parser.has_headers() {
warn!(context, "receive_imf: no headers found.");
return Ok(None);
}
info!(context, "Received message has Message-Id: {rfc724_mid}");
// check, if the mail is already in our database.
@@ -410,7 +404,7 @@ pub async fn from_field_to_contact_id(
} else {
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::load_from_db(context, from_id).await {
if let Ok(contact) = Contact::get_by_id(context, from_id).await {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
@@ -685,7 +679,7 @@ async fn add_parts(
let create_blocked = if from_id == ContactId::SELF {
Blocked::Not
} else {
let contact = Contact::load_from_db(context, from_id).await?;
let contact = Contact::get_by_id(context, from_id).await?;
match contact.is_blocked() {
true => Blocked::Yes,
false if is_bot => Blocked::Not,
@@ -810,7 +804,7 @@ async fn add_parts(
}
}
if chat_id.is_none() && allow_creation {
let to_contact = Contact::load_from_db(context, to_id).await?;
let to_contact = Contact::get_by_id(context, to_id).await?;
if let Some(list_id) = to_contact.param.get(Param::ListId) {
if let Some((id, _, blocked)) =
chat::get_chat_id_by_grpid(context, list_id).await?
@@ -1641,86 +1635,137 @@ async fn apply_group_changes(
return Ok(None);
}
let mut recreate_member_list = false;
let mut send_event_chat_modified = false;
let mut removed_id = None;
let mut better_msg = None;
let removed_id;
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?;
recreate_member_list = true;
match removed_id {
Some(contact_id) => {
better_msg = if contact_id == from_id {
Some(stock_str::msg_group_left(context, from_id).await)
} else {
Some(stock_str::msg_del_member(context, &removed_addr, from_id).await)
};
// True if a Delta Chat client has explicitly added our current primary address.
let self_added =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
context.get_primary_self_addr().await? == *added_addr
} else {
false
};
// Whether to allow any changes to the member list at all.
let allow_member_list_changes =
if chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? || self_added {
// Reject old group changes.
chat_id
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
.await?
} else {
// Member list changes are not allowed if we're not in the group
// and are not explicitly added.
// This message comes from a Delta Chat that restored an old backup
// or the message is a MUA reply to an old message.
false
};
// Whether to rebuild the member list from scratch.
let recreate_member_list = if allow_member_list_changes {
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
// Always recreate membership list if self has been added.
if !mime_parser.has_chat_version() || self_added {
true
} else {
match mime_parser.get_header(HeaderDef::InReplyTo) {
// If we don't know the referenced message, we missed some messages.
// Maybe they added/removed members, so we need to recreate our member list.
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
None => false,
}
None => warn!(context, "Removed {removed_addr:?} has no contact_id."),
}
} else {
removed_id = None;
if let Some(added_member) = mime_parser
.get_header(HeaderDef::ChatGroupMemberAdded)
.cloned()
{
better_msg = Some(stock_str::msg_add_member(context, &added_member, from_id).await);
recreate_member_list = true;
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
// See create_or_lookup_group() for explanation
.map(|s| s.trim())
{
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
// See create_or_lookup_group() for explanation
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200)
{
if chat_id
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
.await?
{
info!(context, "Updating grpname for chat {chat_id}.");
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
(strip_rtlo_characters(grpname), chat_id),
)
.await?;
false
};
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if let Some(contact_id) = removed_id {
if allow_member_list_changes {
// Remove a single member from the chat.
if !recreate_member_list {
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
send_event_chat_modified = true;
}
better_msg =
Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
} else {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
}
} 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
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
}
};
} else {
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
// Add a single member to the chat.
if !recreate_member_list {
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
chat::add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
send_event_chat_modified = true;
} else {
warn!(context, "Added {added_addr:?} has no contact id.")
}
}
} else {
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
}
}
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
// See create_or_lookup_group() for explanation
.map(|s| s.trim())
{
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
// See create_or_lookup_group() for explanation
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200)
{
if chat_id
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
.await?
{
info!(context, "Updating grpname for chat {chat_id}.");
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
(strip_rtlo_characters(grpname), chat_id),
)
.await?;
send_event_chat_modified = true;
}
if !mime_parser.has_chat_version() {
// If a classical MUA user adds someone to TO/CC, then the DC user shall
// see this addition and have the new recipient in the member list.
recreate_member_list = true;
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
} 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
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
}
};
}
}
}
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
@@ -1734,49 +1779,46 @@ async fn apply_group_changes(
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
// add members to group/check members
// Recreate the member list.
if recreate_member_list {
if chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
&& !chat::is_contact_in_chat(context, chat_id, from_id).await?
{
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
warn!(
context,
"Contact {from_id} attempts to modify group chat {chat_id} member list without being a member."
);
} else if chat_id
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
.await?
{
let mut members_to_add = vec![];
if removed_id.is_some()
|| !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
{
// Members could have been removed while we were
// absent. We can't use existing member list and need to
// start from scratch.
} else {
// Only delete old contacts if the sender is not a classical MUA user:
// Classical MUA users usually don't intend to remove users from an email
// thread, so if they removed a recipient then it was probably by accident.
if mime_parser.has_chat_version() {
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
.await?;
members_to_add.push(ContactId::SELF);
}
let mut members_to_add = HashSet::new();
members_to_add.extend(to_ids);
members_to_add.insert(ContactId::SELF);
if !from_id.is_special() {
members_to_add.push(from_id);
members_to_add.insert(from_id);
}
members_to_add.extend(to_ids);
if let Some(removed_id) = removed_id {
members_to_add.retain(|id| *id != removed_id);
}
members_to_add.dedup();
info!(context, "Adding {members_to_add:?} to chat id={chat_id}.");
chat::add_to_chat_contacts_table(context, chat_id, &members_to_add).await?;
if let Some(removed_id) = removed_id {
members_to_add.remove(&removed_id);
}
info!(
context,
"Recreating chat {chat_id} with members {members_to_add:?}."
);
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add))
.await?;
send_event_chat_modified = true;
}
}
@@ -1964,7 +2006,7 @@ async fn apply_mailinglist_changes(
};
let (contact_id, _) =
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let mut contact = Contact::load_from_db(context, contact_id).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
contact.update_param(context).await?;
@@ -2082,7 +2124,7 @@ async fn check_verified_properties(
from_id: ContactId,
to_ids: &[ContactId],
) -> Result<()> {
let contact = Contact::load_from_db(context, from_id).await?;
let contact = Contact::get_by_id(context, from_id).await?;
ensure!(mimeparser.was_encrypted(), "This message is not encrypted");
@@ -2164,7 +2206,7 @@ async fn check_verified_properties(
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
// mark gossiped keys (if any) as verified
if mimeparser.gossiped_addr.contains(&to_addr) {
if mimeparser.gossiped_addr.contains(&to_addr.to_lowercase()) {
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

View File

@@ -2,7 +2,10 @@ use tokio::fs;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::get_chat_contacts;
use crate::chat::{
add_contact_to_chat, add_to_chat_contacts_table, create_group_chat, get_chat_contacts,
is_contact_in_chat, remove_contact_from_chat, send_text_msg,
};
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
use crate::constants::DC_GCL_NO_SPECIALS;
@@ -247,7 +250,7 @@ async fn test_read_receipt_and_unarchive() -> Result<()> {
.await?;
let msg = get_chat_msg(&t, group_id, 0, 1).await;
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert_eq!(msg.text.unwrap(), "hello");
assert_eq!(msg.text, "hello");
assert_eq!(msg.state, MessageState::OutDelivered);
let group = Chat::load_from_db(&t, group_id).await?;
assert!(group.get_visibility() == ChatVisibility::Normal);
@@ -404,7 +407,7 @@ async fn test_escaped_from() {
false,
).await.unwrap();
assert_eq!(
Contact::load_from_db(&t, contact_id)
Contact::get_by_id(&t, contact_id)
.await
.unwrap()
.get_authname(),
@@ -412,7 +415,7 @@ async fn test_escaped_from() {
);
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert_eq!(msg.text.unwrap(), "hello");
assert_eq!(msg.text, "hello");
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
}
@@ -449,7 +452,7 @@ async fn test_escaped_recipients() {
)
.await
.unwrap();
let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, carl_contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "h2");
@@ -458,7 +461,7 @@ async fn test_escaped_recipients() {
.await
.unwrap();
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert_eq!(msg.text.unwrap(), "hello");
assert_eq!(msg.text, "hello");
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
}
@@ -496,7 +499,7 @@ async fn test_cc_to_contact() {
)
.await
.unwrap();
let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, carl_contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Carl");
}
@@ -710,7 +713,7 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
assert_eq!(
last_msg.text,
Some(stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await,)
stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await
);
assert_eq!(last_msg.from_id, ContactId::INFO);
Ok(())
@@ -731,7 +734,7 @@ async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
async fn test_html_only_mail() {
let t = TestContext::new_alice().await;
let msg = load_imf_email(&t, include_bytes!("../../test-data/message/wrong-html.eml")).await;
assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]");
assert_eq!(msg.text, "Guten Abend,\n\nLots of text\n\ntext with Umlaut ä...\n\nMfG\n\n--------------------------------------\n\n[Camping ](https://example.com/)\n\nsomeaddress\n\nsometown");
}
static GH_MAILINGLIST: &[u8] =
@@ -794,12 +797,12 @@ async fn test_github_mailing_list() -> Result<()> {
assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts"
let msg1 = get_chat_msg(&t, chat_id, 0, 2).await;
let contact1 = Contact::load_from_db(&t.ctx, msg1.from_id).await?;
let contact1 = Contact::get_by_id(&t.ctx, msg1.from_id).await?;
assert_eq!(contact1.get_addr(), "notifications@github.com");
assert_eq!(contact1.get_display_name(), "notifications@github.com"); // Make sure this is not "Max Mustermann" or somethinng
let msg2 = get_chat_msg(&t, chat_id, 1, 2).await;
let contact2 = Contact::load_from_db(&t.ctx, msg2.from_id).await?;
let contact2 = Contact::get_by_id(&t.ctx, msg2.from_id).await?;
assert_eq!(contact2.get_addr(), "notifications@github.com");
assert_eq!(msg1.get_override_sender_name().unwrap(), "Max Mustermann");
@@ -844,7 +847,7 @@ async fn test_classic_mailing_list() -> Result<()> {
assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net"));
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap();
let contact1 = Contact::get_by_id(&t.ctx, msg.from_id).await.unwrap();
assert_eq!(contact1.get_addr(), "bob@posteo.org");
let sent = t.send_text(chat.id, "Hello mailinglist!").await;
@@ -887,7 +890,7 @@ async fn test_other_device_writes_to_mailinglist() -> Result<()> {
Contact::lookup_id_by_addr(&t, "delta@codespeak.net", Origin::Unknown)
.await?
.unwrap();
let list_post_contact = Contact::load_from_db(&t, list_post_contact_id).await?;
let list_post_contact = Contact::get_by_id(&t, list_post_contact_id).await?;
assert_eq!(
list_post_contact.param.get(Param::ListId).unwrap(),
"delta.codespeak.net"
@@ -1155,10 +1158,7 @@ async fn test_dhl_mailing_list() -> Result<()> {
.await
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(
msg.text,
Some("Ihr Paket ist in der Packstation 123 bla bla".to_string())
);
assert_eq!(msg.text, "Ihr Paket ist in der Packstation 123 bla bla");
assert!(msg.has_html());
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Mailinglist);
@@ -1183,10 +1183,7 @@ async fn test_dpd_mailing_list() -> Result<()> {
.await
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(
msg.text,
Some("Bald ist Ihr DPD Paket da bla bla".to_string())
);
assert_eq!(msg.text, "Bald ist Ihr DPD Paket da bla bla");
assert!(msg.has_html());
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Mailinglist);
@@ -1291,10 +1288,7 @@ async fn test_mailing_list_with_mimepart_footer() {
.await
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(
msg.text,
Some("[Intern] important stuff Hi mr ... [text part]".to_string())
);
assert_eq!(msg.text, "[Intern] important stuff Hi mr ... [text part]");
assert!(msg.has_html());
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(get_chat_msgs(&t, msg.chat_id).await.unwrap().len(), 1);
@@ -1317,7 +1311,7 @@ async fn test_mailing_list_with_mimepart_footer_signed() {
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(get_chat_msgs(&t, msg.chat_id).await.unwrap().len(), 1);
let text = msg.text.clone().unwrap();
let text = msg.text.clone();
assert!(text.contains("content text"));
assert!(!text.contains("footer text"));
assert!(msg.has_html());
@@ -1362,7 +1356,7 @@ async fn test_apply_mailinglist_changes_assigned_by_reply() {
.await
.unwrap()
.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(
contact.param.get(Param::ListId).unwrap(),
"deltachat-core-rust.deltachat.github.com"
@@ -1381,7 +1375,7 @@ async fn test_mailing_list_chat_message() {
.await
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(msg.text, Some("hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string()));
assert_eq!(msg.text, "hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string());
assert!(!msg.has_html());
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Mailinglist);
@@ -1461,7 +1455,7 @@ async fn test_pdf_filename_simple() {
)
.await;
assert_eq!(msg.viewtype, Viewtype::File);
assert_eq!(msg.text.unwrap(), "mail body");
assert_eq!(msg.text, "mail body");
assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf");
}
@@ -1475,7 +1469,7 @@ async fn test_pdf_filename_continuation() {
)
.await;
assert_eq!(msg.viewtype, Viewtype::File);
assert_eq!(msg.text.unwrap(), "mail body");
assert_eq!(msg.text, "mail body");
assert_eq!(
msg.param.get(Param::File).unwrap(),
"$BLOBDIR/test pdf äöüß.pdf"
@@ -1554,7 +1548,7 @@ async fn test_in_reply_to() {
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "reply foo");
assert_eq!(msg.get_text(), "reply foo");
// Load the first message from the same chat.
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await.unwrap();
@@ -1565,7 +1559,7 @@ async fn test_in_reply_to() {
};
let reply_msg = Message::load_from_db(&t, *msg_id).await.unwrap();
assert_eq!(reply_msg.get_text().unwrap(), "hello foo");
assert_eq!(reply_msg.get_text(), "hello foo");
// Check that reply got into the same chat as the original message.
assert_eq!(msg.chat_id, reply_msg.chat_id);
@@ -1623,7 +1617,7 @@ async fn test_in_reply_to_two_member_group() {
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(msg.get_text().unwrap(), "classic reply");
assert_eq!(msg.get_text(), "classic reply");
// Receive a Delta Chat reply from Alice.
// It is assigned to group chat, because it has a group ID.
@@ -1649,7 +1643,7 @@ async fn test_in_reply_to_two_member_group() {
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(msg.get_text().unwrap(), "chat reply");
assert_eq!(msg.get_text(), "chat reply");
// Receive a private Delta Chat reply from Alice.
// It is assigned to 1:1 chat, because it has no group ID,
@@ -1676,7 +1670,7 @@ async fn test_in_reply_to_two_member_group() {
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(msg.get_text().unwrap(), "private reply");
assert_eq!(msg.get_text(), "private reply");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1687,7 +1681,7 @@ async fn test_save_mime_headers_off() -> anyhow::Result<()> {
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), Some("hi!".to_string()));
assert_eq!(msg.get_text(), "hi!");
assert!(!msg.get_showpadlock());
let mime = message::get_mime_headers(&bob, msg.id).await?;
assert!(mime.is_empty());
@@ -1706,7 +1700,7 @@ async fn test_save_mime_headers_on() -> anyhow::Result<()> {
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), Some("hi!".to_string()));
assert_eq!(msg.get_text(), "hi!");
assert!(!msg.get_showpadlock());
let mime = message::get_mime_headers(&bob, msg.id).await?;
let mime_str = String::from_utf8_lossy(&mime);
@@ -1717,7 +1711,7 @@ async fn test_save_mime_headers_on() -> anyhow::Result<()> {
let chat_bob = bob.create_chat(&alice).await;
chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), Some("ho!".to_string()));
assert_eq!(msg.get_text(), "ho!");
assert!(msg.get_showpadlock());
let mime = message::get_mime_headers(&alice, msg.id).await?;
let mime_str = String::from_utf8_lossy(&mime);
@@ -1777,7 +1771,7 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_subject(), "i have a question");
assert!(msg.get_text().unwrap().contains("hi support!"));
assert!(msg.get_text().contains("hi support!"));
let chat = Chat::load_from_db(&alice, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(get_chat_msgs(&alice, chat.id).await.unwrap().len(), 1);
@@ -1802,7 +1796,7 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
let msg = Message::load_from_db(&claire, msg_id).await.unwrap();
msg.chat_id.accept(&claire).await.unwrap();
assert_eq!(msg.get_subject(), "i have a question");
assert!(msg.get_text().unwrap().contains("hi support!"));
assert!(msg.get_text().contains("hi support!"));
let chat = Chat::load_from_db(&claire, msg.chat_id).await.unwrap();
if group_request {
assert_eq!(chat.typ, Chattype::Group);
@@ -1823,7 +1817,7 @@ async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool
receive_imf(&alice, reply, false).await.unwrap();
let answer = alice.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0"));
assert!(answer.get_text().contains("the version is 1.0"));
assert_eq!(answer.chat_id, request.chat_id);
let chat_contacts = get_chat_contacts(&alice, answer.chat_id)
.await
@@ -1846,7 +1840,7 @@ async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool
receive_imf(&claire, reply, false).await.unwrap();
let answer = claire.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0"));
assert!(answer.get_text().contains("the version is 1.0"));
assert_eq!(answer.chat_id, request.chat_id);
assert_eq!(
answer.get_override_sender_name().unwrap(),
@@ -1918,7 +1912,7 @@ async fn test_dont_assign_to_trash_by_parent() {
chat_id.accept(&t).await.unwrap();
let msg = get_chat_msg(&t, chat_id, 0, 1).await; // Make sure that the message is actually in the chat
assert!(!msg.chat_id.is_special());
assert_eq!(msg.text.unwrap(), "Hi hello");
assert_eq!(msg.text, "Hi hello");
println!("\n========= Delete the message ==========");
msg.id.trash(&t).await.unwrap();
@@ -1942,7 +1936,7 @@ async fn test_dont_assign_to_trash_by_parent() {
.unwrap();
let msg = t.get_last_msg().await;
assert!(!msg.chat_id.is_special()); // Esp. check that the chat_id is not TRASH
assert_eq!(msg.text.unwrap(), "Reply");
assert_eq!(msg.text, "Reply");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1993,7 +1987,7 @@ Message content",
// Outgoing email should create a chat.
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Subj Message content");
assert_eq!(msg.get_text(), "Subj Message content");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2035,12 +2029,12 @@ Message content
Second signature";
receive_imf(&alice, first_message, false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
let contact = Contact::get_by_id(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "First signature");
assert_eq!(contact.get_display_name(), "Bob1");
receive_imf(&alice, second_message, false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
let contact = Contact::get_by_id(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "Second signature");
assert_eq!(contact.get_display_name(), "Bob2");
@@ -2048,7 +2042,7 @@ Second signature";
receive_imf(&alice, first_message, false).await?;
// No change because last message is duplicate of the first.
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
let contact = Contact::get_by_id(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "Second signature");
assert_eq!(contact.get_display_name(), "Bob2");
@@ -2066,7 +2060,7 @@ async fn test_ignore_footer_status_from_mailinglist() -> Result<()> {
)
.await?
.0;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "");
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
@@ -2086,7 +2080,7 @@ Original signature",
.await?;
let msg = t.get_last_msg().await;
let one2one_chat_id = msg.chat_id;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature");
assert!(!msg.has_html());
@@ -2109,7 +2103,7 @@ Tap here to unsubscribe ...",
)
.await?;
let ml_chat_id = t.get_last_msg().await.chat_id;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature");
receive_imf(
@@ -2126,7 +2120,7 @@ Original signature updated",
false,
)
.await?;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature updated");
assert_eq!(get_chat_msgs(&t, one2one_chat_id).await?.len(), 2);
assert_eq!(get_chat_msgs(&t, ml_chat_id).await?.len(), 1);
@@ -2161,7 +2155,7 @@ sig wednesday",
)
.await?;
let chat_id = t.get_last_msg().await.chat_id;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "sig wednesday");
assert_eq!(get_chat_msgs(&t, chat_id).await?.len(), 1);
@@ -2179,7 +2173,7 @@ sig tuesday",
false,
)
.await?;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "sig wednesday");
assert_eq!(get_chat_msgs(&t, chat_id).await?.len(), 2);
@@ -2197,7 +2191,7 @@ sig thursday",
false,
)
.await?;
let bob = Contact::load_from_db(&t, bob_id).await?;
let bob = Contact::get_by_id(&t, bob_id).await?;
assert_eq!(bob.get_status(), "sig thursday");
assert_eq!(get_chat_msgs(&t, chat_id).await?.len(), 3);
@@ -2240,7 +2234,7 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
let group_msg = t.get_last_msg().await;
assert_eq!(
group_msg.text.unwrap(),
group_msg.text,
if *outgoing_is_classical {
"single reply-to Hello, I\'ve just created the group \"single reply-to\" for us."
} else {
@@ -2280,7 +2274,7 @@ Private reply"#,
.unwrap();
let private_msg = t.get_last_msg().await;
assert_eq!(private_msg.text.unwrap(), "Private reply");
assert_eq!(private_msg.text, "Private reply");
let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap();
assert_eq!(private_chat.typ, Chattype::Single);
assert_ne!(private_msg.chat_id, group_msg.chat_id);
@@ -2329,7 +2323,7 @@ Message-ID: <Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org>"
.unwrap();
let group_msg = t.get_last_msg().await;
assert_eq!(
group_msg.text.unwrap(),
group_msg.text,
if *outgoing_is_classical {
"single reply-to Hello, I\'ve just created the group \"single reply-to\" for us."
} else {
@@ -2375,7 +2369,7 @@ Sent with my Delta Chat Messenger: https://delta.chat
.unwrap();
let private_msg = t.get_last_msg().await;
assert_eq!(private_msg.text.unwrap(), "Private reply");
assert_eq!(private_msg.text, "Private reply");
let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap();
assert_eq!(private_chat.typ, Chattype::Single);
assert_ne!(private_msg.chat_id, group_msg.chat_id);
@@ -2417,7 +2411,7 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
let group_msg = t.get_last_msg().await;
assert_eq!(
group_msg.text.unwrap(),
group_msg.text,
if *outgoing_is_classical {
"single reply-to Hello, I\'ve just created the group \"single reply-to\" for us."
} else {
@@ -2454,7 +2448,7 @@ Outgoing reply to all"#,
.unwrap();
let reply = t.get_last_msg().await;
assert_eq!(reply.text.unwrap(), "Out subj Outgoing reply to all");
assert_eq!(reply.text, "Out subj Outgoing reply to all");
let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap();
assert_eq!(reply_chat.typ, Chattype::Group);
assert_eq!(reply.chat_id, group_msg.chat_id);
@@ -2477,7 +2471,7 @@ Reply to all"#,
.unwrap();
let reply = t.get_last_msg().await;
assert_eq!(reply.text.unwrap(), "In subj Reply to all");
assert_eq!(reply.text, "In subj Reply to all");
let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap();
assert_eq!(reply_chat.typ, Chattype::Group);
assert_eq!(reply.chat_id, group_msg.chat_id);
@@ -2763,7 +2757,7 @@ Hi, I created a group"#,
.await?;
let msg_out = t.get_last_msg().await;
assert_eq!(msg_out.from_id, ContactId::SELF);
assert_eq!(msg_out.text.unwrap(), "Hi, I created a group");
assert_eq!(msg_out.text, "Hi, I created a group");
assert_eq!(msg_out.in_reply_to, None);
// Bob replies from a different address
@@ -2787,7 +2781,7 @@ Reply from different address
.await?;
let msg_in = t.get_last_msg().await;
assert_eq!(msg_in.to_id, ContactId::SELF);
assert_eq!(msg_in.text.unwrap(), "Reply from different address");
assert_eq!(msg_in.text, "Reply from different address");
assert_eq!(
msg_in.in_reply_to.unwrap(),
"Gr.qetqsutor7a.Aresxresy-4@deltachat.de"
@@ -2865,22 +2859,22 @@ async fn test_accept_outgoing() -> Result<()> {
alice1.recv_msg(&sent).await;
alice2.recv_msg(&sent).await;
let alice1_msg = bob2.recv_msg(&sent).await;
assert_eq!(alice1_msg.text.unwrap(), "Hello!");
assert_eq!(alice1_msg.text, "Hello!");
let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?;
assert!(alice1_chat.is_contact_request());
let alice2_msg = alice2.get_last_msg().await;
assert_eq!(alice2_msg.text.unwrap(), "Hello!");
assert_eq!(alice2_msg.text, "Hello!");
let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?;
assert!(alice2_chat.is_contact_request());
let bob1_msg = bob1.get_last_msg().await;
assert_eq!(bob1_msg.text.unwrap(), "Hello!");
assert_eq!(bob1_msg.text, "Hello!");
let bob1_chat = chat::Chat::load_from_db(&bob1, bob1_msg.chat_id).await?;
assert!(!bob1_chat.is_contact_request());
let bob2_msg = bob2.get_last_msg().await;
assert_eq!(bob2_msg.text.unwrap(), "Hello!");
assert_eq!(bob2_msg.text, "Hello!");
let bob2_chat = chat::Chat::load_from_db(&bob2, bob2_msg.chat_id).await?;
assert!(!bob2_chat.is_contact_request());
@@ -2926,7 +2920,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
assert_eq!(received.from_id, alice1_bob_contact.id);
assert_eq!(received.to_id, ContactId::SELF);
assert!(!received.hidden);
assert_eq!(received.text, Some("Hello all!".to_string()));
assert_eq!(received.text, "Hello all!");
assert_eq!(received.in_reply_to, None);
assert_eq!(received.chat_blocked, Blocked::Request);
@@ -2936,7 +2930,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request
let mut msg_out = Message::new(Viewtype::Text);
msg_out.set_text(Some("Private reply".to_string()));
msg_out.set_text("Private reply".to_string());
assert_eq!(received_group.blocked, Blocked::Request);
msg_out.set_quote(&alice1, Some(&received)).await?;
@@ -2954,10 +2948,10 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
assert_eq!(received.from_id, ContactId::SELF);
assert_eq!(received.to_id, alice2_bob_contact.id);
assert!(!received.hidden);
assert_eq!(received.text, Some("Private reply".to_string()));
assert_eq!(received.text, "Private reply");
assert_eq!(
received.parent(&alice2).await?.unwrap().text,
Some("Hello all!".to_string())
"Hello all!".to_string()
);
assert_eq!(received.chat_blocked, Blocked::Not);
@@ -3018,13 +3012,13 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
// =============== Alice replies private to Bob ==============
let received = alice.get_last_msg().await;
assert_eq!(received.text, Some("Hello all!".to_string()));
assert_eq!(received.text, "Hello all!");
let received_group = Chat::load_from_db(&alice, received.chat_id).await?;
assert_eq!(received_group.typ, Chattype::Group);
let mut msg_out = Message::new(Viewtype::Text);
msg_out.set_text(Some("Private reply".to_string()));
msg_out.set_text("Private reply".to_string());
msg_out.set_quote(&alice, Some(&received)).await?;
let alice_bob_chat = alice.create_chat(&bob).await;
@@ -3040,7 +3034,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
// since only chat is a group, no new open chat has been created
assert_eq!(chat.typ, Chattype::Group);
let received = bob.get_last_msg().await;
assert_eq!(received.text, Some("Hello all!".to_string()));
assert_eq!(received.text, "Hello all!");
// =============== Bob unblocks Alice ================
// test if the blocked chat is restored correctly
@@ -3051,7 +3045,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Single);
let received = bob.get_last_msg().await;
assert_eq!(received.text, Some("Private reply".to_string()));
assert_eq!(received.text, "Private reply");
Ok(())
}
@@ -3302,3 +3296,268 @@ async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "", "claire@example.de").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
bob.recv_msg(&add).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
// remove bob from chat
remove_contact_from_chat(&alice, alice_chat_id, bob_id).await?;
let remove_bob = alice.pop_sent_msg().await;
bob.recv_msg(&remove_bob).await;
// remove any other member
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
alice.pop_sent_msg().await;
// readd bob
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
let add2 = alice.pop_sent_msg().await;
bob.recv_msg(&add2).await;
// number of members in chat should have updated
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(&bob).await?;
// alice adds a member
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "fiona", "fiona@example.net").await?,
)
.await?;
// bob adds a member.
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
// bob didn't receive the addition of fiona, but alice doesn't overwrite her own
// contact list with the one from bob which only has three members instead of four.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
// bob removes a member.
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
// Bobs chat only has two members after the removal of blue, because he still
// didn't receive the addition of fiona. But that doesn't overwrite alice'
// memberlist.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_fiona = Contact::create(&alice, "fiona", "fiona@example.net").await?;
// create chat with three members
add_to_chat_contacts_table(
&alice,
chat_id,
&[
Contact::create(&alice, "bob", "bob@example.net").await?,
alice_fiona,
],
)
.await?;
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(&bob).await?;
// bob removes a member
let bob_contact_fiona = Contact::create(&bob, "fiona", "fiona@example.net").await?;
remove_contact_from_chat(&bob, bob_chat_id, bob_contact_fiona).await?;
let remove_msg = bob.pop_sent_msg().await;
// bob adds a new member
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
let add_msg = bob.pop_sent_msg().await;
// alice only receives the addition of the member
alice.recv_msg(&add_msg).await;
// since we missed a message, a new contact list should be build
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 3);
// readd fiona
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
alice.recv_msg(&remove_msg).await;
// delayed removal of fiona shouldn't remove her
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_readd_with_normal_msg() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(&bob).await?;
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "fiora", "fiora@example.net").await?,
)
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Alice didn't receive Bobs leave message, but bob shouldn't readded himself just because of that.
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mua_cant_remove() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice creates chat with 3 contacts
let msg = receive_imf(
&alice,
b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
From: alice@example.org\r\n\
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
Chat-Version: 1.0\r\n\
\r\n\
tst\r\n",
false,
)
.await?
.unwrap();
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_chat.typ, Chattype::Group);
// Bob uses a classical MUA to answer, removing a recipient.
let bob_removes = receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>, <claire@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
\r\n\
Hi back!\r\n",
false,
)
.await?
.unwrap();
assert_eq!(bob_removes.chat_id, alice_chat.id);
let group_chat = Chat::load_from_db(&alice, bob_removes.chat_id).await?;
assert_eq!(group_chat.typ, Chattype::Group);
assert_eq!(
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
4
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mua_can_add() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice creates chat with 3 contacts
let msg = receive_imf(
&alice,
b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
From: alice@example.org\r\n\
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
Chat-Version: 1.0\r\n\
\r\n\
Hi!\r\n",
false,
)
.await?
.unwrap();
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_chat.typ, Chattype::Group);
// Bob uses a classical MUA to answer, adding a recipient.
let bob_adds = receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>, <claire@example.org>, <fiona@example.org>, <greg@example.host>\r\n\
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
\r\n\
Hi back!\r\n",
false,
)
.await?
.unwrap();
let group_chat = Chat::load_from_db(&alice, bob_adds.chat_id).await?;
assert_eq!(group_chat.typ, Chattype::Group);
assert_eq!(
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
5
);
Ok(())
}

View File

@@ -178,7 +178,7 @@ async fn send_alice_handshake_msg(
) -> Result<()> {
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(format!("Secure-Join: {step}")),
text: format!("Secure-Join: {step}"),
hidden: true,
..Default::default()
};
@@ -210,7 +210,7 @@ async fn fingerprint_equals_sender(
fingerprint: &Fingerprint,
contact_id: ContactId,
) -> Result<bool> {
let contact = Contact::load_from_db(context, contact_id).await?;
let contact = Contact::get_by_id(context, contact_id).await?;
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
Err(err) => {
@@ -411,7 +411,7 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
return Ok(HandshakeMessage::Ignore);
}
let contact_addr = Contact::load_from_db(context, contact_id)
let contact_addr = Contact::get_by_id(context, contact_id)
.await?
.get_addr()
.to_owned();
@@ -580,10 +580,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
.await?;
return Ok(HandshakeMessage::Ignore);
}
let addr = Contact::load_from_db(context, contact_id)
let addr = Contact::get_by_id(context, contact_id)
.await?
.get_addr()
.to_string();
.to_lowercase();
if mime_message.gossiped_addr.contains(&addr) {
let mut peerstate = match Peerstate::from_addr(context, &addr).await? {
Some(p) => p,
@@ -640,7 +640,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
if mark_peer_as_verified(
context,
fingerprint,
Contact::load_from_db(context, contact_id)
Contact::get_by_id(context, contact_id)
.await?
.get_addr()
.to_owned(),
@@ -889,7 +889,7 @@ mod tests {
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
@@ -933,8 +933,7 @@ mod tests {
.expect("No messages in Alice's 1:1 chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
assert!(msg.get_text().contains("bob@example.net verified"));
}
// Check Alice sent the right message to Bob.
@@ -952,7 +951,7 @@ mod tests {
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
@@ -982,8 +981,7 @@ mod tests {
.expect("No messages in Bob's 1:1 chat");
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("alice@example.org verified"));
assert!(msg.get_text().contains("alice@example.org verified"));
}
// Check Bob sent the final message
@@ -1078,7 +1076,7 @@ mod tests {
Origin::ManuallyCreated,
)
.await?;
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?;
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
@@ -1105,7 +1103,7 @@ mod tests {
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id).await?;
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
@@ -1246,7 +1244,7 @@ mod tests {
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
.await?
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?;
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
@@ -1292,8 +1290,7 @@ mod tests {
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
assert!(msg.get_text().contains("bob@example.net verified"));
}
// Bob should not yet have Alice verified
@@ -1302,7 +1299,7 @@ mod tests {
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id).await?;
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
@@ -1328,7 +1325,7 @@ mod tests {
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() {
if let chat::ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
let text = msg.get_text();
println!("msg {msg_id} text: {text}");
}
}
@@ -1340,7 +1337,7 @@ mod tests {
match msg_iter.next() {
Some(chat::ChatItem::Message { msg_id }) => {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
let text = msg.get_text();
match text.contains("alice@example.org verified") {
true => {
assert!(msg.is_info());

View File

@@ -422,7 +422,7 @@ async fn send_handshake_message(
) -> Result<()> {
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(step.body_text(invite)),
text: step.body_text(invite),
hidden: true,
..Default::default()
};

View File

@@ -72,7 +72,7 @@ pub(crate) fn split_lines(buf: &str) -> Vec<&str> {
}
/// Simplified text and some additional information gained from the input.
#[derive(Debug, Default)]
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct SimplifiedText {
/// The text itself.
pub text: String,
@@ -91,6 +91,14 @@ pub(crate) struct SimplifiedText {
pub footer: Option<String>,
}
pub(crate) fn simplify_quote(quote: &str) -> (String, bool) {
let quote_lines = split_lines(quote);
let (quote_lines, quote_footer_lines) = remove_message_footer(&quote_lines);
let is_cut = quote_footer_lines.is_some();
(render_message(quote_lines, false), is_cut)
}
/// Simplify message text for chat display.
/// Remove quotes, signatures, trailing empty lines etc.
pub(crate) fn simplify(mut input: String, is_chat_message: bool) -> SimplifiedText {
@@ -125,11 +133,9 @@ pub(crate) fn simplify(mut input: String, is_chat_message: bool) -> SimplifiedTe
if !is_chat_message {
top_quote = top_quote.map(|quote| {
let quote_lines = split_lines(&quote);
let (quote_lines, quote_footer_lines) = remove_message_footer(&quote_lines);
is_cut = is_cut || quote_footer_lines.is_some();
render_message(quote_lines, false)
let (quote, quote_cut) = simplify_quote(&quote);
is_cut |= quote_cut;
quote
});
}
@@ -235,12 +241,11 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
let mut ret = String::new();
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut empty_body = true;
for line in lines {
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if !empty_body {
if !ret.is_empty() {
if pending_linebreaks > 2 {
pending_linebreaks = 2
}
@@ -251,11 +256,10 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
}
// the incoming message might contain invalid UTF8
ret += line;
empty_body = false;
pending_linebreaks = 1
}
}
if is_cut_at_end && !empty_body {
if is_cut_at_end && !ret.is_empty() {
ret += " [...]";
}
// redo escaping done by escape_message_footer_marks()

View File

@@ -683,7 +683,7 @@ async fn send_mdn_msg_id(
contact_id: ContactId,
smtp: &mut Smtp,
) -> Result<()> {
let contact = Contact::load_from_db(context, contact_id).await?;
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.is_blocked() {
return Err(format_err!("Contact is blocked"));
}

View File

@@ -245,7 +245,7 @@ impl Sql {
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
if context.get_config_delete_server_after().await?.is_some() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::delete_server_turned_off(context).await);
msg.set_text(stock_str::delete_server_turned_off(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
context
.set_config(Config::DeleteServerAfter, Some("0"))
@@ -1102,13 +1102,13 @@ mod tests {
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
let mut new_draft = Message::new(Viewtype::Text);
new_draft.set_text(Some("This is my draft".to_string()));
new_draft.set_text("This is my draft".to_string());
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
housekeeping(&t).await.unwrap();
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
assert_eq!(loaded_draft.unwrap().text.unwrap(), "This is my draft");
assert_eq!(loaded_draft.unwrap().text, "This is my draft");
}
/// Tests that `housekeeping` deletes the blobs backup dir which is created normally by

View File

@@ -410,6 +410,15 @@ pub enum StockMessage {
#[strum(props(fallback = " Account transferred to your second device."))]
BackupTransferMsgBody = 163,
#[strum(props(fallback = "I added member %1$s."))]
MsgIAddMember = 164,
#[strum(props(fallback = "I removed member %1$s."))]
MsgIDelMember = 165,
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
}
impl StockMessage {
@@ -588,17 +597,35 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
}
}
/// Stock string: `Member %1$s added.`.
/// Stock string: `I added member %1$s.`.
///
/// The `added_member_addr` parameter should be an email address and is looked up in the
/// contacts to combine with the authorized display name.
pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
let addr = added_member_addr;
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_authname_n_addr())
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
translated(context, StockMessage::MsgIAddMember)
.await
.replace1(whom)
}
/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
///
/// The `added_member_addr` parameter should be an email address and is looked up in the
/// contacts to combine with the display name.
pub(crate) async fn msg_add_member(
pub(crate) async fn msg_add_member_local(
context: &Context,
added_member_addr: &str,
by_contact: ContactId,
) -> String {
let addr = added_member_addr;
let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
@@ -608,26 +635,44 @@ pub(crate) async fn msg_add_member(
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouAddMember)
.await
.replace1(who)
.replace1(whom)
} else {
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(who)
.replace1(whom)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Member %1$s removed.`.
/// Stock string: `I removed member %1$s.`.
///
/// The `removed_member_addr` parameter should be an email address and is looked up in
/// the contacts to combine with the display name.
pub(crate) async fn msg_del_member(
pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
let addr = removed_member_addr;
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_authname_n_addr())
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
translated(context, StockMessage::MsgIDelMember)
.await
.replace1(whom)
}
/// Stock string: `I added member %1$s.` or `Member %1$s removed by %2$s.`.
///
/// The `removed_member_addr` parameter should be an email address and is looked up in
/// the contacts to combine with the display name.
pub(crate) async fn msg_del_member_local(
context: &Context,
removed_member_addr: &str,
by_contact: ContactId,
) -> String {
let addr = removed_member_addr;
let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
@@ -637,17 +682,22 @@ pub(crate) async fn msg_del_member(
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDelMember)
.await
.replace1(who)
.replace1(whom)
} else {
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(who)
.replace1(whom)
.replace2(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Group left.`.
pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) -> String {
/// Stock string: `I left the group.`.
pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
translated(context, StockMessage::MsgILeftGroup).await
}
/// Stock string: `You left the group.` or `Group left by %1$s.`.
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouLeftGroup).await
} else {
@@ -1313,7 +1363,7 @@ impl Context {
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(welcome_message(self).await);
msg.text = welcome_message(self).await;
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
Ok(())
}
@@ -1387,7 +1437,7 @@ mod tests {
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
// uses %1$s substitution
assert_eq!(
contact_verified(&t, &contact).await,
@@ -1409,7 +1459,11 @@ mod tests {
async fn test_stock_system_msg_add_member_by_me() {
let t = TestContext::new().await;
assert_eq!(
msg_add_member(&t, "alice@example.org", ContactId::SELF).await,
msg_add_member_remote(&t, "alice@example.org").await,
"I added member alice@example.org."
);
assert_eq!(
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
"You added member alice@example.org."
)
}
@@ -1421,7 +1475,11 @@ mod tests {
.await
.expect("failed to create contact");
assert_eq!(
msg_add_member(&t, "alice@example.org", ContactId::SELF).await,
msg_add_member_remote(&t, "alice@example.org").await,
"I added member alice@example.org."
);
assert_eq!(
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
"You added member Alice (alice@example.org)."
);
}
@@ -1438,7 +1496,7 @@ mod tests {
.expect("failed to create bob")
};
assert_eq!(
msg_add_member(&t, "alice@example.org", contact_id,).await,
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
);
}

Some files were not shown because too many files have changed in this diff Show More