Compare commits

..
Author SHA1 Message Date
link2xt d8b9fd5005 ci: test with Python 3.15
Also moved python version to environment variable
so status checks will be named
"JSON-RPC Python tests (ubuntu-latest, latest)" instead of
"JSON-RPC Python tests (ubuntu-latest, 3.14)" and we don't have to
update the list of required status checks each time the version changes.
We already do the same for Rust.
2026-10-10 18:46:18 +00:00
link2xt 694ef8ba76 chore: bump version to 2.64.0-dev 2026-10-09 17:59:58 +00:00
link2xt de6f469710 chore(release): prepare for 2.63.0 2026-10-09 17:39:17 +00:00
link2xt 81140d514f fix: do not reencode animated WebPs into JPEG
Reencoding animated WebPs into JPEG loses all frames except the first one.
Avatars we still reencode because we don't want animated avatars anyway.

animated.webp is produced with this script using Pillow 12.3.0:
```
#!/usr/bin/env python3
from PIL import Image, ImageOps

with Image.open("avatar900x900.png") as im:
    frames = [im.rotate(x) for x in range(10, 360, 10)]
    im.save(
        "animated.webp",
        "webp",
        append_images=frames,
        lossless=True,
        duration=200,
        save_all=True,
    )
```

Resulting image is 677K, just slightly larger than the size needed to trigger reencoding.
2026-10-09 15:48:18 +00:00
holger krekel a8b6246052 fix: (linux-only) make sure large attachments return memory to kernel
The test fails on main with ~150MB memory remaining allocated
after sending/receiving two 20MB messages,
and stays well below when explicitely setting libc's M_MMAP_THRESHOLD.
2026-10-09 17:12:17 +02:00
biørnandHocuri 4ea5cddc09 api: new dc_msg_get_summary_text() (#8813)
this PR adds a new `msg_get_summary_text(add_forwarded, add_type_emoji,
approx_chars)` api, replacing the old `get_summarytext(approx_chars)`.

checking the concrete existing and new usecases, the new options are
required, eg. for:

- pinned messages: they do not show the user name (therefore, there
should not be a "forward" indicator), and may or may not not e.g.
display image left of string (this makes type emoji optional)

- notifications, quotes: they show user name (therefore, "forward"
indicator needed) and often display e.g. image a bit off (so type emoji
is potentially useful, cmp. whatsapp, but potentially also optional)

- copy to clipboard: clipboard should get forwarded and emoji, but text
should not be truncated and linends should be preserved - esp. the
latter was a silent bug in at least android. to support that better,
`approx_chars` can be set to `0` to skip truncation and unwrapping.

| Use case | `add_forwarded` | `add_type_emoji` | `approx_chars>0` |
| --- | :---: | :---: | :---: |
| Notification | ✅ | ✅ (potentially ❌) | ✅ |
| Quote | ✅  | ✅ (potentially ❌) | ✅ |
| Pinned message | ❌  | ❌ (rarely  ✅) | ✅ |
| Copy to clipboard | ✅ | ✅ | ❌  |
| Chatlist (core internal) | ✅ | ✅ | ✅ |
| Reaction summary (core internal) | ❌ | ✅ | ✅ |

(the get_summary() returning a "lot" can maybe be removed at some point,
in favor of adding the usernames directly to get_summary_text() and
changing add_forwarded to add_usernames. but that is out of scope)

jsonrpc can be added easily once we settle on name etc. 

references: https://github.com/chatmail/core/pull/8812 ,
https://github.com/chatmail/core/pull/8546

cc @Amzd , @adbenitez

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-10-09 13:49:59 +00:00
link2xt f8edd9136a chore: update mail-builder to 1.0 2026-10-09 12:49:55 +00:00
link2xt 697304e98a test: test sending and receiving long filename with emojis
mail-builder changed how such filename is encoded in Content-Disposition between versions 0.5.0 and 1.0.0.
We can parse both encodings already and the test was not failing, but it is worth adding this corner case to the tests.

mail-builder 0.5.0 encoded the filename like this:

Content-Disposition: attachment;
	filename="=?utf-8?Q?very_long_filename_with_emoji_=F0=9F=98=80_very_long_file_name_with_emoji_=F0=9F=AB=A0_very_long_file_name_with_emoji.tar.gz?="

mail-builder 1.0.0 encodes the filename like this:

Content-Disposition: attachment;
 filename*0*=UTF-8''very%20long%20filename%20with%20emoji%20%F0%9F%98%80%20;
 filename*1*=very%20long%20file%20name%20with%20emoji%20%F0%9F%AB%A0%20very;
 filename*2*=%20long%20file%20name%20with%20emoji.tar.gz
2026-10-09 12:49:55 +00:00
link2xt 40c0954e42 chore: update async-imap to 0.12.0
async-imap 0.12.0 uses imap-proto 0.17.0 which includes bugfixes
for parsing responses with unicode characters.

Also upgraded base64 to 0.23 since new async-imap depends on it anyway.
2026-10-08 22:19:32 +00:00
link2xt 3a79324728 refactor: stop excluding COPYUID responses from the logs
We do not move the messages out of the inbox anymore,
so should not get these responses anyway.
2026-10-08 22:19:32 +00:00
link2xt 6ab0cd513a test: do not ignore errors in add_pseudo_transport() 2026-10-08 17:09:55 +00:00
biørn 0bee156449 api! remove unused dc_chatlist_get_summary2() (#8812)
`dc_chatlist_get_summary2()` was introduced for desktop that time,
but then replaced by jsonrpc's `get_chat_list_item_by_id()`.

outside desktop,
the cffi function seems to be never used in the past nor is today.

@adbenitez @Amzd - can you confirm it is not used?

similar for `summaryPreviewImage` - that was used on desktop, and is now
unused member of jsonrpc's ChatListItemFetchResult. question here: i
assume that it is fine to remove the member, and that desktop (maybe
after rebuilding) is fine with that.

@WofWca @nicodh - can you confirm, desktop is fine with the removal of
the member?

for general cleanup,
in an anyway complex area, it makes sense to remove these APIs.
2026-10-07 17:32:42 +02:00
link2xt 9ce20d7763 chore: update rPGP from 0.20.0 to 0.21.0
Main updates are security fix (fixed panic on short session keys)
and stabilized PQC (feature renamed from "draft-pqc" to "pqc").
2026-10-07 14:26:09 +00:00
holger krekel d7376e32b2 refactor: reduce macro-generated lines by >70%, and drop tracing
Every `info!`, `warn!` and `error!` call expanded a complete
`tracing::event!` to mirror its message into `tracing` (#6919),
and that mirror made up most of core's macro output, now removed:
it's down from 219k to 54k lines using

    RUSTC_BOOTSTRAP=1 cargo rustc -p deltachat --lib \
        --profile check -- -Zmacro-stats

A warm build and `touch src/lib.rs` with rustc 1.99.0,

    CARGO_INCREMENTAL=0 RUSTC_WRAPPER= /usr/bin/time -v \
        cargo check -p deltachat

takes about a third less time and 0.37 GiB less peak memory,
`cargo build -p deltachat` about a tenth less time and 0.36 GiB less.
The release binary from `nix build .#deltachat-rpc-server-x86_64-linux`
gets 2.7% smaller.

If we want to use tracing events to integrate better with iroh-debugging,
for example when we move to iroh 1.X,
we could introduce some iroh-relevant tracing events in core.
2026-10-06 23:52:19 +02:00
holger krekel 6378533b6a ci: speedup lint job and the Rust test builds
Since #8350 (2026-09-09) deltachat-jsonrpc-bindings build-depends
on deltachat-jsonrpc, so clippy, nextest and the doctests,
which select the whole workspace, compiled core
and its whole dependency tree a second time for the host.
The lint job also checked every dependency twice, once per panic strategy,
and repeated clippy's checks in a separate all-features `cargo check`.

With a warm cache on 4 CPUs on my machine, the lint job drops
from 150 to 60 seconds and the tests build
from 100 to 75 seconds.
Peak memory falls for lint from
5.1 to 3.5 GiB and for tests
7.5 to 5.4 GiB.
2026-10-06 21:17:34 +02:00
link2xt 1451478911 feat: connect to the most recently successfully used SMTP transport first
When connecting to SMTP, transports are now tried
from the most recently successfully used transport to the least recently used.
Transports that were never used for sending
are tried in the order of increasing ID because init_transports()
tries to select the fastest transport for the first one.

This solves the problem of having to wait for timeout each time
if the first tried transport is down.
2026-10-06 16:54:09 +00:00
link2xt 8468f5b5dd ci: update Rust to 1.99.0 2026-10-03 16:49:16 +00:00
link2xt dbba555a63 chore: update astral-tokio-tar from 0.6.4 to 0.7.0 2026-10-03 16:46:40 +00:00
link2xt 8cedea1958 fix(deltachat-rpc-client): shutdown all threads after RPC server crash
RPC server subprocess may exit for various reasons,
e.g. because of panic or because of debug_assert! during testing,
or because the process is killed manually or by OOM killer.

We still want to shutdown all threads cleanly in this case
and not fail in Rpc.close(). Otherwise Python process cannot exit
as it waits forever for all threads to terminate.

Without the fix, when added test is run in pytest,
pytest prints "ERROR at teardown of test_shutdown" and gets stuck.
When pytest is manually terminated with ^C, the following error is printed:

```
Exception ignored while joining a thread in _thread._shutdown():
Traceback (most recent call last):
  File "/usr/lib/python3.14/threading.py", line 1583, in _shutdown
    _thread_shutdown()
KeyboardInterrupt:
```

_thread_shutdown() is the function waiting for all non-daemon threads to finish.
2026-10-03 16:46:12 +00:00
Hocuri 70730490b2 fix: In SMTP loop, reset transport_id and from when disconnecting (#8797)
Previously, when we disconnected from the current transport, we forgot
to reset the `transport_id` and `from`. I couldn't find any bug that is caused by
this today, but it was a lingering problem.
2026-10-02 19:04:18 +00:00
link2xt 9578c2c054 refactor: do not guess image format from file extension on I/O error
`ImageReader::with_guessed_format()` returns I/O errors, meaning the file is likely not readable.
If we wanted to check for the case when file format was not guessed,
the correct way would be to check if imgreader.format() returns `None`.
All the widely used formats (JPEG, PNG, GIF, WebP) are guessable from the first bytes,
so the code is not worth fixing as we cannot test it.
2026-10-02 15:51:08 +00:00
link2xt 9e2d668490 refactor(deltachat-repl): move all commands to cmdline.rs
Also replaced unnecessary `ExitResult` and `should_continue` indirection with a simple `break` on "quit"/"exit" command.
2026-10-02 15:49:31 +00:00
link2xt d18718d2b9 chore: move deltachat-time to dev dependencies
It is only used for tests.
2026-10-02 15:28:12 +00:00
holger krekel 77bb77c33e refactor: take securejoin addresses from transport list
part of the effort to get rid of primary address conceptions in core.
2026-10-02 15:19:10 +02:00
link2xt 1143317fd1 feat: add context to HTTP(S) proxy connection errors
We already have context for HTTP(S) "CONNECT" command failures,
"Failed to connect to SOCKS5 proxy" and "Failed to connect to Shadowsocks proxy",
but when TCP connection to HTTP(S) fails the error is bubbled up as is.

This resulted in confusing "All connection attempts failed: Connection to host:port failed: ..." error
when configuring a relay, without any indication that "host:port" is the proxy address.
2026-10-02 01:26:54 +00:00
dependabot[bot] 83211e440e chore(cargo): bump dirs from 6.0.0 to 7.0.0
Bumps dirs from 6.0.0 to 7.0.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 01:23:58 +00:00
dependabot[bot] 7cdfb234a3 chore(cargo): bump hyper-util from 0.1.20 to 0.1.21
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.20 to 0.1.21.
- [Release notes](https://github.com/hyperium/hyper-util/releases)
- [Changelog](https://github.com/hyperium/hyper-util/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper-util/compare/v0.1.20...v0.1.21)

---
updated-dependencies:
- dependency-name: hyper-util
  dependency-version: 0.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 01:21:56 +00:00
dependabot[bot] 2a9bc732bf chore(deps): bump taiki-e/install-action from 2.86.7 to 2.87.20
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.86.7 to 2.87.20.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/b6ff580856c41316412a0b9b60540fbc6f8c82cc...9983c65e42da123ff25d1f78505eb6de315aa172)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.20
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:52:51 +00:00
dependabot[bot] 90387fdbea chore(cargo): bump thiserror from 2.0.20 to 2.0.21
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.20 to 2.0.21.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.20...2.0.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:52:30 +00:00
dependabot[bot] 29006e47c5 chore(cargo): bump tokio-rustls from 0.26.4 to 0.26.5
Bumps [tokio-rustls](https://github.com/rustls/tokio-rustls) from 0.26.4 to 0.26.5.
- [Release notes](https://github.com/rustls/tokio-rustls/releases)
- [Commits](https://github.com/rustls/tokio-rustls/compare/v/0.26.4...v/0.26.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:52:14 +00:00
dependabot[bot] 79bf0c79a5 chore(cargo): bump hyper from 1.10.1 to 1.11.1
Bumps [hyper](https://github.com/hyperium/hyper) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.10.1...v1.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:51:48 +00:00
dependabot[bot] e5d1bf7504 chore(deps): bump cachix/install-nix-action from 31.11.0 to 31.11.1
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.11.0 to 31.11.1.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](https://github.com/cachix/install-nix-action/compare/630ae543ea3a38a9a4166f03376c02c50f408342...13d8dd58da0234aa297dedd986986ccb8e7f3e24)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:51:29 +00:00
dependabot[bot] 32d40cbb14 chore(cargo): bump uuid from 1.25.0 to 1.26.1
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.25.0 to 1.26.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.25.0...v1.26.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:51:10 +00:00
dependabot[bot] 4e83357371 chore(cargo): bump mailparse from 0.16.1 to 0.17.0
Bumps [mailparse](https://github.com/staktrace/mailparse) from 0.16.1 to 0.17.0.
- [Commits](https://github.com/staktrace/mailparse/compare/v0.16.1...v0.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:44:05 +00:00
dependabot[bot] 88deea5b18 chore(deps): bump zizmorcore/zizmor-action from 0.6.2 to 0.6.4
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.2 to 0.6.4.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/3dc1ecc9bcb9e94e9b2c709687979e1298497054...cc914d7f3750a2d13d75c7f184a1060aa0e9d482)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:43:27 +00:00
dependabot[bot] 8883e5ada0 chore(cargo): bump syn from 3.0.4 to 3.0.6
Bumps [syn](https://github.com/dtolnay/syn) from 3.0.4 to 3.0.6.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/3.0.4...3.0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:43:09 +00:00
dependabot[bot] ca2a40e210 chore(cargo): bump smallvec from 1.15.2 to 1.16.1
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.15.2 to 1.16.1.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.2...v1.16.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 00:42:23 +00:00
link2xt 1cd2957a4d chore: remove unused "sdp" dependency 2026-10-01 14:51:36 +00:00
link2xt 4d57b8bf32 feat: do not send unencrypted MDNs 2026-10-01 14:50:13 +00:00
link2xt e203ae9571 refactor: replace MimeFactory::from_mdn() with a standalone mimefactory::mdn()
This factors out all MDN-related code that was previously inside `MimeFactory`
into a `mimefactory::mdn` function that constructs `QueuedMail` directly,
similarly to `mimefactory::keyupdate_message`.

Removing MDN-related code from MimeFactory also means that MimeFactory only handles non-MDN messages now
and we don't have to `match` on `MimeFactory.loaded` everywhere to access `msg` and `chat`.

Unlike `MimeFactory::from_mdn`, `mimefactory::mdn` does not need to fill `MimeFactory.from_addr`.
This removes one `get_primary_self_addr()` call. `mimefactory::mdn` also does not need to fill
various fields that are never used for MDNs such as `past_members`.
2026-10-01 14:50:13 +00:00
link2xt a37899b365 refactor: factor add_headers_to_part() out of mimefactory::add_headers_to_protected_part() 2026-10-01 14:50:13 +00:00
link2xt 3173487c9f fix: add headers to unencrypted MDNs
We don't want to wrap MDNs in multipart/mixed,
but we still want to add headers such as Date
like for normal unencrypted messages.
2026-10-01 14:50:13 +00:00
link2xt aade549ccc refactor: get rid of dead sticker-related code in check_or_recode_to_size()
It looks like the function is only called for File and Image viewtypes,
so there is no way to get into the Sticker branch
that checks the corners for transparency.
2026-10-01 13:39:12 +00:00
link2xt 736979b451 feat: do not request MDNs for unencrypted messages 2026-09-29 16:02:09 +00:00
holger krekel 5a882b0aca fix: delete handled securejoin messages on all relays
A join request is sent to every relay of an inviter, but the answering
inviter device only deleted the copy it downloaded. Another device of the inviter
fetching a different relay later answered the request again, re-adding
members who had left or been removed in the meantime.
2026-09-29 14:51:29 +02:00
link2xt 1e36fb74be chore: remove some unwrap() calls 2026-09-28 15:40:37 +00:00
biørn ebf1c87483 feat: re-add "second device added" message if deleted soon after transfer (#8758)
this PR makes it easier to spot unwanted "add second device" actions, if
the unlocked device is left unattended and/or a system lock is not in
place (so cannot be checked directly before doing the "add second
device" action).

already today, the sending device gets the message "ℹ️ Profile
transferred to your second device", which, however, can be deleted.

with this PR, if the message is deleted soon after the transfer, it
first looks as if this was successful. however, the message is re-added
later, at the first housekeeping at least an hour after the transfer.
deleting the message after that is final, so users can still get rid of
it.

It is clear that this does not catch all eventualities, it is
best-effort. e.g. it is known that subsequent "add second device"
attempts remove checks of previous ones.
First line of defense is device locking and asking for secret explicitly
before adding a second device.

previous discussions about that at
https://github.com/chatmail/core/issues/4303
2026-09-28 14:49:58 +02:00
d2weber 172ebc08f9 fix: regenerate deltachat.pc if env changed (#8752)
Paths in deltachat.pc are passed via env variables to build.rs. With
this fix, the pc will be regenerated if these env variables change.

This came up when building via CMake: the paths depend on
CMAKE_INSTALL_PREFIX, if the CMAKE_INSTALL_PREFIX changes during a
reconfigure, deltachat.pc should have the corresponding paths adjusted.
2026-09-28 11:01:22 +02:00
missytake eeb9337a56 chore: add chatmail.cc (#8756) 2026-09-26 23:00:35 +02:00
holger krekel a1eb593d81 chore: add chatmail.au 2026-09-26 19:34:15 +02:00
link2xt 7e070efc28 chore: enable clippy::unnecessary_wraps 2026-09-25 11:10:23 +00:00
link2xt 5d3145d165 chore: fix clippy::string_lit_as_bytes suggestions 2026-09-25 11:10:23 +00:00
link2xt 1605b971ba chore: fix clippy::redundant_clone suggestions 2026-09-25 11:10:23 +00:00
Hocuriandl 22578ea4b6 fix: Correctly percent-encode addresses in securejoin invite codes (#8747)
This PR fixes how addresses in securejoin invite codes (i.e. QR codes
and invite links) are percent-encoded.

Before this PR,
- `@` in the addresses was percent-encoded. This makes the invite code
harder to read, and is not necessary; at least, every software we tested
correctly handled links that contain `@` (Signal, WhatsApp, Telegram,
Thunderbird, Delta Chat on Android, iOS, and Desktop)
- _But_, the first address in the `r=` parameter of a securejoin link
was not percent-encoded at all. This was a sneaky bug caused by using
the `reduce` function; I always find it hard to follow code that uses
`reduce` (and similar functions like `fold`), and apparently others have
the same problem since neither @j-g00da nor @link2xt noticed the problem
when implementing & reviewing the PR that introduced the bug.

With this PR:
- `@` is allowed in addresses in securejoin invite codes
- all addresses are percent-encoded
- `reduced` is not used anymore

---------

Co-authored-by: l <link2xt@testrun.org>
2026-09-24 20:58:11 +00:00
link2xt 076f83f320 chore: reduce noise created by key-contact migration
Stop logging the time migration takes
and don't log anything when migration runs on a fresh database
and only creates empty tables.

Without these changes every time profile is created,
the following info lines are logged:

    src/sql/migrations.rs:35: Starting key-contact transition.
    src/sql/migrations.rs:82: Not yet configured, no need to migrate key-contacts
    src/sql/migrations.rs:1942: key-contacts migration took 1.143721ms in total.
2026-09-24 16:25:22 +00:00
link2xt 8ab98019a7 feat(deltachat-repl): remove "reset" command
This is not a correct way to reset the database, it does not even clear the transports table
so account stays configured. If someone needs a fresh database, then REPL should be restarted
with a new path.
2026-09-23 12:52:17 +00:00
91 changed files with 2129 additions and 1378 deletions
+17 -14
View File
@@ -20,16 +20,21 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.98.1
RUST_VERSION: 1.99.0
# Minimum Supported Rust Version
MSRV: 1.89.0
PYTHON_VERSION: 3.15
jobs:
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
timeout-minutes: 60
env:
# Tests always unwind: match it so dependencies are only checked once.
CARGO_PROFILE_DEV_PANIC: unwind
steps:
- uses: actions/checkout@v7
with:
@@ -48,8 +53,6 @@ jobs:
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- name: Check with all features
run: cargo check --workspace --all-targets --all-features
- name: Check with only default features
run: cargo check --all-targets
@@ -132,19 +135,19 @@ jobs:
cache-bin: false
- name: Install nextest
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
uses: taiki-e/install-action@9983c65e42da123ff25d1f78505eb6de315aa172
with:
tool: nextest
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo nextest run --workspace --locked
run: cargo nextest run --workspace --exclude deltachat-jsonrpc-bindings --locked
- name: Doc-Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace --locked --doc
run: cargo test --workspace --exclude deltachat-jsonrpc-bindings --locked --doc
- name: Test cargo vendor
run: cargo vendor
@@ -267,11 +270,11 @@ jobs:
fail-fast: false
matrix:
include:
# Currently used Rust version.
# Currently used Python version.
- os: ubuntu-latest
python: 3.14
python: latest
- os: macos-latest
python: 3.14
python: latest
# PyPy tests
- os: ubuntu-latest
@@ -302,7 +305,7 @@ jobs:
- name: Install python
uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
- name: Install tox
run: pip install tox
@@ -323,11 +326,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.14
python: latest
- os: macos-latest
python: 3.14
python: latest
- os: windows-latest
python: 3.14
python: latest
# PyPy tests
- os: ubuntu-latest
@@ -350,7 +353,7 @@ jobs:
- name: Install python
uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
- name: Install tox
run: pip install tox
+7 -7
View File
@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
@@ -82,7 +82,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -106,7 +106,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
@@ -157,7 +157,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -181,7 +181,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
@@ -208,7 +208,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- run: nix fmt flake.nix nix/ -- --check
build:
@@ -85,7 +85,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -106,5 +106,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- run: nix build .#${{ matrix.installable }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -63,7 +63,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
+80
View File
@@ -1,5 +1,84 @@
# Changelog
## [2.63.0] - 2026-10-09
### API-Changes
- add `PinnedMessagesChanged` event.
- [**breaking**]: remove unused `dc_chatlist_get_summary2()` ([#8812](https://github.com/chatmail/core/pull/8812))
- new `dc_msg_get_summary_text()` ([#8813](https://github.com/chatmail/core/pull/8813)).
### Features / Changes
- connect to the most recently successfully used SMTP transport first.
- re-add "second device added" message if deleted soon after transfer ([#8758](https://github.com/chatmail/core/pull/8758)).
- do not request MDNs for unencrypted messages.
- do not send unencrypted MDNs.
- add context to HTTP(S) proxy connection errors.
- deltachat-repl: remove "reset" command.
### Fixes
- Correctly percent-encode addresses in securejoin invite codes ([#8747](https://github.com/chatmail/core/pull/8747)).
- regenerate deltachat.pc if env changed ([#8752](https://github.com/chatmail/core/pull/8752)).
- delete handled securejoin messages on all relays.
- add headers to unencrypted MDNs.
- In SMTP loop, reset `transport_id` and `from` when disconnecting ([#8797](https://github.com/chatmail/core/pull/8797)).
- deltachat-rpc-client: shutdown all threads after RPC server crash.
- (linux-only) make sure large attachments return memory to kernel.
- do not reencode animated WebPs into JPEG.
### CI
- update Rust to 1.99.0.
- speedup lint job and the Rust test builds
### Refactor
- [**breaking**] stop saving legacy config values when configuring a transport.
- core does not set deprecated config values `addr`, `mail_pw` etc. Use `list_transports()` API instead.
- get rid of dead sticker-related code in check_or_recode_to_size().
- factor add_headers_to_part() out of mimefactory::add_headers_to_protected_part().
- replace MimeFactory::from_mdn() with a standalone mimefactory::mdn().
- take securejoin addresses from transport list.
- deltachat-repl: move all commands to cmdline.rs.
- do not guess image format from file extension on I/O error.
- reduce macro-generated lines by >70%, and drop tracing.
- stop excluding `COPYUID` responses from the logs.
### Tests
- move pinned messages tests into pinned_messages_tests module.
- Add basic tests for rendering unencrypted messages ([#8736](https://github.com/chatmail/core/pull/8736)).
- do not ignore errors in add_pseudo_transport().
- test sending and receiving long filename with emojis.
### Miscellaneous Tasks
- update rPGP from 0.20.0 to 0.21.0.
- reduce noise created by key-contact migration.
- enable `clippy::unnecessary_wraps`, fix `clippy::redundant_clone` and `clippy::string_lit_as_bytes` suggestions.
- add chatmail.au.
- add chatmail.cc ([#8756](https://github.com/chatmail/core/pull/8756)).
- remove some `unwrap()` calls.
- remove unused "sdp" dependency.
- cargo: bump smallvec from 1.15.2 to 1.16.1.
- cargo: bump syn from 3.0.4 to 3.0.6.
- deps: bump zizmorcore/zizmor-action from 0.6.2 to 0.6.4.
- cargo: bump mailparse from 0.16.1 to 0.17.0.
- cargo: bump uuid from 1.25.0 to 1.26.1.
- deps: bump cachix/install-nix-action from 31.11.0 to 31.11.1.
- cargo: bump hyper from 1.10.1 to 1.11.1.
- cargo: bump tokio-rustls from 0.26.4 to 0.26.5.
- cargo: bump thiserror from 2.0.20 to 2.0.21.
- deps: bump taiki-e/install-action from 2.86.7 to 2.87.20.
- cargo: bump hyper-util from 0.1.20 to 0.1.21.
- cargo: bump dirs from 6.0.0 to 7.0.0.
- move deltachat-time to dev dependencies.
- update astral-tokio-tar from 0.6.4 to 0.7.0.
- update async-imap to 0.12.0.
- update mail-builder to 1.0.
## [2.62.0] - 2026-09-22
### API-Changes
@@ -8903,3 +8982,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0
[2.61.0]: https://github.com/chatmail/core/compare/v2.60.0..v2.61.0
[2.62.0]: https://github.com/chatmail/core/compare/v2.61.0..v2.62.0
[2.63.0]: https://github.com/chatmail/core/compare/v2.62.0..v2.63.0
Generated
+154 -179
View File
@@ -194,9 +194,9 @@ dependencies = [
[[package]]
name = "astral-tokio-tar"
version = "0.6.4"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463"
checksum = "6f2e989b33246fe9240d39accf4dd9a01e0b6c1f3ce9dd095e0a47fa02505523"
dependencies = [
"futures-core",
"libc",
@@ -205,6 +205,7 @@ dependencies = [
"rustix 1.1.4",
"tokio",
"tokio-stream",
"zerocopy 0.8.59",
]
[[package]]
@@ -271,19 +272,19 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.11.3"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
checksum = "f97b87216c9f0ccc63c516263169085fa34bba25633f6d96c6a9b9ca1fd5f70a"
dependencies = [
"async-channel 2.5.0",
"async-compression",
"base64",
"base64 0.23.1",
"bytes",
"chrono",
"futures",
"futures-util",
"imap-proto",
"log",
"nom 7.1.3",
"nom 8.0.0",
"pin-project",
"pin-utils",
"self_cell",
@@ -310,7 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79"
dependencies = [
"native-tls",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"url",
]
@@ -322,12 +323,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55219982f938e74491ba85dc4e49cefe8096b1e8f49348c67180a7d244988dca"
dependencies = [
"anyhow",
"base64",
"base64 0.22.1",
"futures",
"log",
"nom 8.0.0",
"pin-project",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
]
@@ -363,7 +364,7 @@ dependencies = [
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tokio-util",
]
@@ -435,6 +436,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -459,7 +466,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"thiserror 2.0.20",
"thiserror 2.0.21",
]
[[package]]
@@ -832,7 +839,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
dependencies = [
"base64",
"base64 0.22.1",
"encoding_rs",
]
@@ -1327,7 +1334,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1337,7 +1344,7 @@ dependencies = [
"async-native-tls",
"async-smtp",
"async_zip",
"base64",
"base64 0.23.1",
"blake3",
"brotli",
"bytes",
@@ -1386,7 +1393,6 @@ dependencies = [
"regex",
"rusqlite",
"sanitize-filename",
"sdp",
"serde",
"serde_json",
"sha-1",
@@ -1399,14 +1405,13 @@ dependencies = [
"tempfile",
"testdir",
"textwrap",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tokio-io-timeout",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"toml",
"tracing",
"url",
"uuid",
"walkdir",
@@ -1435,11 +1440,11 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
"base64",
"base64 0.23.1",
"deltachat",
"deltachat-contact-tools",
"futures",
@@ -1456,14 +1461,14 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc-bindings"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"deltachat-jsonrpc",
]
[[package]]
name = "deltachat-repl"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,12 +1484,13 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"deltachat",
"deltachat-jsonrpc",
"futures-lite",
"libc",
"log",
"serde",
"serde_json",
@@ -1503,12 +1509,12 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 3.0.4",
"syn 3.0.6",
]
[[package]]
name = "deltachat_ffi"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1518,7 +1524,7 @@ dependencies = [
"num-traits",
"rand 0.9.4",
"serde_json",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"yerpc",
]
@@ -1677,9 +1683,9 @@ dependencies = [
[[package]]
name = "dirs"
version = "6.0.0"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
dependencies = [
"dirs-sys",
]
@@ -2091,12 +2097,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -2228,7 +2228,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.6",
]
[[package]]
@@ -2417,34 +2417,16 @@ checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash 0.2.0",
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.12.2"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.17.1",
"hashbrown",
]
[[package]]
@@ -2489,7 +2471,7 @@ dependencies = [
"once_cell",
"rand 0.9.4",
"ring",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tinyvec",
"tokio",
"tracing",
@@ -2512,7 +2494,7 @@ dependencies = [
"rand 0.9.4",
"resolv-conf",
"smallvec",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tracing",
]
@@ -2678,9 +2660,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
@@ -2718,15 +2700,16 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.20"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body",
"httparse",
"hyper",
"libc",
"pin-project-lite",
@@ -2964,11 +2947,11 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.16.5"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856"
checksum = "5ccf963d57074747b455398a1763d174da80bcaba6f51e3671a82252b531a68b"
dependencies = [
"nom 7.1.3",
"nom 8.0.0",
]
[[package]]
@@ -2978,7 +2961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
"hashbrown",
]
[[package]]
@@ -3069,7 +3052,7 @@ dependencies = [
"strum 0.26.2",
"stun-rs",
"surge-ping",
"thiserror 2.0.20",
"thiserror 2.0.21",
"time",
"tokio",
"tokio-stream",
@@ -3094,7 +3077,7 @@ dependencies = [
"ed25519-dalek",
"rand_core 0.6.4",
"serde",
"thiserror 2.0.20",
"thiserror 2.0.21",
"url",
]
@@ -3136,7 +3119,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serde-error",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tokio-util",
"tracing",
@@ -3181,7 +3164,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.9",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tracing",
"web-time",
@@ -3201,7 +3184,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tinyvec",
"tracing",
"web-time",
@@ -3256,7 +3239,7 @@ dependencies = [
"sha1",
"strum 0.26.2",
"stun-rs",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -3285,12 +3268,11 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.105"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -3369,11 +3351,12 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.38.2"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
dependencies = [
"cc",
"openssl-sys",
"pkg-config",
"vcpkg",
]
@@ -3445,7 +3428,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.4",
"hashbrown",
]
[[package]]
@@ -3468,15 +3451,15 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mail-builder"
version = "0.5.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151"
[[package]]
name = "mailparse"
version = "0.16.1"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f"
checksum = "8469955699e572455f8e1a40689e07ab309911d659632f65062598192254c091"
dependencies = [
"charset",
"data-encoding",
@@ -3741,7 +3724,7 @@ dependencies = [
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.20",
"thiserror 2.0.21",
]
[[package]]
@@ -4204,7 +4187,7 @@ version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64",
"base64 0.22.1",
"serde",
]
@@ -4230,7 +4213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.20",
"thiserror 2.0.21",
"ucd-trie",
]
@@ -4270,16 +4253,16 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.20.0"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1"
checksum = "ae70f4d9325a391db30d115d6191a7bb67cec856aa0bba131290f0c4e09a532a"
dependencies = [
"aead",
"aes",
"aes-gcm",
"aes-kw",
"argon2",
"base64",
"base64 0.22.1",
"bitfields",
"block-padding",
"blowfish",
@@ -4406,7 +4389,7 @@ dependencies = [
"serde",
"sha1_smol",
"simple-dns",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tracing",
"url",
@@ -4558,7 +4541,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"derive_more 1.0.0",
"futures-lite",
@@ -4619,7 +4602,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -4804,7 +4787,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.9",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tracing",
]
@@ -4825,7 +4808,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tinyvec",
"tracing",
"web-time",
@@ -5064,7 +5047,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.20",
"thiserror 2.0.21",
]
[[package]]
@@ -5114,7 +5097,7 @@ version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
@@ -5234,21 +5217,11 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rsqlite-vfs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
dependencies = [
"hashbrown 0.16.1",
"thiserror 2.0.20",
]
[[package]]
name = "rusqlite"
version = "0.40.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
"bitflags 2.11.0",
"fallible-iterator",
@@ -5256,7 +5229,6 @@ dependencies = [
"hashlink",
"libsqlite3-sys",
"smallvec",
"sqlite-wasm-rs",
]
[[package]]
@@ -5479,18 +5451,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdp"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22c3b0257608d7de4de4c4ea650ccc2e6e3e45e3cd80039fcdee768bcb449253"
dependencies = [
"rand 0.9.4",
"substring",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "sec1"
version = "0.7.3"
@@ -5596,7 +5556,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.6",
]
[[package]]
@@ -5731,7 +5691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f"
dependencies = [
"aes",
"base64",
"base64 0.22.1",
"blake3",
"byte_string",
"bytes",
@@ -5752,7 +5712,7 @@ dependencies = [
"shadowsocks-crypto",
"socket2 0.5.9",
"spin 0.10.1",
"thiserror 2.0.20",
"thiserror 2.0.21",
"tokio",
"tokio-tfo",
"trait-variant",
@@ -5859,14 +5819,14 @@ dependencies = [
"sha3",
"signature",
"typenum",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
name = "smallvec"
version = "1.15.2"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
[[package]]
name = "smawk"
@@ -5961,18 +5921,6 @@ dependencies = [
"der",
]
[[package]]
name = "sqlite-wasm-rs"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
dependencies = [
"cc",
"js-sys",
"rsqlite-vfs",
"wasm-bindgen",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -6043,7 +5991,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac"
dependencies = [
"base64",
"base64 0.22.1",
"bounded-integer",
"byteorder",
"crc",
@@ -6061,15 +6009,6 @@ dependencies = [
"rand 0.9.4",
]
[[package]]
name = "substring"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
dependencies = [
"autocfg",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -6116,9 +6055,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.4"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -6262,11 +6201,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.20"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl 2.0.20",
"thiserror-impl 2.0.21",
]
[[package]]
@@ -6282,13 +6221,13 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.20"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.6",
]
[[package]]
@@ -6408,9 +6347,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
version = "0.26.4"
version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
dependencies = [
"rustls",
"tokio",
@@ -6467,7 +6406,7 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-core",
"futures-sink",
@@ -6810,9 +6749,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.25.0"
version = "1.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -6880,32 +6819,48 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.128"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.118",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.78"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.128"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -6913,22 +6868,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.128"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 2.0.118",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.128"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
@@ -6948,9 +6903,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.105"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -7460,7 +7415,7 @@ dependencies = [
"futures",
"log",
"serde",
"thiserror 2.0.20",
"thiserror 2.0.21",
"windows 0.59.0",
"windows-core 0.59.0",
]
@@ -7634,7 +7589,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
dependencies = [
"zerocopy-derive 0.8.59",
]
[[package]]
@@ -7648,6 +7612,17 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
+11 -13
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.63.0-dev"
version = "2.64.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.89"
@@ -36,7 +36,6 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { workspace = true }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
@@ -44,7 +43,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.12.0", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -70,7 +69,7 @@ iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.5.0", default-features = false }
mail-builder = { version = "1.0.0", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
@@ -78,16 +77,15 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.20.0", features = ["draft-pqc"], default-features = false }
pgp = { version = "0.21.0", features = ["pqc"], default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.41", features = ["escape-html"] }
rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["backup"] }
rusqlite = { workspace = true, features = ["sqlcipher"] }
sanitize-filename = { workspace = true }
sdp = "0.17.1"
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
@@ -102,11 +100,10 @@ thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6.3", default-features = false }
astral-tokio-tar = { version = "0.7.0", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
walkdir = "2.5.0"
@@ -115,6 +112,7 @@ webpki-roots = "0.26.8"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.8.1", features = ["async_tokio"] }
deltachat-time = { path = "./deltachat-time" }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -180,7 +178,7 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.5.0"
base64 = "0.22"
base64 = { version = "0.23", default-features = false, features = ["std"] }
chrono = { version = "0.4.44", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
@@ -189,12 +187,12 @@ futures = "0.3.32"
futures-lite = "2.6.1"
libc = "0.2"
log = "0.4"
mailparse = "0.16.1"
mailparse = "0.17.0"
nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.9"
regex = "1.12"
rusqlite = "0.40.2"
rusqlite = "0.37"
sanitize-filename = "0.6"
serde = "1.0"
serde_json = "1"
@@ -209,7 +207,7 @@ yerpc = "0.7"
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"async-native-tls/vendored"
]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.63.0-dev"
version = "2.64.0-dev"
description = "Deltachat FFI"
edition = "2024"
license = "MPL-2.0"
+3 -3
View File
@@ -21,9 +21,9 @@ fn main() {
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
version = env::var("CARGO_PKG_VERSION").unwrap(),
libs_priv = libs_priv,
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
+44 -22
View File
@@ -304,6 +304,21 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Changes the passphrase on the open database.
* Deprecated 2025-11, see `dc_context_open()` for reasoning.
*
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
* It is impossible to encrypt unencrypted database with this method and vice versa.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The new passphrase.
* @return 1 on success, 0 on error.
*/
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
/**
* Returns 1 if database is open.
*
@@ -3528,28 +3543,6 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, size_t index, dc_chat_t* chat);
/**
* Create a chatlist summary item when the chatlist object is already unref()'d.
*
* This function is similar to dc_chatlist_get_summary(), however,
* it takes the chat ID and the message ID as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
* as arguments. The chatlist object itself is not needed directly.
*
* This maybe useful if you convert the complete object into a different representation
* as done e.g. in the node-bindings.
* If you have access to the chatlist object in some way, using this function is not recommended,
* use dc_chatlist_get_summary() in this case instead.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat ID to get a summary for.
* @param msg_id The message ID to get a summary for.
* @return The summary as an dc_lot_t object, see dc_chatlist_get_summary() for details.
* Must be freed using dc_lot_unref(). NULL is never returned.
*/
dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t chat_id, uint32_t msg_id);
/**
* Get info summary for a chat, in JSON format.
*
@@ -4298,6 +4291,7 @@ dc_lot_t* dc_msg_get_summary (const dc_msg_t* msg, const dc_cha
* notifications.
*
* @memberof dc_msg_t
* @deprecated 2026-10-08, use dc_msg_get_summary_text(msg, 1, 1, approx_chars) instead
* @param msg The message object.
* @param approx_characters A rough length of the expected string.
* @return A summary for the given messages.
@@ -4307,6 +4301,34 @@ dc_lot_t* dc_msg_get_summary (const dc_msg_t* msg, const dc_cha
char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_characters);
/**
* Get a message summary.
* Used for notifications, pinned messages and for copying texts to clipboard.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param add_forwarded 1=add a hint if a message was forwarded.
* 0=do not add the hint;
* this is allowed only if the message is shown without a username, e.g. for pinned messages.
* @param add_type_emoji 1=add the type of the message as an emoji prefix.
* 0=do not add type emoji;
* this is allowed only if the message type is visible otherwise,
* close to where the emoji would be displayed otherwise
* (eg. images in pinned messages preceding summary)
* @param approx_chars >0=convert linebreaks to spaces and truncate the line at about the given number of characters;
* for single-line UI elements such as pinned messages or notifications;
* additionally, UIs will most times clip the text to the available space.
* 0=leave linebreaks as is and return the full text, useful e.g. for copying to clipboard.
* @return A summary for the given message.
* The returned string must be released using dc_str_unref().
* Returns an empty string on errors, never returns NULL.
*/
char* dc_msg_get_summary_text (const dc_msg_t* msg, int add_forwarded, int add_type_emoji, int approx_chars);
/**
* Get the name that should be shown over the message (in a group chat) instead of the contact
* display name, or NULL.
+49 -37
View File
@@ -12,7 +12,6 @@
extern crate human_panic;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::mem::ManuallyDrop;
@@ -47,7 +46,6 @@ mod dc_array;
mod lot;
mod string;
use deltachat::chatlist::Chatlist;
use self::string::*;
@@ -160,6 +158,24 @@ pub unsafe extern "C" fn dc_context_open(
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_context_change_passphrase(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_change_passphrase()");
return 0;
}
let ctx = unsafe { &*context };
let passphrase = to_string_lossy(passphrase);
block_on(ctx.change_passphrase(passphrase))
.context("dc_context_change_passphrase() failed")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -2826,34 +2842,6 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
Box::into_raw(Box::new(summary.into()))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_chatlist_get_summary2(
context: *mut dc_context_t,
chat_id: u32,
msg_id: u32,
) -> *mut dc_lot_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_chatlist_get_summary2()");
return ptr::null_mut();
}
let ctx = unsafe { &*context };
let msg_id = if msg_id == 0 {
None
} else {
Some(MsgId::new(msg_id))
};
let summary = block_on(Chatlist::get_summary2(
ctx,
ChatId::new(chat_id),
msg_id,
None,
))
.context("get_summary2 failed")
.log_err(ctx)
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
// dc_chat_t
/// FFI struct for [dc_chat_t]
@@ -3484,6 +3472,7 @@ pub unsafe extern "C" fn dc_msg_get_summary(
Box::into_raw(Box::new(summary.into()))
}
// deprecated, use dc_msg_get_summary_text instead
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_msg_get_summarytext(
msg: *mut dc_msg_t,
@@ -3495,14 +3484,37 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
}
let ffi_msg = unsafe { &mut *msg };
let summary = block_on(ffi_msg.message.get_summary(&ffi_msg.context, None))
.context("dc_msg_get_summarytext failed")
.log_err(&ffi_msg.context)
.unwrap_or_default();
match usize::try_from(approx_characters) {
Ok(chars) => summary.truncated_text(chars).strdup(),
Err(_) => summary.text.strdup(),
let add_forwarded = true;
let add_type_emoji = true;
block_on(ffi_msg.message.get_summary_text_ext(
&ffi_msg.context,
add_forwarded,
add_type_emoji,
usize::try_from(approx_characters).unwrap_or_default(),
))
.strdup()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_msg_get_summary_text(
msg: *mut dc_msg_t,
add_forwarded: libc::c_int,
add_type_emoji: libc::c_int,
approx_chars: libc::c_int,
) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_summary_text()");
return "".strdup();
}
let ffi_msg = unsafe { &mut *msg };
block_on(ffi_msg.message.get_summary_text_ext(
&ffi_msg.context,
add_forwarded != 0,
add_type_emoji != 0,
usize::try_from(approx_chars).unwrap_or(0),
))
.strdup()
}
#[unsafe(no_mangle)]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc-bindings"
version = "2.63.0-dev"
version = "2.64.0-dev"
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
edition = "2024"
license = "MPL-2.0"
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.63.0-dev"
"version": "2.64.0-dev"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.63.0-dev"
version = "2.64.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2024"
license = "MPL-2.0"
@@ -31,8 +31,6 @@ pub enum ChatListItemFetchResult {
summary_text1: String,
summary_text2: String,
summary_status: u32,
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
@@ -103,8 +101,6 @@ pub(crate) async fn get_chat_list_item_by_id(
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
let summary_preview_image = summary.thumbnail_path;
let visibility = chat.get_visibility();
let avatar_path = chat
@@ -157,7 +153,6 @@ pub(crate) async fn get_chat_list_item_by_id(
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
+1 -1
View File
@@ -12,7 +12,7 @@ pub struct JsonrpcReaction {
emoji: String,
/// Emoji frequency.
count: u32,
count: usize,
/// True if we reacted with this emoji.
is_from_self: bool,
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.63.0-dev"
version = "2.64.0-dev"
license = "MPL-2.0"
edition = "2024"
repository = "https://github.com/chatmail/core"
@@ -8,7 +8,7 @@ repository = "https://github.com/chatmail/core"
[dependencies]
anyhow = { workspace = true }
deltachat = { workspace = true, features = ["internals"]}
dirs = "6"
dirs = "7"
log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
+49 -60
View File
@@ -20,62 +20,13 @@ use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::qr_code_generator::{create_qr_svg, get_securejoin_qr_svg};
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
use tokio::fs;
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
if 0 != bits & 4 {
context
.sql()
.execute("DELETE FROM keypairs;", ())
.await
.unwrap();
println!("(4) Private keypairs reset.");
}
if 0 != bits & 8 {
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", ())
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", ())
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", ())
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", ())
.await
.unwrap();
context
.sql()
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
(),
)
.await
.unwrap();
context.sql().config_cache().write().await.clear();
println!("(8) Rest but server config reset.");
}
context.emit_msgs_changed_without_ids();
}
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
@@ -304,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
export-keys\n\
import-keys <key-file>\n\
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
============================================="
),
@@ -396,9 +346,57 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
============================================="
),
},
"connect" => {
context.start_io().await;
}
"disconnect" => {
context.stop_io().await;
}
"fetch" => {
context.background_fetch().await?;
}
"configure" => {
context.configure().await?;
}
"has-backup" => {
has_backup(&context, blobdir).await?;
}
"clear" => {
println!("\n\n\n");
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
context.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = deltachat::securejoin::get_securejoin_qr(&context, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
qr2term::print_qr(qr.as_str())?;
}
}
"getqrsvg" => {
context.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&context, group).await {
Ok(svg) => {
fs::write(&file, svg).await?;
println!("QR code svg written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get QR code svg: {err}");
}
}
}
"joinqr" => {
context.start_io().await;
if !arg0.is_empty() {
deltachat::securejoin::join_securejoin(&context, arg1).await?;
}
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(
@@ -444,15 +442,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
"reset" => {
ensure!(
!arg1.is_empty(),
"Argument <bits> missing: 4=private keys, 8=rest but server config"
);
let bits: i32 = arg1.parse()?;
ensure!(bits < 16, "<bits> must be lower than 16.");
reset_tables(&context, bits).await;
}
"stop" => {
context.stop_ongoing().await;
}
+9 -91
View File
@@ -13,8 +13,6 @@ use anyhow::{Error, bail};
use deltachat::EventType;
use deltachat::chat::ChatId;
use deltachat::context::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
@@ -25,7 +23,6 @@ use rustyline::validate::Validator;
use rustyline::{
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
};
use tokio::fs;
use tokio::runtime::Handle;
use tracing_subscriber::EnvFilter;
@@ -147,7 +144,7 @@ impl Completer for DcHelper {
}
}
const IMEX_COMMANDS: [&str; 10] = [
const IMEX_COMMANDS: [&str; 9] = [
"has-backup",
"export-backup",
"import-backup",
@@ -156,7 +153,6 @@ const IMEX_COMMANDS: [&str; 10] = [
"export-keys",
"import-keys",
"poke",
"reset",
"stop",
];
@@ -353,23 +349,16 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str())?;
let should_continue = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
println!("Exiting ...");
false
}
Err(err) => {
eprintln!("Error: {err:#}");
true
}
}
});
if !should_continue {
let line = line.trim();
if matches!(line, "exit" | "quit") {
println!("Exiting ...");
break;
}
if let Err(err) =
Handle::current().block_on(cmdline(ctx.clone(), line, &mut selected_chat))
{
eprintln!("Error: {err:#}");
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
@@ -393,77 +382,6 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(())
}
#[derive(Debug)]
enum ExitResult {
Continue,
Exit,
}
async fn handle_cmd(
line: &str,
ctx: Context,
selected_chat: &mut ChatId,
) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' ');
let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default();
match arg0 {
"connect" => {
ctx.start_io().await;
}
"disconnect" => {
ctx.stop_io().await;
}
"fetch" => {
ctx.background_fetch().await?;
}
"configure" => {
ctx.configure().await?;
}
"clear" => {
println!("\n\n\n");
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
qr2term::print_qr(qr.as_str())?;
}
}
"getqrsvg" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {
fs::write(&file, svg).await?;
println!("QR code svg written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get QR code svg: {err}");
}
}
}
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
join_securejoin(&ctx, arg1).await?;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),
_ => cmdline(ctx.clone(), line, selected_chat).await?,
}
Ok(ExitResult::Continue)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.63.0-dev"
version = "2.64.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
@@ -149,7 +149,10 @@ class Rpc:
def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
self.stop_io_for_all_accounts()
# JSON-RPC error may happen if RPC server process has crashed already.
# We still want to shutdown all threads in this case.
with contextlib.suppress(JsonRpcError):
self.stop_io_for_all_accounts()
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
# otherwise it might exit through an "RPC server closed" error instead.
self.events_thread.join()
+33
View File
@@ -0,0 +1,33 @@
import os
import sys
import pytest
def anonymous_mib(pid):
with open(f"/proc/{pid}/smaps_rollup") as f:
for line in f:
if line.startswith("Anonymous:"):
return int(line.split()[1]) // 1024
raise LookupError("Anonymous")
@pytest.mark.skipif(sys.platform != "linux", reason="reads /proc")
def test_attachment_memory_is_returned(acf, rpc, tmp_path):
# See also comments for `tune_malloc` in `deltachat-rpc-server/src/main.rs`
ac1, ac2 = acf.get_online_accounts(2)
chat1 = acf.get_accepted_chat(ac1, ac2)
chat2 = ac2.create_chat(ac1)
blob = tmp_path / "blob.bin"
blob.write_bytes(os.urandom(20 << 20))
before = anonymous_mib(rpc.process.pid)
for sender_chat, receiver in ((chat1, ac2), (chat2, ac1)):
sender_chat.send_file(str(blob))
event = receiver.wait_for_incoming_msg_event()
assert receiver.get_message_by_id(event.msg_id).get_snapshot().file_bytes == 20 << 20
for ac in (ac1, ac2):
rpc.wait_for_all_work_done(ac.id)
grown = anonymous_mib(rpc.process.pid) - before
assert grown < 64, f"the server kept {grown} MiB after two 20 MiB attachments"
@@ -351,12 +351,11 @@ def test_qr_works_after_removing_primary_transport(acf, log) -> None:
log.section("Alice creates a QR code")
chat_qr = alice.get_qr_code()
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
assert f"&a={first_addr}" in chat_qr_unquoted
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
assert f"&a={third_addr}" in chat_qr_unquoted
assert f"&r={second_addr},{first_addr}" in chat_qr_unquoted
log.section("Alice removes first and second transport")
alice.set_config("configured_addr", third_addr)
alice.delete_transport(first_addr)
log.section("Alice removes the transport named by the a= parameter")
alice.delete_transport(third_addr)
alice.delete_transport(second_addr)
log.section("Bob scans the QR code, which still works")
@@ -1,4 +1,5 @@
import logging
import time
import pytest
@@ -90,6 +91,30 @@ def test_qr_securejoin(acf):
fiona.wait_for_securejoin_joiner_success()
def test_qr_securejoin_request_deleted_on_all_transports(acf):
alice, bob = acf.get_online_accounts(2)
alice.add_transport_from_qr(acf.get_account_qr())
alice.bring_online()
alice_chat = alice.create_group("Group")
qr_code = alice_chat.get_qr_code()
alice2 = alice.clone()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
alice_chat.remove_contact(bob)
# Stop and start io in order to wait until the remove message is sent out.
alice.stop_io()
alice.bring_online()
# Membership timestamps have a resolution of one second.
time.sleep(1)
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
# Otherwise, alice2 would execute securejoin again and re-add Bob.
alice2.bring_online()
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acf, all_devices_online):
alice, bob, fiona = acf.get_online_accounts(3)
@@ -35,6 +35,19 @@ def test_sleep(rpc) -> None:
assert sleep_5_future in pending
def test_shutdown(rpc) -> None:
"""Test RPC client shutdown if RPC server process is terminated.
This is a regression test, at the time of adding it RPC client
did not terminate all threads and python process did not exit.
Shutting down all threads is tested implicitly by pytest-timeout.
"""
rpc.process.kill()
rpc.process.wait()
with pytest.raises(JsonRpcError):
rpc.get_system_info()
def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.63.0-dev"
version = "2.64.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2024"
license = "MPL-2.0"
@@ -14,6 +14,7 @@ deltachat = { workspace = true }
anyhow = { workspace = true }
futures-lite = { workspace = true }
libc = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.63.0-dev"
"version": "2.64.0-dev"
}
+24
View File
@@ -22,8 +22,32 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
/// Pins Linux glibc's mmap threshold so that freed message buffers go back to the kernel.
///
/// See M_MMAP_THRESHOLD in <https://man7.org/linux/man-pages/man3/mallopt.3.html>:
/// glibc by default starts with a M_MMAP_THRESHOLD threshold of 128 KiB
/// but raises it to the size of every freed block that exceeds it,
/// up to 32 MiB on 64-bit systems,
/// and trims the heap only from its top end once twice that much is free.
/// Fixating the threshold disables the adjustment: allocations at or above it
/// that the free list cannot satisfy are mmapped and unmapped on free,
/// at the price of the kernel zeroing each such buffer after unmap.
/// Large message processing (allocations above 128KiB) very slightly slows
/// down to the kernel zeroing the buffers, but it's hardly measurable,
/// while overall process memory allocation behaviour significantly improves.
#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn tune_malloc() {
unsafe {
libc::mallopt(libc::M_MMAP_THRESHOLD, 128 * 1024);
}
}
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
fn tune_malloc() {}
#[tokio::main(flavor = "multi_thread")]
async fn main() {
tune_malloc();
// Logs from `log` crate and traces from `tracing` crate
// are configurable with `RUST_LOG` environment variable
// and go to stderr to avoid interfering with JSON-RPC using stdout.
+3 -3
View File
@@ -63,17 +63,15 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "base64", version = "0.22.1" },
{ name = "bitflags", version = "1.3.2" },
{ name = "constant_time_eq", version = "0.3.1" },
{ name = "cpufeatures", version = "0.2.17" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "foldhash", version = "0.1.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "getrandom", version = "0.3.3" },
{ name = "hashbrown", version = "0.15.4" },
{ name = "hashbrown", version = "0.16.1" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "hybrid-array", version = "0.2.3" },
@@ -116,6 +114,8 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "zerocopy-derive", version = "0.7.35" },
{ name = "zerocopy", version = "0.7.35" },
]
+15
View File
@@ -456,6 +456,21 @@ CREATE TABLE smtp_status_updates (
descr TEXT NOT NULL -- text to send along with the updates
);
-- Table to record the successful usage transports for sending.
-- Sorting the table by rowid in descending order
-- returns most recently successfully used transport first.
CREATE TABLE smtp_success (
-- Sequentially increasing ID of the success.
-- Transport with the highest ID is to be used first.
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-- ID of the transport that was used to send a message.
transport_id INTEGER UNIQUE NOT NULL,
-- Delete `smtp_success` rows when the transport is deleted.
FOREIGN KEY(transport_id) REFERENCES transports(id) ON DELETE CASCADE
) STRICT;
-- Table of "sync items" to be grouped into sync messages
-- and sent to own devices.
CREATE TABLE multi_device_sync (
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.63.0-dev"
version = "2.64.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
+1 -1
View File
@@ -188,7 +188,7 @@ class Message:
def get_summarytext(self, width: int) -> str:
"""Get a message summary as a single line of text. Typically used for notifications."""
return from_dc_charpointer(lib.dc_msg_get_summarytext(self._dc_msg, width))
return from_dc_charpointer(lib.dc_msg_get_summary_text(self._dc_msg, 1, 1, width))
@props.with_doc
def time_sent(self):
+1 -1
View File
@@ -1 +1 @@
2026-09-22
2026-10-09
+1 -1
View File
@@ -6,4 +6,4 @@
#
# To automatically fix warnings, run
# scripts/clippy.sh --fix --allow-dirty
cargo clippy --locked --workspace --all-targets --all-features "$@" -- -D warnings
cargo clippy --locked --workspace --exclude deltachat-jsonrpc-bindings --all-targets --all-features "$@" -- -D warnings
+55 -24
View File
@@ -76,12 +76,8 @@ impl Accounts {
Accounts::open(events, dir, writable).await
}
/// Get the ID used to log events.
///
/// Account manager logs events with ID 0
/// which is not used by any accounts.
fn get_id(&self) -> u32 {
0
fn log_info(&self, file: &str, line: u32, msg: String) {
self.emit_event(EventType::Info(format!("{file}:{line}: {msg}")));
}
/// Ensures the accounts directory and config file exist.
@@ -169,7 +165,9 @@ impl Accounts {
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
ctx.open().await?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
@@ -393,11 +391,6 @@ impl Accounts {
"Starting background fetch for {n_accounts} accounts."
)),
});
::tracing::event!(
::tracing::Level::INFO,
account_id = 0,
"Starting background fetch for {n_accounts} accounts."
);
let mut set = JoinSet::new();
for account in accounts {
set.spawn(async move {
@@ -413,11 +406,6 @@ impl Accounts {
"Finished background fetch for {n_accounts} accounts."
)),
});
::tracing::event!(
::tracing::Level::INFO,
account_id = 0,
"Finished background fetch for {n_accounts} accounts."
);
}
/// Auxiliary function for [Accounts::background_fetch].
@@ -460,11 +448,6 @@ impl Accounts {
id: 0,
typ: EventType::Warning("Background fetch timed out.".to_string()),
});
::tracing::event!(
::tracing::Level::WARN,
account_id = 0,
"Background fetch timed out."
);
}
events.emit(Event {
id: 0,
@@ -547,7 +530,7 @@ impl Accounts {
}
}
/// Emits a single event.
/// Emits a single event with ID 0, which is not used by any accounts.
pub fn emit_event(&self, event: EventType) {
self.events.emit(Event { id: 0, typ: event })
}
@@ -819,7 +802,9 @@ impl Config {
.build()
.await
.with_context(|| format!("failed to create context from file {dbfile:?}"))?;
ctx.open().await?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
accounts.insert(account_config.id, ctx);
}
@@ -1268,6 +1253,52 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypted_account() -> Result<()> {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create accounts manager")?;
assert_eq!(accounts.accounts.len(), 0);
let account_id = accounts
.add_closed_account()
.await
.context("failed to add closed account")?;
let account = accounts
.get_selected_account()
.context("failed to get account")?;
assert_eq!(account.id, account_id);
let passphrase_set_success = account
.open("foobar".to_string())
.await
.context("failed to set passphrase")?;
assert!(passphrase_set_success);
drop(accounts);
let writable = false;
let accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create second accounts manager")?;
let account = accounts
.get_selected_account()
.context("failed to get account")?;
assert_eq!(account.is_open().await, false);
// Try wrong passphrase.
assert_eq!(account.open("barfoo".to_string()).await?, false);
assert_eq!(account.open("".to_string()).await?, false);
assert_eq!(account.open("foobar".to_string()).await?, true);
assert_eq!(account.is_open().await, true);
Ok(())
}
/// Tests that accounts share stock string translations.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accounts_share_translations() -> Result<()> {
+3 -1
View File
@@ -34,6 +34,8 @@ const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
"chat.nuvon.app",
"chat.tinydispatch.org",
"chat.vim.wtf",
"chatmail.au",
"chatmail.cc",
"chatmail.uk",
"chtml.ca",
"deltachat.me",
@@ -163,7 +165,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
for _ in 0..NUM_TRANSPORTS_TARGET {
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
context
.set_config_internal(Config::AutorelayFinished, config::from_bool(true))
.set_config_internal(Config::AutorelayFinished, Some(config::from_bool(true)))
.await?;
return Ok(relay_added);
+26 -31
View File
@@ -164,9 +164,9 @@ impl<'a> BlobObject<'a> {
/// you want to create a [BlobObject] for a filename read from the
/// database.
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
let name = match name.starts_with("$BLOBDIR/") {
true => name.splitn(2, '/').last().unwrap(),
false => name,
let name = match name.strip_prefix("$BLOBDIR/") {
Some(name) => name,
None => name,
};
if !BlobObject::is_acceptible_blob_name(name) {
return Err(format_err!("not an acceptable blob name: {name}"));
@@ -289,6 +289,7 @@ impl<'a> BlobObject<'a> {
name: Option<String>,
viewtype: &mut Viewtype,
) -> Result<String> {
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
let (max_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
@@ -326,6 +327,7 @@ impl<'a> BlobObject<'a> {
max_bytes: usize,
is_avatar: bool,
) -> Result<String> {
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = is_avatar;
let mut no_exif = false;
@@ -340,45 +342,38 @@ impl<'a> BlobObject<'a> {
// It's strange that BufReader modifies a file position while it takes a non-mut
// reference. Ok, just rewind it.
file.rewind()?;
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
let imgreader = match imgreader {
Ok(ir) => ir,
_ => {
file.rewind()?;
ImageReader::with_format(
std::io::BufReader::new(&file),
ImageFormat::from_path(self.to_abs_path())?,
)
}
};
let imgreader = ImageReader::new(std::io::BufReader::new(&file))
.with_guessed_format()
.context("Failed to guess image format due to I/O error")?;
let fmt = imgreader.format().context("Unknown format")?;
if *vt == Viewtype::File {
*vt = Viewtype::Image;
return Ok(name);
}
let mut img = imgreader.decode().context("image decode failure")?;
let mut img = match fmt {
image::ImageFormat::WebP => {
// `with_guessed_format()` restores file position,
// so `buf_reader` is at the beginning of the file.
let buf_reader = imgreader.into_inner();
let webp_decoder = image::codecs::webp::WebPDecoder::new(buf_reader)
.context("Failed to create WebP decoder")?;
// If WebP has animation, do not try to recode it.
// Recoding into JPEG will result in losing the animation.
if !is_avatar && webp_decoder.has_animation() {
return Ok(name);
}
DynamicImage::from_decoder(webp_decoder)?
}
_ => imgreader.decode().context("Failed to decode image")?,
};
let orientation = exif
.as_ref()
.map(|exif| exif_orientation(exif, context))
.unwrap_or(Orientation::NoTransforms);
let mut encoded = Vec::new();
if *vt == Viewtype::Sticker {
let x_max = img.width().saturating_sub(1);
let y_max = img.height().saturating_sub(1);
if !img.in_bounds(x_max, y_max)
|| !(img.get_pixel(0, 0).0[3] == 0
|| img.get_pixel(x_max, 0).0[3] == 0
|| img.get_pixel(0, y_max).0[3] == 0
|| img.get_pixel(x_max, y_max).0[3] == 0)
{
*vt = Viewtype::Image;
} else {
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
// from UIs shouldn't contain sensitive Exif info.
return Ok(name);
}
}
img.apply_orientation(orientation);
// max_wh is the maximum image width and height, i.e. the resolution-limit,
+67 -1
View File
@@ -4,7 +4,9 @@ use super::*;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::sql;
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
use crate::test_utils::{
self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
};
use crate::tools::SystemTime;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
@@ -738,6 +740,70 @@ async fn test_send_gif_as_sticker() -> Result<()> {
Ok(())
}
/// Tests that animated WebP is sent without reencoding.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_animated_webp_as_image() -> Result<()> {
let bytes = include_bytes!("../../test-data/image/animated.webp");
assert!(
bytes.len() > constants::WORSE_IMAGE_SIZE as usize,
"Animated WebP should be large enough to trigger reencoding"
);
let (width, height) = (900u32, 900u32);
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config(
Config::MediaQuality,
Some(&(MediaQuality::Worse as i32).to_string()),
)
.await?;
let file = alice.get_blobdir().join("file").with_extension("webp");
fs::write(&file, &bytes)
.await
.context("Failed to write file")?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_and_deduplicate(alice, &file, Some("file.webp"), None)?;
let chat = alice.create_chat(bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, width);
assert_eq!(bob_msg.get_height() as u32, height);
assert_eq!(
bob_msg.get_filebytes(bob).await?.unwrap(),
bytes.len() as u64
);
Ok(())
}
/// Tests that if user sets animated WebP as an avatar, it may be recoded.
///
/// We don't want to recode animated WebPs into JPEG and lose animation,
/// but for avatars we don't want animation and transparency anyway.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_animated_webp_avatar() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let avatar_src = t.dir.path().join("avatar.webp");
let avatar_bytes = include_bytes!("../../test-data/image/animated.webp");
fs::write(&avatar_src, avatar_bytes).await.unwrap();
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await?;
let avatar_blob = t.get_config(Config::Selfavatar).await?.unwrap();
assert!(avatar_blob.ends_with(".jpg"));
let scaled_avatar_size = fs::metadata(&avatar_blob).await.unwrap().len();
assert!(
scaled_avatar_size < avatar_bytes.len() as u64,
"Animated WebP avatar must be recoded"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_and_deduplicate() -> Result<()> {
let t = TestContext::new().await;
+16 -13
View File
@@ -3,7 +3,6 @@
use std::cmp;
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::io::Cursor;
use std::marker::Sync;
use std::path::{Path, PathBuf};
use std::time::Duration;
@@ -1918,8 +1917,7 @@ impl Chat {
let new_mime_headers: Option<String> = new_mime_headers.map(|s| {
let html_part = MimePart::new("text/html", s);
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
html_part.write_part(cursor).ok();
html_part.write_part(&mut buffer);
String::from_utf8_lossy(&buffer).to_string()
});
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
@@ -2924,9 +2922,7 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
);
}
if let Some(ref side_effects) = side_effects {
msg.subject.clone_from(&side_effects.subject);
}
msg.subject.clone_from(&side_effects.subject);
if is_encrypted {
msg.param.set_int(Param::GuaranteeE2ee, 1);
} else {
@@ -2968,13 +2964,13 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
now,
msg.id,
&queued_pre_msg,
pre_side_effects.as_ref(),
Some(&pre_side_effects),
)
.context("Failed to enqueue pre-message")?;
row_ids.push(row_id)
}
row_ids.push(
enqueue_mail(transaction, now, msg.id, &queued_msg, side_effects.as_ref())
enqueue_mail(transaction, now, msg.id, &queued_msg, Some(&side_effects))
.context("Failed to enqueue message")?,
);
Ok(row_ids)
@@ -3639,7 +3635,7 @@ pub(crate) async fn create_out_broadcast_ext(
)?;
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
let mut params: Params = Params::new();
params.update_timestamp(Param::GroupNameTimestamp, time())?;
params.update_timestamp(Param::GroupNameTimestamp, time());
t.execute(
"INSERT INTO chats
@@ -3985,7 +3981,7 @@ ORDER BY timestamp DESC, id DESC -- final ORDER BY is needed as UNION does not g
(
chat_id,
Viewtype::Webxdc,
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER as u32,
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER,
ContactId::INFO,
),
|row: &rusqlite::Row| Ok(row.get::<_, MsgId>(0)?),
@@ -4795,12 +4791,14 @@ pub(crate) async fn get_chat_id_by_grpid(
///
/// Optional `label` can be provided to ensure that message is added only once.
/// If `important` is true, a notification will be sent.
/// `timestamp_sent` is the time shown on the message; it does not affect ordering.
#[expect(clippy::arithmetic_side_effects)]
pub async fn add_device_msg_with_importance(
context: &Context,
label: Option<&str>,
msg: Option<&mut Message>,
important: bool,
timestamp_sent: i64,
) -> Result<MsgId> {
ensure!(
label.is_some() || msg.is_some(),
@@ -4820,11 +4818,10 @@ pub async fn add_device_msg_with_importance(
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid();
let timestamp_sent = time();
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
msg.timestamp_sort = timestamp_sent;
msg.timestamp_sort = time();
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
&& msg.timestamp_sort <= last_msg_time
{
@@ -4892,7 +4889,7 @@ pub async fn add_device_msg(
label: Option<&str>,
msg: Option<&mut Message>,
) -> Result<MsgId> {
add_device_msg_with_importance(context, label, msg, false).await
add_device_msg_with_importance(context, label, msg, false, time()).await
}
/// Returns true if device message with a given label was ever added to the device chat.
@@ -4920,6 +4917,12 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
.await?;
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
context
.set_config_internal(Config::BackupTransferMsgId, None)
.await?;
context
.set_config_internal(Config::BackupTransferTimestamp, None)
.await?;
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
context
+14 -3
View File
@@ -336,6 +336,17 @@ pub enum Config {
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
/// ID of the device message added after backup transfer.
///
/// If the message does not exist on the first housekeeping
/// after `READD_BACKUP_TRANSFER_MSG_DELAY`, it is re-added.
BackupTransferMsgId,
/// Timestamp of the device message added after backup transfer.
///
/// Needed to check against `READD_BACKUP_TRANSFER_MSG_DELAY`.
BackupTransferTimestamp,
/// Timestamp of the last time accumulated broadcast channel reactions were sent
LastReactionsBroadcast,
@@ -809,7 +820,7 @@ impl Context {
/// Set the given config to a boolean value.
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, from_bool(value)).await?;
self.set_config(key, Some(from_bool(value))).await?;
Ok(())
}
@@ -830,8 +841,8 @@ impl Context {
}
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
Some(if val { "1" } else { "0" })
pub(crate) fn from_bool(val: bool) -> &'static str {
if val { "1" } else { "0" }
}
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
+1 -1
View File
@@ -29,7 +29,7 @@ impl ServerParams {
if self.username.is_empty() {
vec![Self {
username: addr.to_string(),
..self.clone()
..self
}]
} else {
vec![self]
+52 -6
View File
@@ -67,6 +67,7 @@ pub struct ContextBuilder {
id: u32,
events: Events,
stock_strings: StockStrings,
password: Option<String>,
push_subscriber: Option<PushSubscriber>,
}
@@ -83,6 +84,7 @@ impl ContextBuilder {
id: rand::random(),
events: Events::new(),
stock_strings: StockStrings::new(),
password: None,
push_subscriber: None,
}
}
@@ -129,6 +131,19 @@ impl ContextBuilder {
self
}
/// Sets the password to unlock the database.
/// Deprecated 2025-11:
/// - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
/// - Isolation from other apps is needed anyway.
///
/// If an encrypted database is used it must be opened with a password. Setting a
/// password on a new database will enable encryption.
#[deprecated(since = "TBD")]
pub fn with_password(mut self, password: String) -> Self {
self.password = Some(password);
self
}
/// Sets push subscriber.
pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
self.push_subscriber = Some(push_subscriber);
@@ -153,10 +168,11 @@ impl ContextBuilder {
///
/// Returns error if context cannot be opened.
pub async fn open(self) -> Result<Context> {
let password = self.password.clone().unwrap_or_default();
let context = self.build().await?;
match context.open().await? {
match context.open(password).await? {
true => Ok(context),
false => bail!("FIXME database could not be decrypted, incorrect or missing password"),
false => bail!("database could not be decrypted, incorrect or missing password"),
}
}
}
@@ -370,7 +386,10 @@ impl Context {
let context =
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
context.sql.open(&context).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
context.sql.open(&context, "".to_string()).await?;
}
Ok(context)
}
@@ -414,9 +433,20 @@ impl Context {
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
/// errors.
#[deprecated(since = "TBD")]
pub async fn open(&self) -> Result<bool> {
self.sql.open(self).await?;
Ok(true)
pub async fn open(&self, passphrase: String) -> Result<bool> {
if self.sql.check_passphrase(passphrase.clone()).await? {
self.sql.open(self, passphrase).await?;
Ok(true)
} else {
Ok(false)
}
}
/// Changes encrypted database passphrase.
/// Deprecated 2025-11, see [`ContextBuilder::with_password()`] for reasoning.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(passphrase).await?;
Ok(())
}
/// Returns true if database is open.
@@ -424,6 +454,15 @@ impl Context {
self.sql.is_open().await
}
/// Tests the database passphrase.
///
/// Returns true if passphrase is correct.
///
/// Fails if database is already open.
pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
self.sql.check_passphrase(passphrase).await
}
pub(crate) fn with_blobdir(
dbfile: PathBuf,
blobdir: PathBuf,
@@ -811,6 +850,13 @@ impl Context {
res.insert("number_of_contacts", contacts.to_string());
res.insert("database_dir", self.get_dbfile().display().to_string());
res.insert("database_version", dbversion.to_string());
res.insert(
"database_encrypted",
self.sql
.is_encrypted()
.await
.map_or_else(|| "closed".to_string(), |b| b.to_string()),
);
res.insert("journal_mode", journal_mode);
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert(
+61
View File
@@ -1,4 +1,6 @@
use anyhow::Context as _;
use strum::IntoEnumIterator;
use tempfile::tempdir;
use super::*;
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
@@ -292,6 +294,8 @@ async fn test_get_info_completeness() {
// too sensitive or summarized in another item.
let skip_from_get_info = vec![
"addr",
"backup_transfer_msg_id",
"backup_transfer_timestamp",
"displayname",
"imap_certificate_checks",
"mail_server",
@@ -484,6 +488,63 @@ async fn test_limit_search_msgs() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_check_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let context = ContextBuilder::new(dbfile.clone())
.with_id(1)
.build()
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
drop(context);
let context = ContextBuilder::new(dbfile)
.with_id(2)
.build()
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
assert_eq!(context.open("false".to_string()).await?, false);
assert_eq!(context.open("foo".to_string()).await?, true);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_context_change_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let context = ContextBuilder::new(dbfile)
.with_id(1)
.build()
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
context
.set_config(Config::Addr, Some("alice@example.org"))
.await?;
context
.change_passphrase("bar".to_string())
.await
.context("Failed to change passphrase")?;
assert_eq!(
context.get_config(Config::Addr).await?.unwrap(),
"alice@example.org"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;
+14 -24
View File
@@ -1454,10 +1454,9 @@ impl Session {
/// or flags have been changed.
/// In this case we may want to skip next IDLE and do a round
/// of fetching new messages and synchronizing seen flags.
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
fn drain_unsolicited_responses(&self, context: &Context) -> bool {
use UnsolicitedResponse::*;
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
let folder = self.selected_folder.as_deref().unwrap_or_default();
let mut should_refetch = false;
@@ -1472,34 +1471,25 @@ impl Session {
}
Expunge(_) | Recent(_) => {}
Other(ref response_data) => {
match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them.
Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
} => {}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
Other(ref response_data) => match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
},
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
Ok(should_refetch)
should_refetch
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ impl Session {
self.select_with_uidvalidity(context, folder).await?;
if self.drain_unsolicited_responses(context)? {
if self.drain_unsolicited_responses(context) {
self.new_mail = true;
}
+25 -16
View File
@@ -30,6 +30,7 @@ use crate::tools::{
mod transfer;
use ::pgp::types::KeyDetails;
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
pub use transfer::{BackupProvider, get_backup};
// Name of the database file in the backup.
@@ -200,9 +201,6 @@ async fn import_backup(
backup_to_import: &Path,
passphrase: String,
) -> Result<()> {
if !passphrase.is_empty() {
bail!("Encrypted passphrase is not supported");
}
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
info!(
@@ -213,7 +211,7 @@ async fn import_backup(
context.get_dbfile().display()
);
import_backup_stream(context, backup_file, file_size).await?;
import_backup_stream(context, backup_file, file_size, passphrase).await?;
Ok(())
}
@@ -234,6 +232,7 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> Result<()> {
ensure!(
!context.is_configured().await?,
@@ -244,7 +243,7 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
"Cannot import backup, IO is running"
);
import_backup_stream_inner(context, backup_file, file_size)
import_backup_stream_inner(context, backup_file, file_size, passphrase)
.await
.0
}
@@ -317,6 +316,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> (Result<()>,) {
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
let mut archive = Archive::new(backup_file);
@@ -363,7 +363,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
if res.is_ok() {
res = context
.sql
.import(&unpacked_database)
.import(&unpacked_database, passphrase.clone())
.await
.context("cannot import unpacked database");
}
@@ -391,7 +391,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
}
context
.sql
.open(context)
.open(context, "".to_string())
.await
.log_err(context)
.ok();
@@ -736,7 +736,7 @@ where
/// overwritten.
///
/// This also verifies that IO is not running during the export.
async fn export_database(context: &Context, dest: &Path, _passphrase: String) -> Result<()> {
async fn export_database(context: &Context, dest: &Path, passphrase: String) -> Result<()> {
ensure!(
!context.scheduler.is_running().await,
"cannot export backup, IO is running"
@@ -746,7 +746,6 @@ async fn export_database(context: &Context, dest: &Path, _passphrase: String) ->
let dest = dest
.to_str()
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
let mut dest_conn = rusqlite::Connection::open(dest)?;
context.set_config(Config::BccSelf, Some("1")).await?;
context
@@ -757,12 +756,22 @@ async fn export_database(context: &Context, dest: &Path, _passphrase: String) ->
context
.sql
.call_write(|conn| {
if let Err(err) = conn.execute("VACUUM", ()) {
warn!(context, "Vacuum failed, exporting anyway: {err:#}.");
}
let backup = rusqlite::backup::Backup::new(conn, &mut dest_conn)?;
backup.run_to_completion(5, std::time::Duration::ZERO, None)?;
conn.execute("VACUUM;", ())
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
.ok();
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
.context("failed to export to attached backup database");
conn.execute(
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
[],
)
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;
Ok(())
})
.await
@@ -1032,7 +1041,7 @@ mod tests {
ar.unpack(&unpack_dir).await?;
let sql = sql::Sql::new(unpack_dir.path().join(DBFILE_BACKUP_NAME));
sql.open(&context2).await?;
sql.open(&context2, "".to_string()).await?;
assert_eq!(
sql.get_raw_config_int("backup_version").await?.unwrap(),
DCBACKUP_VERSION
+99 -8
View File
@@ -40,21 +40,68 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::EventType;
use crate::chat::add_device_msg;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::key;
use crate::log::warn;
use crate::message::Message;
use crate::message::{Message, MsgId};
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{TempPathGuard, create_id};
use crate::tools::{TempPathGuard, create_id, time};
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
/// ALPN protocol identifier for the backup transfer protocol.
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
/// Minimum time after a backup transfer before we check
/// whether the "second device added" device message still exists.
///
/// The check is done in the first housekeeping after this delay;
/// if the message was deleted until then, it is re-added with the original timestamp.
/// After the check, the user can delete the message as usual without it being re-added.
///
/// This makes it easier to spot unwanted "add second device" actions.
/// It is clear that this does not catch all eventualities, it is best-effort.
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
/// Checks `Config::BackupTransferMsgId`
/// and re-adds the backup transfer device message if it does not exist and some time passed.
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
let Some(msg_id) = context
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
.await?
else {
return Ok(());
};
let timestamp = context
.get_config_i64(Config::BackupTransferTimestamp)
.await?;
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
return Ok(());
}
context
.set_config_internal(Config::BackupTransferMsgId, None)
.await?;
context
.set_config_internal(Config::BackupTransferTimestamp, None)
.await?;
if Message::load_from_db_optional(context, MsgId::new(msg_id))
.await?
.is_none()
{
let mut msg = Message::new_text(backup_transfer_msg_body(context));
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
}
Ok(())
}
/// Provide or send a backup of this device.
///
/// This creates a backup of the current device and starts a service which offers another
@@ -212,8 +259,23 @@ impl BackupProvider {
info!(context, "Received backup reception acknowledgement.");
context.emit_event(EventType::ImexProgress(1000));
let timestamp = time();
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
add_device_msg(&context, None, Some(&mut msg)).await?;
let msg_id =
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
.await?;
context
.set_config_internal(
Config::BackupTransferTimestamp,
Some(&timestamp.to_string()),
)
.await?;
context
.set_config_internal(
Config::BackupTransferMsgId,
Some(&msg_id.to_u32().to_string()),
)
.await?;
Ok(())
}
@@ -324,6 +386,7 @@ pub async fn get_backup2(
info!(context, "Sending backup authentication token.");
send_stream.write_all(auth_token.as_bytes()).await?;
let passphrase = String::new();
info!(context, "Starting to read backup from the stream.");
let mut file_size_buf = [0u8; 8];
@@ -333,7 +396,7 @@ pub async fn get_backup2(
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
context.emit_event(EventType::ImexProgress(1));
import_backup_stream(context, recv_stream, file_size)
import_backup_stream(context, recv_stream, file_size, passphrase)
.await
.context("Failed to import backup from QUIC stream")?;
info!(context, "Finished importing backup from the stream.");
@@ -392,14 +455,17 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
mod tests {
use std::time::Duration;
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
use crate::message::Viewtype;
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
use crate::contact::ContactId;
use crate::message::{Viewtype, delete_msgs};
use crate::sql::housekeeping;
use crate::test_utils::TestContextManager;
use crate::tools::SystemTime;
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_receive() {
async fn test_send_receive() -> Result<()> {
let mut tcm = TestContextManager::new();
// Create first device.
@@ -471,6 +537,31 @@ mod tests {
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
.await;
}
// When deleting the backup transfer message on the sending device,
// it is re-added once on housekeeping,
// but not within the first hour after the transfer.
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
delete_msgs(&ctx0, &[original_msg.id]).await?;
housekeeping(&ctx0).await?;
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
SystemTime::shift(Duration::from_secs(60 * 60));
housekeeping(&ctx0).await?;
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
assert_ne!(readded_msg.id, original_msg.id);
assert_eq!(readded_msg.get_text(), original_msg.get_text());
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
delete_msgs(&ctx0, &[readded_msg.id]).await?;
housekeeping(&ctx0).await?;
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
assert!(device_msgs.is_empty()); // re-adding is done only once
Ok(())
}
/// Tests that trying to accidentally overwrite a profile
+2 -1
View File
@@ -98,7 +98,8 @@ pub trait DcKey: Serialize + Deserializable + Clone {
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf).unwrap();
self.to_writer(&mut buf)
.expect("Writing to Vec<u8> cannot fail");
buf
}
+2 -2
View File
@@ -52,7 +52,7 @@ const KEYUPDATE_CHUNK_CONTACTS: usize = 200;
const KEYUPDATE_MAX_SILENCE: i64 = 3 * 365 * 24 * 3600;
/// Upper bound on the contacts informed after a relay list change, keeping the freshest.
const KEYUPDATE_MAX_RECIPIENTS: u32 = 5000;
const KEYUPDATE_MAX_RECIPIENTS: usize = 5000;
/// A contact to inform: the relays to reach them at, and the key to encrypt to.
struct KeyupdateRecipient {
@@ -63,7 +63,7 @@ struct KeyupdateRecipient {
/// Returns at most `max_recipients` key-contacts to inform.
async fn keyupdate_recipients(
context: &Context,
max_recipients: u32,
max_recipients: usize,
) -> Result<Vec<KeyupdateRecipient>> {
// Single chat contacts only become keyupdate recipient candidates
// if we have a record of a sent message or `last_seen` is not 0.
+2 -1
View File
@@ -15,7 +15,8 @@
clippy::explicit_iter_loop,
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied,
clippy::manual_is_variant_and
clippy::manual_is_variant_and,
clippy::unnecessary_wraps
)]
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
+23 -30
View File
@@ -3,6 +3,7 @@
#![allow(missing_docs)]
use crate::context::Context;
use crate::events::EventType;
mod stream;
@@ -12,15 +13,9 @@ macro_rules! info {
($ctx:expr, $msg:expr) => {
info!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let full = format!("{file}:{line}: {msg}",
file = file!(),
line = line!(),
msg = &formatted);
::tracing::event!(::tracing::Level::INFO, account_id = $ctx.get_id(), "{}", &formatted);
$ctx.emit_event($crate::EventType::Info(full));
}};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
$ctx.log_info(file!(), line!(), format!($msg, $($args),*))
};
}
// Workaround for <https://github.com/rust-lang/rust/issues/133708>.
@@ -30,15 +25,9 @@ mod warn_macro_mod {
($ctx:expr, $msg:expr) => {
warn_macro!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let full = format!("{file}:{line}: {msg}",
file = file!(),
line = line!(),
msg = &formatted);
::tracing::event!(::tracing::Level::WARN, account_id = $ctx.get_id(), "{}", &formatted);
$ctx.emit_event($crate::EventType::Warning(full));
}};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
$ctx.log_warn(file!(), line!(), format!($msg, $($args),*))
};
}
pub(crate) use warn_macro;
@@ -50,15 +39,25 @@ macro_rules! error {
($ctx:expr, $msg:expr) => {
error!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
::tracing::event!(::tracing::Level::ERROR, account_id = $ctx.get_id(), "{}", &formatted);
$ctx.set_last_error(&formatted);
$ctx.emit_event($crate::EventType::Error(formatted));
}};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
$ctx.log_error(format!($msg, $($args),*))
};
}
impl Context {
pub(crate) fn log_info(&self, file: &str, line: u32, msg: String) {
self.emit_event(EventType::Info(format!("{file}:{line}: {msg}")));
}
pub(crate) fn log_warn(&self, file: &str, line: u32, msg: String) {
self.emit_event(EventType::Warning(format!("{file}:{line}: {msg}")));
}
pub(crate) fn log_error(&self, msg: String) {
self.set_last_error(&msg);
self.emit_event(EventType::Error(msg));
}
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
@@ -116,12 +115,6 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
);
// We can't use the warn!() macro here as the file!() and line!() macros
// don't work with #[track_caller]
tracing::event!(
::tracing::Level::WARN,
account_id = context.get_id(),
"{}",
&full
);
context.emit_event(crate::EventType::Warning(full));
};
self
-5
View File
@@ -93,11 +93,6 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
this.metrics.total_read, this.metrics.total_written
);
tracing::event!(
::tracing::Level::WARN,
account_id = *this.account_id,
log_message
);
this.events.emit(Event {
id: *this.account_id,
typ: EventType::Warning(log_message),
+13 -8
View File
@@ -580,7 +580,7 @@ impl Message {
if let Some(msg) = &mut msg {
msg.additional_text =
Self::get_additional_text(context, msg.download_state, &msg.param)?;
Self::get_additional_text(context, msg.download_state, &msg.param);
}
Ok(msg)
@@ -618,7 +618,7 @@ impl Message {
context: &Context,
download_state: DownloadState,
param: &Params,
) -> Result<String> {
) -> String {
if download_state != DownloadState::Done {
let file_size = param
.get(Param::PostMessageFileBytes)
@@ -635,14 +635,14 @@ impl Message {
.unwrap_or("?".to_owned());
return match viewtype {
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
Viewtype::File => format!(" [{file_name} – {file_size}]"),
_ => {
let translated_viewtype = viewtype.to_locale_string(context);
Ok(format!(" [{translated_viewtype} – {file_size}]"))
format!(" [{translated_viewtype} – {file_size}]")
}
};
}
Ok(String::new())
String::new()
}
/// Returns the MIME type of an attached file if it exists.
@@ -2017,14 +2017,19 @@ pub(crate) async fn set_msg_failed(
/// Inserts a tombstone into `msgs` table
/// to prevent downloading the same message in the future.
/// With `on_server`, copies seen on IMAP later are deleted on the server.
///
/// Returns tombstone database row ID.
pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
pub(crate) async fn insert_tombstone(
context: &Context,
rfc724_mid: &str,
on_server: bool,
) -> Result<MsgId> {
let row_id = context
.sql
.insert(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, ChatId::TRASH),
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
(rfc724_mid, ChatId::TRASH, on_server),
)
.await?;
let msg_id = MsgId::new(u32::try_from(row_id)?);
+1 -3
View File
@@ -784,7 +784,7 @@ async fn test_get_existing_msg_ids() -> Result<()> {
}
#[test]
fn test_can_fail() -> Result<()> {
fn test_can_fail() {
use MessageState::*;
// states that are not allowed to transition to OutFailed
@@ -799,6 +799,4 @@ fn test_can_fail() -> Result<()> {
assert!(OutPending.can_fail());
assert!(OutDelivered.can_fail());
assert!(OutFailed.can_fail());
Ok(())
}
+347 -428
View File
File diff suppressed because it is too large Load Diff
+81 -62
View File
@@ -5,7 +5,7 @@ use pgp::armor;
use pgp::packet::{Packet, PacketParser};
use pretty_assertions::assert_eq;
use regex::regex;
use std::io::BufReader;
use std::io::{BufReader, Cursor};
use std::str;
use std::time::Duration;
@@ -30,8 +30,7 @@ fn render_email_address(display_name: &str, addr: &str) -> String {
let mut output = Vec::<u8>::new();
new_address_with_name(display_name, addr.to_string())
.unwrap_address()
.write_header(&mut output, 0)
.unwrap();
.write_header(&mut output, 0);
String::from_utf8(output).unwrap()
}
@@ -50,9 +49,7 @@ fn test_render_email_address() {
let s = render_email_address(display_name, addr);
println!("{s}");
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>");
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>\r\n");
}
#[test]
@@ -70,14 +67,14 @@ fn test_render_email_address_noescape() {
let s = render_email_address(display_name, addr);
// Addresses should not be unnecessarily be encoded, see <https://github.com/deltachat/deltachat-core-rust/issues/1575>:
assert_eq!(s, r#""a space" <x@y.org>"#);
assert_eq!(s, "\"a space\" <x@y.org>\r\n");
}
#[test]
fn test_render_email_address_duplicated_as_name() {
let addr = "x@y.org";
let s = render_email_address(addr, addr);
assert_eq!(s, "<x@y.org>");
assert_eq!(s, "<x@y.org>\r\n");
}
#[test]
@@ -102,8 +99,7 @@ fn render_header_text(text: &str) -> String {
// Some non-zero length of the header name.
let bytes_written = 20;
mail_builder::headers::text::Text::new(text.to_string())
.write_header(&mut output, bytes_written)
.unwrap();
.write_header(&mut output, bytes_written);
String::from_utf8(output).unwrap()
}
@@ -281,8 +277,12 @@ async fn test_subject_mdn() {
assert_eq!("Re: Hello, Bob", mf.subject_str(t).await.unwrap());
}
/// Tests that MDN for unencrypted message can be created without throwing an error.
///
/// We do not send unencrypted MDNs, but do not want SMTP loop to get stuck
/// if we somehow request the creation of unencrypted MDN.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_create_encrypted() -> Result<()> {
async fn test_mdn_create_unencrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.allow_unencrypted().await?;
@@ -297,21 +297,43 @@ async fn test_mdn_create_encrypted() -> Result<()> {
.await?;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// MDN for unencrypted message is not encrypted.
// MDN for unencrypted message.
// Should not happen, but should also not throw an error.
let mut msg = Message::new(Viewtype::Text);
let chat_alice = alice.create_email_chat(&bob).await.id;
let sent = alice.send_msg(chat_alice, &mut msg).await;
let rcvd = bob.recv_msg(&sent).await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory =
MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
assert!(!mimefactory.will_be_encrypted());
let bob_addr = bob.get_primary_self_addr().await?;
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
// Only sending MDN to self (if BCC-self is enabled) because address-contact recipient has no key.
assert!(queued_mdn.recipients.is_empty());
// MDNs are always encrypted, even if requested for unencrypted message.
assert!(queued_mdn.encryption.is_encrypted());
bob.assert_warn("has no key, sending to self").await;
Ok(())
}
/// Tests that MDNs sent in reply to encrypted messages are encrypted
/// and MDNs for unencrypted messages are not created.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_create_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice
.set_config(Config::Displayname, Some("Alice Exampleorg"))
.await?;
let bob = tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
.await?;
bob.set_config(Config::Selfstatus, Some("Bob Examplenet"))
.await?;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
assert!(!rendered_msg.message.contains("Bob Examplenet"));
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
let bob_alice_contact = bob.add_or_lookup_contact(&alice).await;
assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg");
@@ -319,10 +341,9 @@ async fn test_mdn_create_encrypted() -> Result<()> {
let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
assert!(mimefactory.will_be_encrypted());
let bob_addr = bob.get_primary_self_addr().await?;
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
assert!(queued_mdn.encryption.is_encrypted());
let rendered_msg = render_queued_mail_with_context(queued_mdn, &bob).await?;
assert!(!rendered_msg.message.contains("Bob Examplenet"));
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
@@ -349,8 +370,8 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
)?;
import_public_key(alice, &bob_public_key).await?;
let mimefactory = MimeFactory::from_mdn(alice, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
let mut recipients = mimefactory.recipients();
let queued_mdn = mdn(alice, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
let mut recipients = queued_mdn.recipients;
recipients.sort();
assert_eq!(recipients, vec!["bob@example.net", "bob@relay2.example"]);
@@ -364,9 +385,8 @@ async fn test_mdn_autocrypt_throttle() -> Result<()> {
alice: &TestContext,
rcvd: &Message,
) -> Result<bool> {
let mf = MimeFactory::from_mdn(bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
let addr = bob.get_primary_self_addr().await?;
let rendered_msg = mf.render(bob, &addr).await?;
let queued_mdn = mdn(bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
let rendered_msg = render_queued_mail_with_context(queued_mdn, bob).await?;
let mime = MimeMessage::from_bytes(alice, rendered_msg.message.as_bytes()).await?;
Ok(mime.autocrypt_fingerprint.is_some())
}
@@ -647,8 +667,7 @@ async fn test_render_reply() {
let recipients = mimefactory.recipients();
assert_eq!(recipients, vec!["charlie@example.net"]);
let addr = t.get_primary_self_addr().await.unwrap();
let rendered_msg = mimefactory.render(t, &addr).await.unwrap();
let rendered_msg = mimefactory.render(t).await.unwrap();
let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap();
assert_eq!(
@@ -795,7 +814,7 @@ async fn test_protected_headers_directive() -> Result<()> {
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
// decoded_data to check presence of the necessary headers.
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
msg.set_file_from_bytes(&bob, "foo.bar", b"content", None)?;
let sent = bob.send_msg(chat, &mut msg).await;
assert!(msg.get_showpadlock());
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
@@ -932,7 +951,7 @@ async fn test_no_empty_to_header() -> Result<()> {
assert!(
// It would be equally fine if the payload contained `To: alice@example.org` or similar,
// as long as it's a valid header
payload.contains("To: \"hidden-recipients\": ;"),
payload.contains("To: \"hidden-recipients\":;"),
"Payload doesn't contain correct To: header: {payload}"
);
@@ -1041,8 +1060,8 @@ MIME-Version: 1.0
To: "hidden-recipients": ;
Subject: [...]
Chat-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="BOUNDARY"
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="BOUNDARY"
--BOUNDARY
@@ -1083,28 +1102,28 @@ async fn test_render_unencrypted_msg_basic() -> Result<()> {
let expected = r#"From: <alice@example.org>
Message-ID: <MESSAGE_ID@localhost>
MIME-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Content-Type: text/plain; charset="utf-8"
Date: DATE
To: <bob@example.net>
Subject: Message from alice@example.org
References: <MESSAGE_ID@localhost>
Chat-Version: 1.0
Chat-Disposition-Notification-To: alice@example.org
Content-Transfer-Encoding: 7bit
Hello!"#
.replace("\n", "\r\n");
.replace("\n", "\r\n");
assert_eq!(
unencrypted, expected,
"---------------- Actual: ----------------
@@ -1127,32 +1146,32 @@ async fn test_render_unencrypted_msg_with_attachment() -> Result<()> {
.await;
let mut msg = Message::new(Viewtype::File);
msg.set_text("Hello!".to_string());
msg.set_file_from_bytes(alice, "foo.bar", "content".as_bytes(), None)?;
msg.set_file_from_bytes(alice, "foo.bar", b"content", None)?;
let sent = alice.send_msg(chat.id, &mut msg).await;
let unencrypted = normalized_payload(sent).await;
let expected = r#"From: <alice@example.org>
Message-ID: <MESSAGE_ID@localhost>
MIME-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Content-Type: multipart/mixed;
boundary="BOUNDARY"
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Content-Type: multipart/mixed;
boundary="BOUNDARY"
Date: DATE
To: <bob@example.net>
Subject: Message from alice@example.org
References: <MESSAGE_ID@localhost>
Chat-Version: 1.0
Chat-Disposition-Notification-To: alice@example.org
--BOUNDARY
+7 -10
View File
@@ -294,11 +294,7 @@ impl MimeMessage {
&mut wants_mdn,
&mail,
);
headers_removed.extend(
headers
.extract_if(|k, _v| is_hidden(k))
.map(|(k, _v)| k.to_string()),
);
headers_removed.extend(headers.extract_if(|k, _v| is_hidden(k)).map(|(k, _v)| k));
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -935,9 +931,10 @@ impl MimeMessage {
// See if an MDN is requested from the other side
if self.decryption_error.is_none()
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
&& self.wants_mdn
&& self.incoming
&& let Some(part) = self.parts.last_mut()
&& self.wants_mdn
&& self.was_encrypted() // Do not send MDNs for unencrypted messages.
&& self.incoming
&& let Some(part) = self.parts.last_mut()
{
part.param.set_int(Param::WantsMdn, 1);
}
@@ -951,7 +948,7 @@ impl MimeMessage {
typ: Viewtype::Text,
..Default::default()
};
if self.wants_mdn && self.incoming {
if self.wants_mdn && self.was_encrypted() && self.incoming {
part.param.set_int(Param::WantsMdn, 1);
}
if let Some(ref subject) = self.get_subject()
@@ -1751,7 +1748,7 @@ impl MimeMessage {
headers_removed.extend(
headers
.extract_if(|k, _v| has_header_protection || is_protected(k))
.map(|(k, _v)| k.to_string()),
.map(|(k, _v)| k),
);
if has_header_protection {
+1 -1
View File
@@ -33,7 +33,7 @@ use tls::wrap_tls;
pub(crate) const TIMEOUT: Duration = Duration::from_secs(60);
/// TTL for caches in seconds.
pub(crate) const CACHE_TTL: u32 = 30 * 24 * 60 * 60;
pub(crate) const CACHE_TTL: u64 = 30 * 24 * 60 * 60;
/// Removes connection history entries after `CACHE_TTL`.
pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
+4 -2
View File
@@ -410,7 +410,8 @@ impl ProxyConfig {
};
let tcp_stream =
crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
.await?;
.await
.context("Failed to connect to HTTP proxy")?;
let auth = if let Some((username, password)) = &http_config.user_password {
Some((username.as_str(), password.as_str()))
} else {
@@ -429,7 +430,8 @@ impl ProxyConfig {
let tcp_stream =
crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
.await?;
.await
.context("Failed to connect to HTTPS proxy")?;
let use_sni = true;
let tls_stream = wrap_rustls(
&hostname,
+1 -1
View File
@@ -46,7 +46,7 @@ use crate::mimeparser::SystemMessage;
/// The length of an ed25519 `PublicKey`, in bytes.
const PUBLIC_KEY_LENGTH: usize = 32;
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
const PUBLIC_KEY_STUB: &[u8] = b"static_string";
/// Store Iroh peer channels for the context.
#[derive(Debug)]
+10 -10
View File
@@ -86,14 +86,14 @@ async fn test_can_communicate() {
.get_or_try_init_peer_channel()
.await
.unwrap()
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
.await
.unwrap();
loop {
let event = bob.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
if data == "alice -> bob".as_bytes() {
if data == b"alice -> bob" {
break;
} else {
panic!(
@@ -107,14 +107,14 @@ async fn test_can_communicate() {
bob.get_or_try_init_peer_channel()
.await
.unwrap()
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
.await
.unwrap();
loop {
let event = alice.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
if data == "bob -> alice".as_bytes() {
if data == b"bob -> alice" {
break;
} else {
panic!(
@@ -149,14 +149,14 @@ async fn test_can_communicate() {
bob.get_or_try_init_peer_channel()
.await
.unwrap()
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice 2".to_vec())
.await
.unwrap();
loop {
let event = alice.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
if data == "bob -> alice 2".as_bytes() {
if data == b"bob -> alice 2" {
break;
} else {
panic!(
@@ -314,14 +314,14 @@ async fn test_can_reconnect() {
.get_or_try_init_peer_channel()
.await
.unwrap()
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
.await
.unwrap();
loop {
let event = bob.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
if data == "alice -> bob".as_bytes() {
if data == b"alice -> bob" {
break;
} else {
panic!(
@@ -373,14 +373,14 @@ async fn test_can_reconnect() {
bob.get_or_try_init_peer_channel()
.await
.unwrap()
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
.await
.unwrap();
loop {
let event = alice.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
if data == "bob -> alice".as_bytes() {
if data == b"bob -> alice" {
break;
} else {
panic!(
+1 -1
View File
@@ -401,7 +401,7 @@ pub fn merge_openpgp_certificates(
// such as Alice's key in `test-data/key/alice-secret.asc`.
let best_user: Option<SignedUser> = old_users
.into_iter()
.chain(new_users.clone())
.chain(new_users)
.filter_map(|SignedUser { id, signatures }| {
// Select the best signature for each User ID.
// If User ID has no valid signatures, it is filtered out.
+1 -1
View File
@@ -384,7 +384,7 @@ fn test_merge_openpgp_certificates() {
// Cannot merge certificates with different primary key.
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
assert!(merge_openpgp_certificates(bob, alice).is_err());
}
/// Test PQC support.
+5 -9
View File
@@ -1,6 +1,6 @@
//! Handle plain text together with some attributes.
use std::sync::LazyLock;
use regex::regex;
use crate::simplify::remove_message_footer;
@@ -25,12 +25,8 @@ impl PlainText {
/// Convert plain text to HTML.
/// The function handles quotes, links, fixed and floating text paragraphs.
pub fn to_html(&self) -> String {
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
@@ -52,12 +48,12 @@ impl PlainText {
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
let line = line.to_string().replace('\r', "");
let mut line = LINKIFY_MAIL_RE
let mut line = linkify_mail_re
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
line = LINKIFY_URL_RE
line = linkify_url_re
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
+8 -10
View File
@@ -2,13 +2,13 @@
mod dclogin_scheme;
use std::collections::BTreeMap;
use std::sync::LazyLock;
use anyhow::{Context as _, Result, anyhow, bail, ensure};
pub use dclogin_scheme::LoginOptions;
pub(crate) use dclogin_scheme::login_param_from_login_qr;
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
use regex::regex;
use serde::Deserialize;
use crate::autorelay::login_param_from_host;
@@ -764,7 +764,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
let addr = server_config.addr();
let host = addr.host().to_string();
let host = addr.host();
let port = addr.port();
Ok(Qr::Proxy {
url: qr.to_string(),
@@ -1071,16 +1071,11 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
Qr::from_address(context, name, &addr, None).await
}
static VCARD_NAME_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
/// Extract address for the vcard scheme.
///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
let name = VCARD_NAME_RE
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
.captures(qr)
.and_then(|caps| {
let last_name = caps.get(1)?.as_str().trim();
@@ -1090,7 +1085,10 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
})
.unwrap_or_default();
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
.captures(qr)
.and_then(|caps| caps.get(2))
{
normalize_address(cap.as_str().trim())?
} else {
bail!("Bad e-mail address");
@@ -1124,7 +1122,7 @@ fn normalize_address(addr: &str) -> Result<String> {
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
Ok(new_addr.to_string())
Ok(new_addr)
}
#[cfg(test)]
+1 -1
View File
@@ -120,7 +120,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
};
Ok(Qr::Login {
address: addr.to_owned(),
address: addr,
options,
})
} else {
+3 -4
View File
@@ -84,7 +84,7 @@ pub struct ReactionFrequency {
pub reaction: Reaction,
/// Number of contacts that reacted with this emoji.
pub count: u32,
pub count: usize,
/// True if `ContactId::SELF` is among the contacts that reacted with this emoji.
pub is_from_self: bool,
@@ -165,7 +165,7 @@ async fn set_msg_id_reaction(
.await?;
if chat
.param
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
.update_timestamp(Param::LastReactionTimestamp, timestamp)
{
chat.param
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
@@ -420,7 +420,7 @@ pub(crate) async fn apply_pending_reactions(
/// sorted in descending order of frequency.
fn calc_frequencies(by_contact: &BTreeMap<ContactId, Reaction>) -> Vec<ReactionFrequency> {
let mut self_reaction = Reaction::new("");
let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for (contact_id, reaction) in by_contact {
let count = counts.entry(reaction.as_str()).or_insert(0);
*count = count.saturating_add(1);
@@ -1032,7 +1032,6 @@ Content-Disposition: reaction\n\
assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
assert!(summary.prefix.is_none());
assert!(summary.thumbnail_path.is_none());
assert_summary(&alice, "BOB reacted 👍 to \"Party?\"").await;
// Alice reacts to own message as well
+4 -4
View File
@@ -42,7 +42,7 @@ struct WireMessage {
#[derive(Debug, Serialize, Deserialize)]
struct WireEntry {
emoji: String,
count: u32,
count: usize,
}
/// Renders one or more message's states as a JSON string, ready to be sent in `Chat-Broadcast-States:` header.
@@ -235,10 +235,10 @@ pub(crate) async fn load_broadcast_reactions(
(msg_id,),
|row| {
let reaction: String = row.get(0)?;
let count: u32 = row.get(1)?;
let count: i64 = row.get(1)?;
Ok(ReactionFrequency {
reaction: Reaction::new(&reaction),
count,
count: count as usize,
is_from_self: false,
})
},
@@ -405,7 +405,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_frequencies() {
// Helper to create a ReactionFrequency entry
let freq = |emoji: &str, count: u32, is_from_self: bool| -> ReactionFrequency {
let freq = |emoji: &str, count: usize, is_from_self: bool| -> ReactionFrequency {
ReactionFrequency {
reaction: Reaction::new(emoji),
count,
+8 -13
View File
@@ -483,7 +483,7 @@ pub(crate) async fn receive_imf_inner(
}
let trash = || async {
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
Ok(Some(ReceivedMsg {
chat_id: ChatId::TRASH,
state: MessageState::Undefined,
@@ -668,14 +668,15 @@ pub(crate) async fn receive_imf_inner(
match res {
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
let msg_id = insert_tombstone(context, rfc724_mid).await?;
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
received_msg = Some(ReceivedMsg {
chat_id: ChatId::TRASH,
state: MessageState::InSeen,
hidden: false,
sort_timestamp: mime_parser.timestamp_sent,
msg_ids: vec![msg_id],
needs_delete_job: res == securejoin::HandshakeMessage::Done,
needs_delete_job,
});
}
securejoin::HandshakeMessage::Propagate => {
@@ -2311,7 +2312,7 @@ INSERT INTO msgs
// This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
if chat
.param
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)
{
// write the last subject even if empty -
// otherwise a reply may get an outdated subject.
@@ -2401,7 +2402,7 @@ async fn handle_edit_delete(
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
insert_tombstone(context, rfc724_mid).await?;
insert_tombstone(context, rfc724_mid, false).await?;
continue;
};
@@ -2716,12 +2717,6 @@ async fn lookup_or_create_adhoc_group(
for &id in &contact_ids {
stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
}
// Contact IDs are 32-bit internally,
// so this conversion of contact ID set size
// to u32 should never fail.
let contact_ids_len = u32::try_from(contact_ids.len())?;
let val = t
.query_row(
"SELECT c.id, c.blocked
@@ -2735,7 +2730,7 @@ async fn lookup_or_create_adhoc_group(
AND contact_id NOT IN (SELECT id FROM temp.contacts)
AND add_timestamp >= remove_timestamp)=0
ORDER BY m.timestamp DESC",
(&grpname, contact_ids_len),
(&grpname, contact_ids.len()),
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;
@@ -3420,7 +3415,7 @@ async fn apply_chat_name_avatar_and_description_changes(
&& is_from_in_chat
&& chat
.param
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)
{
info!(context, "Group-avatar change for {}.", chat.id);
match avatar_action {
+4 -1
View File
@@ -416,7 +416,9 @@ async fn test_escaped_from() {
);
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
assert_eq!(msg.text, "hello");
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
// MDN request from unencrypted message is ignored.
assert!(msg.param.get_int(Param::WantsMdn).is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2955,6 +2957,7 @@ async fn test_weird_and_duplicated_filenames() -> Result<()> {
"a.tar.gz",
"a.a..a.a.a.a.tar.gz",
"a. tar.tar.gz",
"very long filename with emoji \u{1F600} very long file name with emoji \u{1FAE0} very long file name with emoji.tar.gz",
] {
let attachment = alice.blobdir.join(filename_sent);
let content = "File content of tar.gz archive".to_string();
+16 -21
View File
@@ -33,14 +33,14 @@ pub(crate) use qrinvite::QrInvite;
use crate::token::Namespace;
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_').remove(b'@');
fn inviter_progress(
context: &Context,
contact_id: ContactId,
chat_id: ChatId,
chat_type: Chattype,
) -> Result<()> {
) {
// No other values are used.
let progress = 1000;
context.emit_event(EventType::SecurejoinInviterProgress {
@@ -49,8 +49,6 @@ fn inviter_progress(
chat_type,
progress,
});
Ok(())
}
/// Shorten name to max. `length` characters.
@@ -121,21 +119,18 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
let fingerprint = self_fingerprint(context).await?;
let self_addr = context.get_primary_self_addr().await?;
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
let self_addrs = context.get_self_addrs().await?;
let mut encoded_addrs = self_addrs
.iter()
.map(|addr| utf8_percent_encode(addr, DISALLOWED_CHARACTERS).to_string());
let self_addr_urlencoded = encoded_addrs.next().context("No self addr configured")?;
let encoded_extra_relays: Vec<String> = encoded_addrs.collect();
let r_param = context
.get_self_addrs()
.await?
.into_iter()
.filter(|addr| *addr != self_addr)
.reduce(|acc, addr| {
format!(
"{acc},{}",
utf8_percent_encode(&addr, DISALLOWED_CHARACTERS)
)
})
.map_or(String::default(), |addrs| format!("&r={addrs}"));
let r_param = if encoded_extra_relays.is_empty() {
"".to_string()
} else {
format!("&r={}", encoded_extra_relays.join(","))
};
let self_name = context
.get_config(Config::Displayname)
@@ -659,7 +654,7 @@ pub(crate) async fn handle_securejoin_handshake(
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
inviter_progress(context, contact_id, joining_chat_id, chat.typ);
// IMAP-delete the message to avoid handling it by another device and adding the
// member twice. Another device will know the member's key from Autocrypt-Gossip.
Ok(HandshakeMessage::Done)
@@ -670,7 +665,7 @@ pub(crate) async fn handle_securejoin_handshake(
.await
.context("failed sending vc-contact-confirm message")?;
inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
inviter_progress(context, contact_id, chat_id, Chattype::Single);
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
}
@@ -817,7 +812,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
// and tests which don't care about the chat ID,
// so we pass invalid chat ID here.
let chat_id = ChatId::new(0);
inviter_progress(context, contact_id, chat_id, chat_type)?;
inviter_progress(context, contact_id, chat_id, chat_type);
}
if matches!(step, SecureJoinStep::MemberAdded) {
+65
View File
@@ -6,6 +6,7 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::key::self_fingerprint;
use crate::message::rfc724_mid_exists_ext;
use crate::qr::Qr;
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2ee_info_msg};
@@ -13,6 +14,7 @@ use crate::test_utils::{
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
TimeShiftFalsePositiveNote, get_chat_msg, sync,
};
use crate::transport::add_pseudo_transport;
#[derive(PartialEq)]
enum SetupContactCase {
@@ -1142,6 +1144,43 @@ async fn test_get_securejoin_qr_name_is_last() -> Result<()> {
Ok(())
}
/// Test that addresses in QR codes are percent-encoded.
/// `@` should not be encoded unnecessarily,
/// since this would just make the QR code longer.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_securejoin_qr_encoding() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// `@` in email addresses must not be percent-encoded:
add_pseudo_transport(alice, "asdf@example.org").await?;
// But `%` does need percent-encoding:
add_pseudo_transport(alice, "jk%l@example.net").await?;
let qr = get_securejoin_qr(alice, None).await?;
assert!(
qr.contains("a=jk%25l@example.net"),
"{qr} doesn't contain 'a=jk%25l@example.net'"
);
assert!(
qr.contains("r=asdf@example.org,alice@example.org"),
"{qr} doesn't contain 'r=asdf@example.org,alice@example.org'"
);
let qr = check_qr(bob, &qr).await?;
let Qr::AskVerifyContact { mut addrs, .. } = qr else {
unreachable!()
};
addrs.sort();
assert_eq!(
addrs,
vec!["alice@example.org", "asdf@example.org", "jk%l@example.net",]
);
Ok(())
}
/// QR codes should not get arbitrary big because of long names.
/// The truncation, however, should not let the url end with a `.`, which is a call for trouble in linkfiers.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1525,3 +1564,29 @@ async fn test_deduplicate_member_added() -> Result<()> {
Ok(())
}
/// Tests that a handled join request is also marked as "deleted"
/// in the database, so that if a copy of the request arrives via
/// other relays in the future, then this copy will also be deleted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_join_request_deleted_on_all_relays() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
bob.add_or_lookup_contact_id(alice).await;
join_securejoin(bob, &qr).await?;
let request = bob.pop_sent_msg().await;
alice.recv_msg_trash(&request).await;
let rfc724_mid = Message::load_from_db(bob, request.sender_msg_id)
.await?
.rfc724_mid;
let (_, deleted) = rfc724_mid_exists_ext(alice, &rfc724_mid, "deleted=1")
.await?
.unwrap();
assert!(deleted);
Ok(())
}
+84 -32
View File
@@ -22,7 +22,6 @@ use crate::log::{LogExt, warn};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory;
use crate::mimefactory::MimeFactory;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::scheduler::connectivity::ConnectivityStore;
@@ -55,6 +54,39 @@ pub(crate) struct Smtp {
pub(crate) last_send_error: Option<String>,
}
/// Returns transports with their IDs in the order in which they should be tried.
async fn sorted_transports(context: &Context) -> Result<Vec<(u32, ConfiguredLoginParam)>> {
context
.sql
.query_map_vec(
"SELECT transports.id, configured_param FROM transports
LEFT JOIN smtp_success ON smtp_success.transport_id=transports.id
ORDER BY IFNULL(smtp_success.id, 0) DESC, transports.id ASC",
(),
|row| {
let id: u32 = row.get(0)?;
let json: String = row.get(1)?;
let param = ConfiguredLoginParam::from_json(&json)?;
Ok((id, param))
},
)
.await
}
/// Records successful use of SMTP transport so it is tried first next time we connect to SMTP.
async fn record_success(context: &Context, transport_id: u32) -> Result<()> {
// INSERT OR REPLACE essentially replaces rowid of the row
// if the row exists already, so it becomes the highest rowid in the table.
context
.sql
.execute(
"INSERT OR REPLACE INTO smtp_success (transport_id) VALUES (?)",
(transport_id,),
)
.await?;
Ok(())
}
impl Smtp {
/// Create a new Smtp instances.
pub fn new() -> Self {
@@ -69,6 +101,8 @@ impl Smtp {
// separate task to avoid waiting for reply or timeout.
task::spawn(async move { transport.quit().await });
}
self.transport_id = None;
self.from = None;
self.last_success = None;
}
@@ -100,13 +134,7 @@ impl Smtp {
self.connectivity.set_connecting(context);
let proxy_config = ProxyConfig::load(context).await?;
let transports = ConfiguredLoginParam::load_all(context).await?;
// Try to connect to the newest transport first. If sending is unreliable,
// user can configure a new transport and it will be the one used.
// Conversely, if user just added a new transport and sending got less reliable,
// user can restore old state by removing the just added transport.
for (transport_id, lp) in transports.into_iter().rev() {
for (transport_id, lp) in sorted_transports(context).await? {
info!(context, "Trying to connect to transport {transport_id}.");
match self
.connect(
@@ -326,6 +354,18 @@ pub(crate) async fn smtp_send(
Ok(()) => SendResult::Success,
};
if matches!(status, SendResult::Success) {
debug_assert!(smtp.transport_id.is_some());
if let Some(transport_id) = smtp.transport_id
&& let Err(err) = record_success(context, transport_id).await
{
warn!(
context,
"Failed to record successful use of transport {transport_id} in smtp_success table: {err:#}."
);
}
}
if let SendResult::Failure(err) = &status
&& let Some(msg_id) = msg_id
{
@@ -356,7 +396,7 @@ pub(crate) async fn insert_into_smtp(
queued_msg: &QueuedMail,
) -> Result<()> {
let now = tools::time();
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
context
.sql
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
@@ -652,6 +692,24 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
Ok(())
}
async fn delete_mdns_by_rfc724_mid(
context: &Context,
rfc724_mid: &str,
additional_rfc724_mids: Vec<String>,
) -> Result<()> {
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
stmt.execute((rfc724_mid,))?;
for additional_rfc724_mid in additional_rfc724_mids {
stmt.execute((additional_rfc724_mid,))?;
}
Ok(())
})
.await
}
/// Tries to send MDN for message identified by `rfc724_mdn` to `contact_id`.
///
/// Attempts to aggregate additional MDNs for `contact_id` into sent MDN.
@@ -687,28 +745,30 @@ async fn send_mdn_rfc724_mid(
)
.await?;
let mimefactory = MimeFactory::from_mdn(
let queued_mdn = mimefactory::mdn(
context,
contact_id,
rfc724_mid.to_string(),
rfc724_mid,
additional_rfc724_mids.clone(),
)
.await?;
let encrypted = mimefactory.will_be_encrypted();
let mut recipients = if contact_id == ContactId::SELF {
Vec::new()
} else {
mimefactory.recipients()
};
let bcc_self = queued_mdn.bcc_self;
let encrypted = queued_mdn.encryption.is_encrypted();
let mut recipients = queued_mdn.recipients.clone();
let public_key = key::load_self_public_key(context).await?;
let secret_key = key::load_self_secret_key(context).await?;
let from = smtp
.from
.as_ref()
.context("No From address, not connected")?
.to_string();
let rendered_msg = Box::pin(mimefactory.render(context, &from)).await?;
let rendered_msg =
mimefactory::render_queued_mail(queued_mdn, &public_key, &secret_key, from.clone())?;
let body = rendered_msg.message;
if context.get_config_bool(Config::BccSelf).await? {
if bcc_self {
add_self_recipients(context, &mut recipients, encrypted, from).await?;
}
let recipients: Vec<_> = recipients
@@ -720,24 +780,13 @@ async fn send_mdn_rfc724_mid(
.ok()
})
.collect();
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
match smtp_send(context, &recipients, &body, smtp, None).await {
SendResult::Success => {
if !recipients.is_empty() {
info!(context, "Successfully sent MDN for {rfc724_mid}.");
}
context
.sql
.transaction(|transaction| {
let mut stmt =
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
stmt.execute((rfc724_mid,))?;
for additional_rfc724_mid in additional_rfc724_mids {
stmt.execute((additional_rfc724_mid,))?;
}
Ok(())
})
.await?;
delete_mdns_by_rfc724_mid(context, rfc724_mid, additional_rfc724_mids).await?;
Ok(true)
}
SendResult::Retry => {
@@ -848,3 +897,6 @@ pub(crate) async fn add_self_recipients(
Ok(())
}
#[cfg(test)]
mod smtp_tests;
+1 -1
View File
@@ -109,7 +109,7 @@ pub struct SideEffects {
}
/// Email message ready to be queued with the side effects that should be applied at the same time.
pub(crate) type ToBeQueuedMail = (QueuedMail, Option<SideEffects>);
pub(crate) type ToBeQueuedMail = (QueuedMail, SideEffects);
/// Process side effects and store queued mail.
pub(crate) fn enqueue_mail(
+49
View File
@@ -0,0 +1,49 @@
use anyhow::Result;
use crate::test_utils::TestContextManager;
use crate::transport;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_smtp_candidates() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.unconfigured().await;
transport::add_pseudo_transport(t, "foo@example.net").await?;
transport::add_pseudo_transport(t, "bar@example.net").await?;
transport::add_pseudo_transport(t, "baz@example.net").await?;
let transports = super::sorted_transports(t).await?;
let [
(transport_id1, ref transport1),
(transport_id2, ref transport2),
(transport_id3, ref transport3),
] = transports[..]
else {
panic!("Unexpected number of transports");
};
// By default first added transport is used first.
assert_eq!(transport1.addr, "foo@example.net");
assert_eq!(transport2.addr, "bar@example.net");
assert_eq!(transport3.addr, "baz@example.net");
super::record_success(t, transport_id3).await?;
let transports2 = super::sorted_transports(t).await?;
assert_eq!(transports2[0].0, transport_id3);
assert_eq!(transports2[1].0, transport_id1);
assert_eq!(transports2[2].0, transport_id2);
super::record_success(t, transport_id2).await?;
let transports3 = super::sorted_transports(t).await?;
assert_eq!(transports3[0].0, transport_id2);
assert_eq!(transports3[1].0, transport_id3);
assert_eq!(transports3[2].0, transport_id1);
super::record_success(t, transport_id3).await?;
let transports4 = super::sorted_transports(t).await?;
assert_eq!(transports4[0].0, transport_id3);
assert_eq!(transports4[1].0, transport_id2);
assert_eq!(transports4[2].0, transport_id1);
Ok(())
}
+120 -16
View File
@@ -14,7 +14,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
use crate::ephemeral::start_ephemeral_timers;
use crate::imex::BLOBS_BACKUP_NAME;
use crate::imex::{self, BLOBS_BACKUP_NAME};
use crate::location;
use crate::log::{LogExt, warn};
use crate::message::MsgId;
@@ -56,6 +56,10 @@ pub struct Sql {
/// SQL connection pool.
pool: RwLock<Option<Pool>>,
/// None if the database is not open, true if it is open with passphrase and false if it is
/// open without a passphrase.
is_encrypted: RwLock<Option<bool>>,
/// Cache of `config` table.
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
}
@@ -66,15 +70,52 @@ impl Sql {
Self {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
config_cache: Default::default(),
}
}
/// Tests SQLCipher passphrase.
///
/// Returns true if passphrase is correct, i.e. the database is new or can be unlocked with
/// this passphrase, and false if the database is already encrypted with another passphrase or
/// corrupted.
///
/// Fails if database is already open.
pub async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
if self.is_open().await {
bail!("Database is already opened.");
}
// Hold the lock to prevent other thread from opening the database.
let _lock = self.pool.write().await;
// Test that the key is correct using a single connection.
let connection = Connection::open(&self.dbfile)?;
if !passphrase.is_empty() {
connection
.pragma_update(None, "key", &passphrase)
.context("Failed to set PRAGMA key")?;
}
let key_is_correct = connection
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.is_ok();
Ok(key_is_correct)
}
/// Checks if there is currently a connection to the underlying Sqlite database.
pub async fn is_open(&self) -> bool {
self.pool.read().await.is_some()
}
/// Returns true if the database is encrypted.
///
/// If database is not open, returns `None`.
pub(crate) async fn is_encrypted(&self) -> Option<bool> {
*self.is_encrypted.read().await
}
/// Closes all underlying Sqlite connections.
pub(crate) async fn close(&self) {
let _ = self.pool.write().await.take();
@@ -82,22 +123,52 @@ impl Sql {
}
/// Imports the database from a separate file with the given passphrase.
pub(crate) async fn import(&self, path: &Path) -> Result<()> {
pub(crate) async fn import(&self, path: &Path, passphrase: String) -> Result<()> {
let path_str = path
.to_str()
.with_context(|| format!("path {path:?} is not valid unicode"))?
.to_string();
// Keep `config_cache` locked all the time the db is imported so that nobody can use invalid
// values from there. And clear it immediately so as not to forget in case of errors.
let mut config_cache = self.config_cache.write().await;
config_cache.clear();
let src_conn =
rusqlite::Connection::open(path).context("Failed to open source database")?;
let query_only = false;
self.call(query_only, move |conn| {
let backup = rusqlite::backup::Backup::new(&src_conn, &mut *conn)?;
backup.run_to_completion(5, std::time::Duration::ZERO, None)?;
drop(backup);
// Check that backup passphrase is correct before resetting our database.
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.context("backup passphrase is not correct");
conn.execute("VACUUM", [])
.context("failed to vacuum the database")?;
// Reset the database without reopening it. We don't want to reopen the database because we
// don't have main database passphrase at this point.
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
// Without resetting import may fail due to existing tables.
res.and_then(|_| {
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
})
.and_then(|_| {
conn.execute("VACUUM", [])
.context("failed to vacuum the database")
})
.and(
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
)
.and_then(|_| {
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
Ok(())
})
.context("failed to import from attached backup database")
})
.and(
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database"),
)?;
Ok(())
})
.await
@@ -106,10 +177,10 @@ impl Sql {
const N_DB_CONNECTIONS: usize = 3;
/// Creates a new connection pool.
fn new_pool(dbfile: &Path) -> Result<Pool> {
fn new_pool(dbfile: &Path, passphrase: String) -> Result<Pool> {
let mut connections = Vec::with_capacity(Self::N_DB_CONNECTIONS);
for _ in 0..Self::N_DB_CONNECTIONS {
let connection = new_connection(dbfile)?;
let connection = new_connection(dbfile, &passphrase)?;
connections.push(connection);
}
@@ -117,8 +188,8 @@ impl Sql {
Ok(pool)
}
async fn try_open(&self, context: &Context, dbfile: &Path) -> Result<()> {
*self.pool.write().await = Some(Self::new_pool(dbfile)?);
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
if let Err(e) = self.run_migrations(context).await {
error!(context, "Running migrations failed: {e:#}");
@@ -176,7 +247,7 @@ impl Sql {
/// Opens the provided database and runs any necessary migrations.
/// If a database is already open, this will return an error.
pub async fn open(&self, context: &Context) -> Result<()> {
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
if self.is_open().await {
error!(
context,
@@ -185,8 +256,10 @@ impl Sql {
bail!("SQL database is already opened.");
}
self.try_open(context, &self.dbfile).await?;
let passphrase_nonempty = !passphrase.is_empty();
self.try_open(context, &self.dbfile, passphrase).await?;
info!(context, "Opened database {:?}.", self.dbfile);
*self.is_encrypted.write().await = Some(passphrase_nonempty);
// setup debug logging if there is an entry containing its id
if let Some(xdc_id) = self
@@ -198,6 +271,28 @@ impl Sql {
Ok(())
}
/// Changes the passphrase of encrypted database.
///
/// The database must already be encrypted and the passphrase cannot be empty.
/// It is impossible to turn encrypted database into unencrypted
/// and vice versa this way, use import/export for this.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
let mut lock = self.pool.write().await;
let pool = lock.take().context("SQL connection pool is not open")?;
let query_only = false;
let conn = pool.get(query_only).await?;
if !passphrase.is_empty() {
conn.pragma_update(None, "rekey", passphrase.clone())
.context("Failed to set PRAGMA rekey")?;
}
drop(pool);
*lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?);
Ok(())
}
/// Allocates a connection and calls `function` with the connection.
///
/// If `query_only` is true, allocates read-only connection,
@@ -595,7 +690,7 @@ impl Sql {
///
/// `passphrase` is the SQLCipher database passphrase.
/// Empty string if database is not encrypted.
fn new_connection(path: &Path) -> Result<Connection> {
fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
| OpenFlags::SQLITE_OPEN_READ_WRITE
| OpenFlags::SQLITE_OPEN_CREATE;
@@ -631,6 +726,9 @@ fn new_connection(path: &Path) -> Result<Connection> {
conn.busy_timeout(Duration::ZERO)?;
}
if !passphrase.is_empty() {
conn.pragma_update(None, "key", passphrase)?;
}
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
// VACUUM, which usually happens before backup export.
@@ -729,6 +827,12 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
imex::maybe_readd_backup_transfer_msg(context)
.await
.context("Failed to re-add backup transfer message")
.log_err(context)
.ok();
if let Err(err) = incremental_vacuum(context).await {
warn!(context, "Failed to run incremental vacuum: {err:#}.");
}
+22 -15
View File
@@ -32,8 +32,6 @@ fn migrate_key_contacts(
context: &Context,
transaction: &mut rusqlite::Transaction<'_>,
) -> std::result::Result<(), anyhow::Error> {
info!(context, "Starting key-contact transition.");
// =============================== Step 1: ===============================
// Alter tables
transaction.execute_batch(
@@ -79,13 +77,12 @@ fn migrate_key_contacts(
.optional()
.context("Step 0")?
else {
info!(
context,
"Not yet configured, no need to migrate key-contacts"
);
// Not yet configured, no need to migrate key-contacts.
return Ok(());
};
info!(context, "Starting key-contact transition.");
// =============================== Step 2: ===============================
// Create up to 3 new contacts for every contact that has a peerstate:
// one from the Autocrypt key fingerprint, one from the verified key fingerprint,
@@ -657,7 +654,7 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
return Ok(());
}
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut first_key_contacts_msg_id: u32 = t
let mut first_key_contacts_msg_id: u64 = t
.query_one(
"SELECT CAST(value AS INTEGER) FROM config WHERE keyname='first_key_contacts_msg_id'",
(),
@@ -681,10 +678,10 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
)
.context("Prepare stmt")?;
let msgs_to_migrate = 1000;
let mut msgs_migrated: u32 = 0;
let mut msgs_migrated: u64 = 0;
while first_key_contacts_msg_id > 0 && msgs_migrated < msgs_to_migrate {
let start_msg_id = first_key_contacts_msg_id.saturating_sub(msgs_to_migrate);
let cnt: u32 = stmt
let cnt: u64 = stmt
.execute((start_msg_id, first_key_contacts_msg_id))
.context("UPDATE msgs")?
.try_into()?;
@@ -1936,14 +1933,9 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
inc_and_check(&mut migration_version, 132)?;
if dbversion < migration_version {
let start = Time::now();
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
.await?;
info!(
context,
"key-contacts migration took {:?} in total.",
time_elapsed(&start),
);
// Schedule `msgs_to_key_contacts()`.
context
.set_config_internal(Config::LastHousekeeping, None)
@@ -2680,6 +2672,21 @@ CREATE TABLE smtp2 (
.await?;
}
inc_and_check(&mut migration_version, 168)?;
if dbversion < migration_version {
sql.execute_migration(
"
CREATE TABLE smtp_success (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
transport_id INTEGER UNIQUE NOT NULL,
FOREIGN KEY(transport_id) REFERENCES transports(id) ON DELETE CASCADE
) STRICT;
",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
+96 -3
View File
@@ -83,7 +83,7 @@ async fn test_housekeeping_db_closed() {
t.sql.close().await;
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
t.sql.open(&t).await.unwrap();
t.sql.open(&t, "".to_string()).await.unwrap();
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
@@ -155,11 +155,11 @@ async fn test_db_reopen() -> Result<()> {
let sql = Sql::new(dbfile);
// Create database with all the tables.
sql.open(&t).await.unwrap();
sql.open(&t, "".to_string()).await.unwrap();
sql.close().await;
// Reopen the database
sql.open(&t).await?;
sql.open(&t, "".to_string()).await?;
sql.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?);",
("foo", "bar"),
@@ -209,6 +209,99 @@ async fn test_migration_flags() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_check_passphrase() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new(dbfile.clone());
sql.check_passphrase("foo".to_string()).await?;
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database first time")?;
sql.close().await;
// Reopen the database
let sql = Sql::new(dbfile);
// Test that we can't open encrypted database without a passphrase.
assert!(sql.open(&t, "".to_string()).await.is_err());
// Now open the database with passpharse, it should succeed.
sql.check_passphrase("foo".to_string()).await?;
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database second time")?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sql_change_passphrase() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new(dbfile.clone());
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database first time")?;
sql.close().await;
// Change the passphrase from "foo" to "bar".
let sql = Sql::new(dbfile.clone());
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database second time")?;
sql.change_passphrase("bar".to_string())
.await
.context("failed to change passphrase")?;
// Test that at least two connections are still working.
// This ensures that not only the connection which changed the password is working,
// but other connections as well.
{
let lock = sql.pool.read().await;
let pool = lock.as_ref().unwrap();
let query_only = true;
let conn1 = pool.get(query_only).await?;
let conn2 = pool.get(query_only).await?;
conn1
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.unwrap();
conn2
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.unwrap();
}
sql.close().await;
let sql = Sql::new(dbfile);
// Test that old passphrase is not working.
assert!(sql.open(&t, "foo".to_string()).await.is_err());
// Open the database with the new passphrase.
sql.check_passphrase("bar".to_string()).await?;
sql.open(&t, "bar".to_string())
.await
.context("failed to open the database third time")?;
sql.close().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_query_only() -> Result<()> {
let t = TestContext::new().await;
+3 -3
View File
@@ -69,7 +69,7 @@ struct ContactStat {
#[serde(skip_serializing_if = "is_false", rename = "direct_chat")]
single_chat: bool,
last_seen: i64,
last_seen: u64,
/// Whether the contact was established after stats-sending was enabled
#[serde(skip_serializing_if = "is_false")]
@@ -312,7 +312,7 @@ async fn ensure_last_old_contact_id(context: &Context) -> Result<()> {
return Ok(());
}
let last_contact_id: u32 = context
let last_contact_id: u64 = context
.sql
.query_get_value("SELECT MAX(id) FROM contacts", ())
.await?
@@ -436,7 +436,7 @@ async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<V
|row| {
let id = row.get(0)?;
let encrypted: bool = row.get(1)?;
let last_seen: i64 = row.get(2)?;
let last_seen: u64 = row.get(2)?;
let bot: bool = row.get(3)?;
Ok(ContactStat {
+8 -16
View File
@@ -56,18 +56,16 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
let blobdir_size =
tokio::task::spawn_blocking(move || get_blobdir_storage_usage(&context_clone));
let page_size: i64 = ctx
let page_size: u64 = ctx
.sql
.query_get_value("PRAGMA page_size", ())
.await?
.unwrap_or_default();
let page_size = u64::try_from(page_size)?;
let page_count: i64 = ctx
let page_count: u64 = ctx
.sql
.query_get_value("PRAGMA page_count", ())
.await?
.unwrap_or_default();
let page_count = u64::try_from(page_count)?;
let mut largest_tables = ctx
.sql
@@ -80,8 +78,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
(),
|row| {
let name: String = row.get(0)?;
let size: i64 = row.get(1)?;
let size: u64 = u64::try_from(size)?;
let size: u64 = row.get(1)?;
Ok((name, size, None))
},
)
@@ -89,13 +86,12 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
for row in &mut largest_tables {
let name = &row.0;
let row_count: Option<i64> = ctx
let row_count: Result<Option<u64>> = ctx
.sql
// SECURITY: the table name comes from the db, not from the user
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
.await
.unwrap_or_default();
row.2 = row_count.map(|count| u64::try_from(count).unwrap_or_default());
.await;
row.2 = row_count.unwrap_or_default();
}
let largest_webxdc_data = ctx
@@ -107,12 +103,8 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
(),
|row| {
let msg_id: MsgId = row.get(0)?;
let size: i64 = row.get(1)?;
let count: i64 = row.get(2)?;
// This should never fail as the count cannot be negative.
let size: u64 = u64::try_from(size)?;
let count: u64 = u64::try_from(count)?;
let size: u64 = row.get(1)?;
let count: u64 = row.get(2)?;
Ok((msg_id, size, count))
},
+98 -35
View File
@@ -10,6 +10,7 @@ use num_traits::FromPrimitive;
use crate::calls::{CallState, call_state};
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::constants::DC_DESIRED_TEXT_LEN;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
@@ -56,9 +57,6 @@ pub struct Summary {
/// Message state.
pub state: MessageState,
/// Message preview image path
pub thumbnail_path: Option<String>,
}
impl Summary {
@@ -83,7 +81,6 @@ impl Summary {
text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
timestamp: msg.get_timestamp(), // message timestamp (not reaction) to make timestamps more consistent with chats ordering
state: msg.state, // message state (not reaction) - indicating if it was me sending the last message
thumbnail_path: None,
});
}
Self::new(context, msg, chat, contact).await
@@ -121,30 +118,11 @@ impl Summary {
None
};
let mut text = msg.get_summary_text(context).await;
if text.is_empty() && msg.quoted_text().is_some() {
text = stock_str::reply_noun(context)
}
let thumbnail_path = if msg.viewtype == Viewtype::Image
|| msg.viewtype == Viewtype::Gif
|| msg.viewtype == Viewtype::Sticker
{
msg.get_file(context)
.and_then(|path| path.to_str().map(|p| p.to_owned()))
} else if msg.viewtype == Viewtype::Webxdc {
Some("webxdc-icon://last-msg-id".to_string())
} else {
None
};
Ok(Summary {
prefix,
text,
text: msg.get_summary_text(context).await,
timestamp: msg.get_timestamp(),
state: msg.state,
thumbnail_path,
})
}
@@ -155,19 +133,32 @@ impl Summary {
}
impl Message {
/// Returns a summary text.
/// Returns a summary text with emoji and "Forwarded:" prefixes.
/// This is the standard summary to be used in chatlists, notifications etc.
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
let summary = self.get_summary_text_without_prefix(context).await;
if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context), summary)
} else {
summary
}
let add_forwarded = true;
let add_type_emoji = true;
self.get_summary_text_ext(context, add_forwarded, add_type_emoji, DC_DESIRED_TEXT_LEN)
.await
}
/// Returns a summary text without "Forwarded:" prefix.
/// Returns a summary text with emoji prefixes but without "Forwarded:" prefix.
/// Used for shorter reaction summaries as "USER reacts 👋 to SUMMARY"
async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
let add_forwarded = false;
let add_type_emoji = true;
self.get_summary_text_ext(context, add_forwarded, add_type_emoji, DC_DESIRED_TEXT_LEN)
.await
}
/// Returns a summary text with optional "Forwarded:" and emoji prefixes and optionally converting to one line.
pub async fn get_summary_text_ext(
&self,
context: &Context,
add_forwarded: bool,
add_type_emoji: bool,
approx_chars: usize,
) -> String {
let (emoji, type_name, type_file, append_text);
let viewtype = match self
.param
@@ -281,7 +272,11 @@ impl Message {
}
};
let text = self.text.clone();
let text = if approx_chars > 0 {
truncate(self.text.trim(), approx_chars).to_string()
} else {
self.text.trim().to_string()
};
let summary = if let Some(type_file) = type_file {
if append_text && !text.is_empty() {
@@ -303,13 +298,30 @@ impl Message {
"".to_string()
};
let emoji = emoji.filter(|_| add_type_emoji);
let summary = if let Some(emoji) = emoji {
format!("{emoji} {summary}")
} else {
summary
};
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
let summary = if summary.is_empty() && self.quoted_text().is_some() {
stock_str::reply_noun(context)
} else {
summary
};
let summary = if add_forwarded && self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context), summary)
} else {
summary
};
if approx_chars > 0 {
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
} else {
summary
}
}
}
@@ -331,6 +343,13 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_text() {
let forwarded = true;
let emoji = true;
let one_line = 2000;
let no_forwarded = false;
let no_emoji = false;
let multi_line = 0;
let d = TestContext::new_alice().await;
let ctx = &d.ctx;
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
@@ -347,12 +366,22 @@ mod tests {
let msg = Message::new_text(some_text.to_string());
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
assert_eq!(
msg.get_summary_text_ext(ctx, forwarded, emoji, multi_line)
.await,
"bla \t\n\tbla" // lineends are preserved, but text is still trimmed
);
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
.unwrap();
assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, one_line)
.await,
"Image"
);
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::Image);
@@ -360,6 +389,16 @@ mod tests {
msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
.unwrap();
assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, one_line)
.await,
"bla bla"
);
assert_eq!(
msg.get_summary_text_ext(ctx, forwarded, emoji, multi_line)
.await,
"📷 bla \t\n\tbla"
);
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::Video);
@@ -490,6 +529,30 @@ mod tests {
msg.get_summary_text_without_prefix(ctx).await,
"📎 foo.bar \u{2013} bla bla"
); // skipping prefix used for reactions summaries
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, one_line)
.await,
"foo.bar \u{2013} bla bla"
);
assert_eq!(
msg.get_summary_text_ext(ctx, forwarded, no_emoji, one_line)
.await,
"Forwarded: foo.bar \u{2013} bla bla"
);
d.assert_warn("Not a valid DeltaChat vCard").await;
// If nothing else is present, but the message is a reply, we say so. needed for summary of draft
let mut msg = Message::new_text("".to_string());
msg.set_quote_text(Some(("blubb".to_string(), true)));
assert_summary_texts(&msg, ctx, "Reply").await;
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, multi_line)
.await,
"Reply"
);
// If there is nothing, the summary is empty
let msg = Message::new_text("".to_string());
assert_summary_texts(&msg, ctx, "").await;
}
}
+3 -4
View File
@@ -35,7 +35,7 @@ use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, self_fingerprint};
use crate::message::{Message, MessageState, MsgId};
use crate::mimefactory::{self, MimeFactory};
use crate::mimefactory;
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::pgp::SeipdVersion;
use crate::receive_imf::{ReceivedMsg, receive_imf};
@@ -852,9 +852,8 @@ ORDER BY id"
/// Receives a read receipt from `reader`, who received `msg`.
pub async fn recv_mdn(&self, reader: &TestContext, msg: &Message) -> Result<()> {
let mdn = MimeFactory::from_mdn(reader, msg.from_id, msg.rfc724_mid.clone(), vec![])
.await?
.render(reader, &reader.get_primary_self_addr().await?)
let queued_mdn = mimefactory::mdn(reader, msg.from_id, &msg.rfc724_mid, vec![]).await?;
let mdn = mimefactory::render_queued_mail_with_context(queued_mdn, reader)
.await?
.message;
receive_imf(self, mdn.as_bytes(), false).await?;
+4 -1
View File
@@ -105,7 +105,10 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await;
tcm.section("Test switching back");
tcm.change_addr(alice, "alice@example.org").await;
alice
.set_primary_self_addr("alice@example.org")
.await
.unwrap();
let sent = alice
.send_text(chat_to_send, "Hello from my old addr!")
.await;
+1
View File
@@ -220,6 +220,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
),
Some(&mut msg),
true,
time(),
)
.await
.ok();
+2 -2
View File
@@ -740,14 +740,14 @@ pub(crate) fn maybe_update_sending_transport(
pub async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
context.sql
.execute(
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
(
addr,
serde_json::to_string(&EnteredLoginParam{addr: addr.to_string(), ..Default::default()})?,
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
),
)
.await?;
.await.with_context(|| format!("Failed to insert pseudo transport for {addr:?}"))?;
Ok(())
}
+13 -13
View File
@@ -27,7 +27,7 @@ impl Context {
Ok(param.parse().unwrap_or_default())
},
)?;
let update = param.update_timestamp(scope, new_timestamp)?;
let update = param.update_timestamp(scope, new_timestamp);
if update {
transaction.execute(
"UPDATE contacts SET param=? WHERE id=?",
@@ -57,7 +57,7 @@ impl ChatId {
let param: String = row.get(0)?;
Ok(param.parse().unwrap_or_default())
})?;
let update = param.update_timestamp(scope, new_timestamp)?;
let update = param.update_timestamp(scope, new_timestamp);
if update {
transaction.execute(
"UPDATE chats SET param=? WHERE id=?",
@@ -73,13 +73,13 @@ impl ChatId {
impl Params {
/// Updates a param's timestamp in memory, if reasonable.
/// Returns true if the caller shall update the settings belonging to the scope.
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> Result<bool> {
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> bool {
let old_timestamp = self.get_i64(scope).unwrap_or_default();
if new_timestamp >= old_timestamp {
self.set_i64(scope, new_timestamp);
return Ok(true);
return true;
}
Ok(false)
false
}
}
@@ -96,18 +96,18 @@ mod tests {
let mut params = Params::new();
let ts = time();
assert!(params.update_timestamp(Param::LastSubject, ts)?);
assert!(params.update_timestamp(Param::LastSubject, ts)?); // same timestamp -> update
assert!(params.update_timestamp(Param::LastSubject, ts + 10)?);
assert!(!params.update_timestamp(Param::LastSubject, ts)?); // `ts` is now too old
assert!(!params.update_timestamp(Param::LastSubject, 0)?);
assert!(params.update_timestamp(Param::LastSubject, ts));
assert!(params.update_timestamp(Param::LastSubject, ts)); // same timestamp -> update
assert!(params.update_timestamp(Param::LastSubject, ts + 10));
assert!(!params.update_timestamp(Param::LastSubject, ts)); // `ts` is now too old
assert!(!params.update_timestamp(Param::LastSubject, 0));
assert_eq!(params.get_i64(Param::LastSubject).unwrap(), ts + 10);
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // stay unset -> update ...
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // ... also on multiple calls
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // stay unset -> update ...
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // ... also on multiple calls
assert_eq!(params.get_i64(Param::GroupNameTimestamp).unwrap(), 0);
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1)?);
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1));
assert_eq!(params.get_i64(Param::AvatarTimestamp), None);
Ok(())
+3 -3
View File
@@ -350,7 +350,7 @@ impl Context {
if let Some(ref document) = status_update_item.document
&& instance
.param
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)
{
instance.param.set(Param::WebxdcDocument, document);
param_changed = true;
@@ -359,10 +359,10 @@ impl Context {
if let Some(ref summary) = status_update_item.summary
&& instance
.param
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)
{
let summary = sanitize_bidi_characters(summary);
instance.param.set(Param::WebxdcSummary, summary.clone());
instance.param.set(Param::WebxdcSummary, summary);
param_changed = true;
}
+12 -15
View File
@@ -1201,30 +1201,28 @@ async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webxdc_manifest() -> Result<()> {
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());
let result = parse_webxdc_manifest(br#"key = syntax error"#);
assert!(result.is_err());
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?;
let manifest = parse_webxdc_manifest(br#"no_name = "no name, no icon""#)?;
assert_eq!(manifest.name, None);
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?;
let manifest = parse_webxdc_manifest(br#"name = "name, no icon""#)?;
assert_eq!(manifest.name, Some("name, no icon".to_string()));
let manifest = parse_webxdc_manifest(
r#"name = "foo"
icon = "bar""#
.as_bytes(),
br#"name = "foo"
icon = "bar""#,
)?;
assert_eq!(manifest.name, Some("foo".to_string()));
let manifest = parse_webxdc_manifest(
r#"name = "foz"
br#"name = "foz"
icon = "baz"
add_item = "that should be just ignored"
[section]
sth_for_the = "future""#
.as_bytes(),
sth_for_the = "future""#,
)?;
assert_eq!(manifest.name, Some("foz".to_string()));
Ok(())
@@ -1232,13 +1230,13 @@ sth_for_the = "future""#
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?;
let manifest = parse_webxdc_manifest(br#"min_api = 3"#)?;
assert_eq!(manifest.min_api, Some(3));
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes());
let result = parse_webxdc_manifest(br#"min_api = "1""#);
assert!(result.is_err());
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes());
let result = parse_webxdc_manifest(br#"min_api = 1.2"#);
assert!(result.is_err());
Ok(())
@@ -1246,10 +1244,10 @@ async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes());
let result = parse_webxdc_manifest(br#"source_code_url = 3"#);
assert!(result.is_err());
let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?;
let manifest = parse_webxdc_manifest(br#"source_code_url = "https://foo.bar""#)?;
assert_eq!(
manifest.source_code_url,
Some("https://foo.bar".to_string())
@@ -1666,7 +1664,6 @@ async fn test_webxdc_chatlist_summary() -> Result<()> {
assert_eq!(chatlist.len(), 1);
let summary = chatlist.get_summary(&t, 0, None).await?;
assert_eq!(summary.text, "📱 nice app!".to_string());
assert_eq!(summary.thumbnail_path.unwrap(), "webxdc-icon://last-msg-id");
Ok(())
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB