Compare commits

...
Author SHA1 Message Date
link2xt eaf461707b fix(deltachat-rpc-server): always run with at least two threads
We already run all async tests with
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
as we had problems in the past when running them with one thread.

This is a similar change for deltachat-rpc-server
that hopefully prevents timeouts of deltachat-rpc-client tests
on CI.
2023-10-28 00:08:40 +00:00
link2xt 9be56a5e56 test(deltachat-rpc-client): test securejoin 2023-10-27 20:07:32 +00:00
link2xt da744958c2 chore: move pytest option from pyproject.toml to tox.ini and set log level
The option from pyproject.toml was not picked up
because tox.ini has higher precedence.
2023-10-27 20:00:20 +00:00
link2xt f6bda1e480 docs: add missing 1.127.1 link to changelog 2023-10-27 19:34:49 +00:00
link2xt d2e24534c7 chore(release): prepare for 1.127.1 2023-10-27 19:27:40 +00:00
link2xt df6f974eca Add scripts for running deltachat-rpc-client tests 2023-10-27 17:24:39 +00:00
link2xt 2f5c6b5e16 test(jsonrpc): test get_provider_info 2023-10-27 03:07:47 +00:00
link2xt 97176b13f1 api(jsonrpc): add id to ProviderInfo 2023-10-27 03:07:18 +00:00
link2xt 18bb7e58be refactor: move api/mod.rs to api.rs 2023-10-27 02:33:17 +00:00
Simon Laux c2bab44bdd api: jsonrpc add .is_protection_broken to FullChat and BasicChat 2023-10-27 00:03:12 +00:00
link2xt 53bb8a9831 chore: update to async-channel 2 2023-10-26 23:41:34 +00:00
link2xt 1b66120e7d chore(release): prepare for 1.127.0 2023-10-26 15:54:48 +00:00
link2xt 1478f321ae fix: restore try_many_times workaround
Even though r2d2 connection pool is removed,
deleting accounts still fails in Windows CI.

This reverts commit e88f21c010.
`try_many_times` documentation is modified to explain
why the workaround is still needed.
2023-10-26 15:15:44 +00:00
link2xt 5fb92c78ad ci: test deltachat-rpc-client on Windows 2023-10-26 15:15:44 +00:00
link2xt a0a792b821 chore: update sct, serde and serde_derive 2023-10-26 14:06:44 +00:00
link2xt 3feb0e648d build: switch to iroh 0.4.x fork with updated dependencies 2023-10-26 14:04:32 +00:00
link2xt fa5358a5bf chore: update tracing 2023-10-26 13:17:54 +00:00
Sebastian Klähn 7399a398a7 api: add mailto parse api (#4829)
close #4620 

This PR introduces a new core API to parse mailto links into a uniform
data format. This could be used to unify the different implementations
on the current platforms.
To complete this PR we have to decide for which APIs we want to expose
this (now) internal API (c, python, json-rpc, etc.), and if we want such
an API at all as it doesn't have a corresponding UI-PR and is not
_really_ needed.
2023-10-26 11:46:51 +02:00
link2xt 25a78aceb9 ci: increase MSRV to 1.70.0
This is required by `anstyle v1.0.4` dependency.
2023-10-26 02:16:10 +00:00
link2xt 66708454dd chore: update dependencies 2023-10-26 02:13:15 +00:00
link2xt bb5e3d11d8 chore: update futures-lite dependency 2023-10-26 01:11:11 +00:00
link2xt ff54db2e5f test: adapt the test for updated chrono 2023-10-26 00:47:32 +00:00
link2xt 434d8fc35f chore: update quick-xml 2023-10-25 22:42:14 +00:00
link2xt 12eb813bc3 ci(concourse): replace master branch with main 2023-10-25 22:02:35 +00:00
link2xt 88d5576150 ci(github): replace references to master branch with main 2023-10-25 22:02:32 +00:00
link2xt af35e4adeb chore: update dependencies 2023-10-25 21:58:13 +00:00
link2xt eaeacb8848 ci: run only on main branch pushes 2023-10-25 21:46:09 +00:00
link2xt f00e68e142 chore: update some dependencies 2023-10-25 21:45:22 +00:00
link2xt 113356a24e docs: fix CI badge URL in the readme 2023-10-25 19:31:22 +00:00
link2xt b89c134e7f Merge branch 'master' into stable 2023-10-25 16:50:18 +00:00
link2xt 3748794048 fix(sql): order migrations the same as on stable branch 2023-10-25 16:37:39 +00:00
iequidoo ccca12176e feat: Replace Config::SendSyncMsgs with SyncMsgs (#4817)
And execute sync messages only if `Config::SyncMsgs` is enabled. Earlier executing was always
enabled, the messages are force-encrypted anyway. But for users it's probably more clear whether a
device is synchronised or not.
2023-10-25 04:47:37 -03:00
dependabot[bot] c89dd331f7 chore(cargo): bump libc from 0.2.147 to 0.2.149
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.147 to 0.2.149.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.147...0.2.149)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 03:10:25 +00:00
link2xt fa81ed5f39 ci: rename async tests into JSON-RPC tests 2023-10-25 01:33:56 +00:00
link2xt 6c34f6b8d9 ci: remove misplaced comment 2023-10-25 01:33:56 +00:00
dependabot[bot] 4f21a5691d chore(cargo): bump tokio from 1.29.1 to 1.33.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.1 to 1.33.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.1...tokio-1.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 00:56:33 +00:00
dependabot[bot] b2a839971b chore(cargo): bump strum_macros from 0.25.1 to 0.25.3
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.25.1 to 0.25.3.
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 23:01:25 +00:00
link2xt 8ad99be322 ci: increase MSRV to 1.67.0 2023-10-24 21:18:15 +00:00
link2xt 4e771e8727 Merge branch 'stable' 2023-10-24 17:49:23 +00:00
Sebastian Klähn e725bdfb2b feat: add bot field to contact (#4821)
closes #4647
2023-10-24 17:42:29 +00:00
link2xt ac557f73b3 chore: remove unneeded idna rule for cargo-deny 2023-10-24 17:02:16 +00:00
link2xt 0ba3501a46 chore: update trust-dns-resolver to hickory-resolver 2023-10-24 17:00:14 +00:00
B. Petersen b1fe12881e document configured_addr
in fact, it was already used like that,
eg. to detect an AEAP change on android.
but it was never documented officially.
2023-10-24 17:59:19 +02:00
link2xt c1eb33c0da Merge tag 'v1.126.1'
Release 1.126.1
2023-10-24 14:50:49 +00:00
link2xt 03bb92c942 chore(release): prepare for 1.126.1 2023-10-24 14:30:02 +00:00
link2xt b0da5a54cc chore(cargo): update ahash to make cargo-deny happy 2023-10-23 21:56:14 +00:00
link2xt 44e056e210 ci: build Windows wheels for deltachat-rpc-server 2023-10-23 20:38:58 +00:00
link2xt 48a8680ba4 build: set 755 permission on binaries inside deltachat-rpc-server .whl 2023-10-23 20:20:28 +00:00
link2xt b73bcc2c22 ci: build macOS wheels for deltachat-rpc-server 2023-10-23 20:20:14 +00:00
link2xt cff42936aa ci: add missing .exe suffix to windows deltachat-rpc-server paths 2023-10-23 20:19:35 +00:00
link2xt d3b04004b4 feat(imap): buffer STARTTLS command
Using BufWriter ensures that `STARTTLS` command is sent
as a single packet.

Also refactor the code to ensure we only convert to
Box<dyn SessionStream> in the end.
2023-10-23 16:26:29 +00:00
link2xt 5cd92f10ef ci: build deltachat-rpc-server binaries for aarch64 macOS 2023-10-23 16:02:36 +00:00
link2xt 22a3ab983b refactor: download messages without jobs 2023-10-23 11:52:47 +00:00
link2xt 83d2e6b8b4 fix: do not interrupt IMAP loop from get_connectivity_html()
Android calls get_connectivity_html()
every time connectivity changes, which in turn interrupts
IMAP loop and triggers change from "not connected" to "connecting"
state.

To avoid such infinite loop of IMAP interrupts when
there is not connectivity, update quota only when IMAP
loop is interrupted otherwise. This anyway happens
when a message is received or maybe_network is called.

Also remove outdated comments about `Action::UpdateRecentQuota` job
which does not exist anymore.
2023-10-22 18:48:14 +00:00
link2xt 4e979c5880 build: make source package for deltachat-rpc-server install fixed version 2023-10-22 17:49:03 +00:00
link2xt 71e1089139 fix: do not hardcode version in deltachat-rpc-server source package 2023-10-22 16:46:26 +00:00
link2xt 349c154a99 ci: attempt to fix artifacts download for deltachat-rpc-server
Had to do this step manually for 1.126.0,
hopefully the step will work automatically for 1.127.0.
2023-10-22 15:24:02 +00:00
link2xt 54410dbe49 Merge tag 'v1.126.0' 2023-10-22 15:16:11 +00:00
link2xt 4e08bb7b05 chore(release): prepare for 1.126.0 2023-10-22 14:01:07 +00:00
Asiel Díaz Benítez 934ca6a7d7 api: add send_draft() to JSON-RPC API (#4839) 2023-10-22 13:14:08 +00:00
link2xt e878caebe3 ci: build Python wheels for deltachat-rpc-server 2023-10-22 13:13:04 +00:00
link2xt 088eda2983 build(scripts/zig-rpc-server.sh): move built binaries to dist/ 2023-10-22 13:13:04 +00:00
link2xt 418cd24979 build: strip release binaries
This significantly reduces binary size for deltachat-rpc-server,
from 42 MiB to 15 MiB in case of aarch64.
2023-10-22 13:13:04 +00:00
link2xt b4fe9e3eec build: use Zig via ziglang PyPI package
This avoids cluttering workdir with unpacked tarball
and will work automatically once PEP-723 is implemented.
2023-10-22 13:13:04 +00:00
link2xt c13bbd05cd chore: add dist/ to .gitignore 2023-10-22 13:13:04 +00:00
link2xt 58330fe8b2 ci: remove musl check
It requires installing Zig and slows down CI.
This was not broken for a while and we can do
more frequent core releases to catch problems.
2023-10-22 13:13:04 +00:00
link2xt 680d024b05 docs: document scripts/codespell.sh 2023-10-22 06:53:44 +00:00
link2xt defcd5764b chore: spellcheck 2023-10-22 06:53:26 +00:00
link2xt e87f785a0a test: adjust expected info message in test_verified_group_vs_delete_server_after
Test was written for stable branch
and has to be adjusted for verified 1:1 chats branch
2023-10-21 04:18:39 +00:00
link2xt 0227bbc305 fix(imap): fallback to STATUS if SELECT did not return UIDNEXT
Winmail Pro Mail Server 5.1.0616 does not return UIDNEXT
in response to SELECT, but returns it when explicitly requested
via STATUS command:

    ? SELECT INBOX
    * FLAGS (\Draft \Answered \Flagged \Deleted \Seen \Recent)
    * OK [PERMANENTFLAGS (\Draft \Answered \Flagged \Deleted \Seen)] Limited
    * 2 EXISTS
    * 0 RECENT
    * OK [UIDVALIDITY 1697802109] Ok
    ? OK [READ-WRITE] Ok SELECT completed
    ? STATUS INBOX (UIDNEXT)
    * STATUS "INBOX" (UIDNEXT 4)
    ? OK STATUS completed

Previously used FETCH method is reported to fail for some users,
the FETCH command sometimes returns no results.
Besides, there is no guarantee that the message with
the highest sequence number has the highest UID.

In the worst case if STATUS does not return UIDNEXT
in response to explicit request, we fall back to setting
UIDNEXT to 1 instead of returning an error.
2023-10-20 22:29:44 +00:00
link2xt d05afec289 chore(cargo): update async-imap to fix STATUS command 2023-10-20 22:29:44 +00:00
link2xt 64035d3ecb fix: set soft_heap_limit on SQLite database
This should prevent unlimited growth of memory usage
by SQLite for is page cache.
2023-10-20 05:18:21 +00:00
link2xt 21e0bb28ad build: create source distribution for deltachat-rpc-server 2023-10-19 14:38:03 +00:00
link2xt c6358169ad fix: s/env/venv/ in scripts/make-python-testenv.sh 2023-10-18 20:10:51 +00:00
Asiel Díaz BenítezandB. Petersen 955f4fbb19 add self-address to backup filename (#4820)
close #4816

---------

Co-authored-by: B. Petersen <r10s@b44t.com>
2023-10-18 12:31:32 -04:00
adbenitez df7c44ae42 fix: wrong type hint 2023-10-17 17:18:25 +02:00
Hocuri 8573649bf7 feat: Make broadcast lists create their own chat (#4644)
feat: Make broadcast lists create their own chat - UIs need to ask for
the name when creating broadcast lists now (see
https://github.com/deltachat/deltachat-android/pull/2653)

That's quite a minimal approach: Add a List-ID header to outgoing
broadcast lists, so that the receiving Delta Chat shows them as a
separate chat, as talked about with @r10s and @hpk42.

Done:
- [x] Fix an existing bug that the chat name isn't updated when the
broadcast/mailing list name changes (I already started this locally)

To be done in other PRs:
- [ ] Right now the receiving side shows "Mailing list" in the subtitle
of such a chat, it would be nicer if it showed "Broadcast list" (or
alternatively, rename "Broadcast list" to "Mailing list", too)
- [ ] The UIs should probably ask for a name before creating the
broadcast list, since it will actually be sent over the wire. (Android
PR: https://github.com/deltachat/deltachat-android/pull/2653)

Fixes https://github.com/deltachat/deltachat-core-rust/issues/4597

BREAKING CHANGE: This means that UIs need to ask for the name when creating a broadcast list, similar to https://github.com/deltachat/deltachat-android/pull/2653.
2023-10-17 10:40:47 +02:00
link2xt 52c46c6dca build: add script to build deltachat-rpc-server wheels 2023-10-16 17:04:55 +00:00
link2xt 54ea3ec5d6 build: workaround OpenSSL crate expecting libatomic to be available 2023-10-15 22:57:46 +00:00
Sebastian Klähn 1632035784 feat: add bot field to contact (#4821)
closes #4647
2023-10-15 12:40:32 +02:00
Sebastian Klähn b239535964 api: allow to filter by unread in chatlist:try_load (#4824)
close #4738
2023-10-14 11:12:53 +02:00
Sebastian KlähnandSeptias 0751cc50b9 api(json-rpc): force stickers to be sent as stickers (#4819)
This approach uses a param field to enable forcing the sticker
`viewtype`. The first commit has the memory-only flag implemented, but
this flag is not persistent through the database conversion needed for
draft/undraft. That's why `param` has to be used.

follow up to #4814 
fixes #4739

---------

Co-authored-by: Septias <scoreplayer2000@gmail.comclear>
2023-10-14 08:34:46 +00:00
link2xt da5d844ec4 chore: rustfmt 2023-10-14 04:25:13 +00:00
link2xt 2775fd1fcf Merge tag 'v1.125.0'
Release 1.125.0
2023-10-14 04:24:08 +00:00
link2xt a87635dcf4 chore(release): prepare for 1.125.0 2023-10-14 04:21:48 +00:00
link2xt e30517e62c refactor: log MDN sending errors 2023-10-14 03:29:20 +00:00
iequidoo a54f3c4b31 fix: Don't try to send more MDNs if there's a tmp SMTP error (#4534)
If there's a temporary SMTP error, pretend there are no more MDNs to send in send_mdn(). It's
unlikely that other MDNs could be sent successfully in case of connectivity problems. This approach
is simpler and perhaps even better than adding a progressive backoff between MDN sending retries --
MDNs just will be resent after a reconnection, which makes some sense.
2023-10-12 19:19:43 -03:00
iequidoo bda6cea0ce feat: Make gossip period configurable (#4346)
This is needed to test periodic re-gossiping in existing chats.

Also add a test for verified groups on that even if "member added" message is missed by a device of
newly added member, after re-gossiping Autocrypt keys to the group it successfully learns these keys
and marks other members as verified.
2023-10-12 05:45:20 -03:00
iequidoo 6fece09ed7 test: test_qr_new_group_unblocked(): W/a message reordering on server
There was a recent failure of the test probably as a result of message reordering on the server:
https://github.com/deltachat/deltachat-core-rust/actions/runs/6464605602/job/17549624095?pr=4813.
2023-10-11 04:54:03 -03:00
link2xt 3917c6b2f0 test(deltachat-rpc-client): enable logs in pytest
This makes pytest setup a logger for `logging` module.
2023-10-10 19:20:42 +00:00
link2xt 96a89b5bdc fix: return verifier contacts regardless of their origin
Previously `Origin::AddressBook` was required,
resulting in a lot of
"Could not lookup contact with address ... which introduced ..."
warnings.
2023-10-10 19:20:22 +00:00
link2xt b55027fe71 fix: set connectivity status to "connected" during fake idle
Set connectivity status to "connected" at the end of connect() call.
Otherwise for servers that do not support IMAP IDLE
connect() is called at the beginning of fake idle
and connectivity stays in "connecting" status most of the time.
2023-10-10 05:37:51 +00:00
link2xt eacbb82399 feat: add developer option to disable IDLE 2023-10-10 00:52:18 +00:00
Sebastian Klähn ee279f84ad fix: show all contacts in Contact::get_all for bots (#4811)
successor of #4810
2023-10-09 21:02:19 +02:00
link2xt 26959d5b75 test(python): fix flaky test_set_get_group_image
Wait for one "Member added" message to be delivered
before sending another text message.
Otherwise they may be reordered by the mail server.
2023-10-09 12:48:33 +00:00
link2xt ff5005fa93 fix(python): fix scripts/make-python-testenv.sh
Without `-c python` tox does not find tox.ini and creates empty environment.

Renamed env/ into venv/ as it is more common.
2023-10-09 12:43:22 +00:00
iequidoo 8f316e12d5 fix: Assign encrypted partially downloaded group messages to 1:1 chat (#4757)
Before they were trashed. Note that for unencrypted ones DC works as expected creating the requested
group immediately because Chat-Group-Id is duplicated in the Message-Id header and Subject is
fetched.
2023-10-09 05:45:18 -03:00
iequidoo 5f00fc4e27 fix: Don't update timestamp, timestamp_rcvd, state when replacing partially downloaded message (#4700)
Also add a test on downloading a message later. Although it doesn't reproduce #4700 for some reason,
it fails w/o the fix because before a message state was changing to `InSeen` after a full download
which doesn't look correct. The result of a full message download should be such as if it was fully
downloaded initially.
2023-10-09 05:45:18 -03:00
link2xt f279730b0f feat: validate boolean values passed to set_config
They may only be set to "0" and "1".
Validation prevents accidentally setting the value to "true", "True" etc.
2023-10-08 23:15:49 +00:00
missytake 5a5f8b03d1 fix(python): don't automatically set the displayname to 'bot' when setting log level 2023-10-08 17:38:57 +00:00
missytake 5e73e9cd72 chore: added more typical virtualenv paths to gitignore 2023-10-08 17:38:57 +00:00
link2xt 129de9182f chore(deltachat-rpc-client): remove AsyncIO classifier 2023-10-08 01:38:52 +00:00
79 changed files with 2219 additions and 1614 deletions
+18 -14
View File
@@ -14,8 +14,7 @@ on:
pull_request:
push:
branches:
- master
- stable
- main
env:
RUSTFLAGS: -Dwarnings
@@ -39,10 +38,6 @@ jobs:
- name: Check
run: cargo check --workspace --all-targets --all-features
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
- name: Check musl
run: scripts/zig-musl-check.sh
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
@@ -87,9 +82,9 @@ jobs:
- os: macos-latest
rust: 1.73.0
# Minimum Supported Rust Version = 1.67.0
# Minimum Supported Rust Version = 1.70.0
- os: ubuntu-latest
rust: 1.67.0
rust: 1.70.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -135,7 +130,7 @@ jobs:
name: Build deltachat-rpc-server
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -150,7 +145,7 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug/deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
retention-days: 1
python_lint:
@@ -171,8 +166,8 @@ jobs:
working-directory: deltachat-rpc-client
run: tox -e lint
python_tests:
name: Python tests
cffi_python_tests:
name: CFFI Python tests
needs: ["c_library", "python_lint"]
strategy:
fail-fast: false
@@ -222,8 +217,8 @@ jobs:
working-directory: python
run: tox -e mypy,doc,py
aysnc_python_tests:
name: Async Python tests
rpc_python_tests:
name: JSON-RPC Python tests
needs: ["python_lint", "rpc_server"]
strategy:
fail-fast: false
@@ -233,6 +228,8 @@ jobs:
python: 3.12
- os: macos-latest
python: 3.12
- os: windows-latest
python: 3.12
# PyPy tests
- os: ubuntu-latest
@@ -263,11 +260,18 @@ jobs:
path: target/debug
- name: Make deltachat-rpc-server executable
if: ${{ matrix.os != 'windows-latest' }}
run: chmod +x target/debug/deltachat-rpc-server
- name: Add deltachat-rpc-server to path
if: ${{ matrix.os != 'windows-latest' }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: Add deltachat-rpc-server to path
if: ${{ matrix.os == 'windows-latest' }}
run: |
"${{ github.workspace }}/target/debug" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Run deltachat-rpc-client tests
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
+67 -37
View File
@@ -26,35 +26,17 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Build
- name: Install ziglang
run: pip install wheel ziglang==0.11.0
- name: Build deltachat-rpc-server binaries
run: sh scripts/zig-rpc-server.sh
- name: Upload x86_64 binary
- name: Upload dist directory with Linux binaries
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-x86_64
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload i686 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-i686
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload aarch64 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-aarch64
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload armv7 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-armv7
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
name: linux
path: dist/
if-no-files-found: error
build_windows:
@@ -92,39 +74,87 @@ jobs:
build_macos:
name: Build deltachat-rpc-server for macOS
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
- arch: aarch64
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add x86_64-apple-darwin
run: rustup target add ${{ matrix.arch }}-apple-darwin
- name: Build
run: cargo build --release --package deltachat-rpc-server --target x86_64-apple-darwin --features vendored
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
if-no-files-found: error
publish:
name: Upload binaries to the release
name: Build wheels and upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
permissions:
contents: write
runs-on: "ubuntu-latest"
steps:
- name: Download built binaries
uses: "actions/download-artifact@v3"
- uses: actions/checkout@v3
- name: Compose dist/ directory
- name: Download Linux binaries
uses: actions/download-artifact@v3
with:
name: linux
path: dist/
- name: Download win32 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-win32.exe
path: deltachat-rpc-server-win32.exe.d
- name: Download win64 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-win64.exe
path: deltachat-rpc-server-win64.exe.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Flatten dist/ directory
run: |
mkdir dist
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
done
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install wheel
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
run: scripts/wheel-rpc-server.py
- name: List downloaded artifacts
run: ls -l dist/
+2 -2
View File
@@ -2,9 +2,9 @@ name: JSON-RPC API Test
on:
push:
branches: [master]
branches: [main]
pull_request:
branches: [master]
branches: [main]
env:
CARGO_TERM_COLOR: always
+1 -1
View File
@@ -8,7 +8,7 @@ name: Generate & upload node.js documentation
on:
push:
branches:
- master
- main
jobs:
generate:
+1 -1
View File
@@ -13,7 +13,7 @@ on:
pull_request:
push:
branches:
- master
- main
jobs:
tests:
+1 -2
View File
@@ -3,8 +3,7 @@ name: Build & Deploy Documentation on rs.delta.chat
on:
push:
branches:
- master
- docs-gh-action
- main
jobs:
build:
+1 -2
View File
@@ -7,8 +7,7 @@ name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- master
- docs-gh-action
- main
jobs:
build:
+4
View File
@@ -1,6 +1,7 @@
/target
**/*.rs.bk
/build
/dist
# ignore vi temporaries
*~
@@ -18,6 +19,9 @@ python/.eggs
__pycache__
python/src/deltachat/capi*.so
python/.venv/
python/venv/
venv/
env/
python/liveconfig*
+155
View File
@@ -1,5 +1,155 @@
# Changelog
## [1.127.1] - 2023-10-27
### API-Changes
- jsonrpc: add `.is_protection_broken` to `FullChat` and `BasicChat`.
- jsonrpc: Add `id` to `ProviderInfo`.
## [1.127.0] - 2023-10-26
### API-Changes
- [**breaking**] `dc_accounts_new` API is changed. Unused `os_name` argument is removed and `writable` argument is added.
- jsonrpc: Add `resend_messages`.
- [**breaking**] Remove unused function `is_verified_ex()` ([#4551](https://github.com/deltachat/deltachat-core-rust/pull/4551))
- [**breaking**] Make `MsgId.delete_from_db()` private.
- [**breaking**] deltachat-jsonrpc: use `kind` as a tag for all union types
- json-rpc: Force stickers to be sent as stickers ([#4819](https://github.com/deltachat/deltachat-core-rust/pull/4819)).
- Add mailto parse api ([#4829](https://github.com/deltachat/deltachat-core-rust/pull/4829)).
- [**breaking**] Remove unused `DC_STR_PROTECTION_(EN)ABLED` strings
- [**breaking**] Remove unused `dc_set_chat_protection()`
- Hide `DcSecretKey` trait from the API.
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
### CI
- Run Rust tests with `RUST_BACKTRACE` set.
- Replace `master` branch with `main`. Run CI only on `main` branch pushes.
- Test `deltachat-rpc-client` on Windows.
### Documentation
- Document how logs and error messages should be formatted in `CONTRIBUTING.md`.
- Clarify transitive behaviour of `dc_contact_is_verfified()`.
- Document `configured_addr`.
### Features / Changes
- Add lockfile to account manager ([#4314](https://github.com/deltachat/deltachat-core-rust/pull/4314)).
- Don't show a contact as verified if their key changed since the verification ([#4574](https://github.com/deltachat/deltachat-core-rust/pull/4574)).
- deltachat-rpc-server: Add `--openrpc` option to print OpenRPC specification for JSON-RPC API. This specification can be used to generate JSON-RPC API clients.
- Track whether contact is a bot or not ([#4821](https://github.com/deltachat/deltachat-core-rust/pull/4821)).
- Replace `Config::SendSyncMsgs` with `SyncMsgs` ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
### Fixes
- Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt ([#4538](https://github.com/deltachat/deltachat-core-rust/pull/4538)).
- Allow to save a draft if the verification is broken ([#4542](https://github.com/deltachat/deltachat-core-rust/pull/4542)).
- Fix info-message orderings of verified 1:1 chats ([#4545](https://github.com/deltachat/deltachat-core-rust/pull/4545)).
- Fix example; this was changed some time ago, see https://docs.webxdc.org/spec.html#sendupdate
- `receive_imf`: Update peerstate from db after handling Securejoin handshake ([#4600](https://github.com/deltachat/deltachat-core-rust/pull/4600)).
- Sort old incoming messages below all outgoing ones ([#4621](https://github.com/deltachat/deltachat-core-rust/pull/4621)).
- Do not mark non-verified group chats as verified when using securejoin.
- `receive_imf`: Set protection only for Chattype::Single ([#4597](https://github.com/deltachat/deltachat-core-rust/pull/4597)).
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
- Clear VerifiedOneOnOneChats config on backup ([#4615](https://github.com/deltachat/deltachat-core-rust/pull/4615)).
- Try removal of accounts multiple times with timeouts in case the database file is blocked (restore `try_many_times` workaround).
### Build system
- Remove examples/simple.rs.
- Increase MSRV to 1.70.0.
- Update dependencies.
- Switch to iroh 0.4.x fork with updated dependencies.
## [1.126.1] - 2023-10-24
### Fixes
- Do not hardcode version in deltachat-rpc-server source package.
- Do not interrupt IMAP loop from `get_connectivity_html()`.
### Features / Changes
- imap: Buffer `STARTTLS` command.
### Build system
- Build `deltachat-rpc-server` binary for aarch64 macOS.
- Build `deltachat-rpc-server` wheels for macOS and Windows.
### Refactor
- Remove job queue.
### Miscellaneous Tasks
- cargo: Update `ahash` to make `cargo-deny` happy.
## [1.126.0] - 2023-10-22
### API-Changes
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
### Features / Changes
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
### CI
- Build Python wheels for deltachat-rpc-server.
### Build system
- Strip release binaries.
- Workaround OpenSSL crate expecting libatomic to be available.
### Fixes
- Set `soft_heap_limit` on SQLite database.
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
## [1.125.0] - 2023-10-14
### API-Changes
- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads.
- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error.
### CI
- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7.
### Features / Changes
- Add developer option to disable IDLE.
### Fixes
- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`.
- python: Don't automatically set the displayname to "bot" when setting log level.
- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)).
- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)).
- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)).
- Set connectivity status to "connected" during fake idle.
- Return verifier contacts regardless of their origin.
- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)).
### Refactor
- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`.
- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead.
### Tests
- deltachat-rpc-client: Enable logs in pytest.
## [1.124.1] - 2023-10-05
### Fixes
@@ -2879,3 +3029,8 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
[1.126.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.0...v1.126.1
[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0
[1.127.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.0...v1.127.1
Generated
+537 -561
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.124.1"
version = "1.127.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.67"
rust-version = "1.70"
[profile.dev]
debug = 0
@@ -24,10 +24,7 @@ lto = true
panic = 'abort'
opt-level = "z"
codegen-units = 1
[patch.crates-io]
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -35,7 +32,7 @@ format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
async-channel = "2.0.0"
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
@@ -50,11 +47,12 @@ escaper = "0.1"
fast-socks5 = "0.8"
fd-lock = "3.0.11"
futures = "0.3"
futures-lite = "1.13.0"
futures-lite = "2.0.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.1", default-features = false }
iroh = { git = "https://github.com/deltachat/iroh", branch = "0.4-update-quic", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
@@ -67,9 +65,10 @@ once_cell = "1.18.0"
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.30"
quick-xml = "0.31"
rand = "0.8"
regex = "1.9"
reqwest = { version = "0.11.20", features = ["json"] }
@@ -92,14 +91,13 @@ tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.9"
toml = "0.7"
trust-dns-resolver = "0.23"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = "1.13"
futures-lite = "2.0.0"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
+2 -2
View File
@@ -3,8 +3,8 @@
</p>
<p align="center">
<a href="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml">
<img alt="Rust CI" src="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml/badge.svg">
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
</a>
</p>
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.124.1"
version = "1.127.1"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+21 -22
View File
@@ -384,7 +384,12 @@ char* dc_get_blobdir (const dc_context_t* context);
/**
* Configure the context. The configuration is handled by key=value pairs as:
*
* - `addr` = address to display (always needed)
* - `addr` = Email address to use for configuration.
* If dc_configure() fails this is not the email address actually in use.
* Use `configured_addr` to find out the email address actually in use.
* - `configured_addr` = Email address actually in use.
* Unless for testing, do not set this value using dc_set_config().
* Instead, set `addr` and call dc_configure().
* - `mail_server` = IMAP-server, guessed if left out
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
@@ -492,6 +497,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
* 0=use IMAP IDLE if the server supports it.
* This is a developer option used for testing polling used as an IDLE fallback.
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
* For larger messages, only the header is downloaded and a placeholder is shown.
* These messages can be downloaded fully using dc_download_full_msg() later.
@@ -500,6 +508,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
@@ -885,7 +896,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
* is added as needed.
* @param query_str An optional query for filtering the list. Only chats matching this query
* are returned. Give NULL for no filtering.
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
* the chatlist is filtered such that only chats with unread messages show up.
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
* are returned. Give 0 for no filtering.
* @return A chatlist as an dc_chatlist_t object.
@@ -1700,24 +1712,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
* however, recipients get the messages in a read-only chat
* and will see who the other members are.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
* For historical reasons, this function does not take a name directly,
* instead you have to set the name using dc_set_chat_name()
* after creating the broadcast list.
*
* @memberof dc_context_t
* @param context The context object.
@@ -2260,8 +2260,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
* the backup is not encrypted.
* The backup contains all contacts, chats, images and other data and device independent settings.
* The backup does not contain device dependent settings as ringtones or LED notification settings.
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
* the format is `delta-chat-<day>-<number>.tar`
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
*
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
@@ -3967,7 +3966,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
* Get the message time used for sorting.
* This function returns the timestamp that is used for sorting the message
* into lists as returned e.g. by dc_get_chat_msgs().
* This may be the reveived time, the sending time or another time.
* This may be the received time, the sending time or another time.
*
* To get the receiving time, use dc_msg_get_received_timestamp().
* To get the sending time, use dc_msg_get_timestamp().
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.124.1"
version = "1.127.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -19,12 +19,12 @@ schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.8.0"
log = "0.4"
async-channel = { version = "1.8.0" }
async-channel = { version = "2.0.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.105"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.32.0" }
tokio = { version = "1.33.0" }
sanitize-filename = "0.5"
walkdir = "2.3.3"
base64 = "0.21"
@@ -34,7 +34,7 @@ axum = { version = "0.6.20", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
[features]
@@ -815,24 +815,12 @@ impl CommandApi {
/// Create a new broadcast list.
///
/// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in normal one-to-one chats
/// and will not be aware of other members.
/// however, recipients get the messages in a read-only chat
/// and will see who the other members are.
///
/// Replies to broadcasts go only to the sender
/// and not to all broadcast recipients.
/// Moreover, replies will not appear in the broadcast list
/// but in the one-to-one chat with the person answering.
///
/// The name and the image of the broadcast list is set automatically
/// and is visible to the sender only.
/// Not asking for these data allows more focused creation
/// and we bypass the question who will get which data.
/// Also, many users will have at most one broadcast list
/// so, a generic name and image is sufficient at the first place.
///
/// Later on, however, the name can be changed using dc_set_chat_name().
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
/// All in all, this is also what other messengers are doing here.
/// For historical reasons, this function does not take a name directly,
/// instead you have to set the name using dc_set_chat_name()
/// after creating the broadcast list.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
@@ -1756,6 +1744,9 @@ impl CommandApi {
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(&sticker_path, None);
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
msg.force_sticker();
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
@@ -2068,6 +2059,23 @@ impl CommandApi {
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
}
// send the chat's current set draft
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
let mut draft = draft;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
.await?
.to_u32();
Ok(msg_id)
} else {
Err(anyhow!(
"chat with id {} doesn't have draft message",
chat_id
))
}
}
}
// Helper functions (to prevent code duplication)
+4
View File
@@ -31,6 +31,7 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
@@ -100,6 +101,7 @@ impl FullChat {
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
@@ -134,6 +136,7 @@ pub struct BasicChat {
is_self_talk: bool,
color: String,
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}
@@ -160,6 +163,7 @@ impl BasicChat {
is_self_talk: chat.is_self_talk(),
color,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(),
})
@@ -6,6 +6,8 @@ use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
/// Unique ID, corresponding to provider database filename.
pub id: String,
pub before_login_hint: String,
pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
@@ -14,6 +16,7 @@ pub struct ProviderInfo {
impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo {
id: p.id.to_owned(),
before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(),
+10 -10
View File
@@ -17,7 +17,7 @@ mod tests {
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (sender, receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
@@ -36,17 +36,17 @@ mod tests {
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
let result = receiver.recv().await?;
println!("{result:?}");
assert_eq!(result, Some(response.to_owned()));
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
let result = receiver.recv().await?;
println!("{result:?}");
assert_eq!(result, Some(response.to_owned()));
assert_eq!(result, response.to_owned());
}
Ok(())
@@ -59,7 +59,7 @@ mod tests {
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (sender, receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
@@ -78,15 +78,15 @@ mod tests {
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
let result = receiver.recv().await?;
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
let result = receiver.recv().await?;
assert_eq!(result, response.to_owned());
}
Ok(())
+1 -1
View File
@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.124.1"
"version": "1.127.1"
}
+1 -1
View File
@@ -148,7 +148,7 @@ describe("online tests", function () {
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arives at A and if it is encrypted
// Check if answer arrives at A and if it is encrypted
await eventPromise2;
const messageId = (
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.124.1"
version = "1.127.1"
license = "MPL-2.0"
edition = "2021"
-1
View File
@@ -7,7 +7,6 @@ name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: POSIX :: Linux",
@@ -3,7 +3,6 @@ import logging
from typing import (
TYPE_CHECKING,
Callable,
Coroutine,
Dict,
Iterable,
Optional,
@@ -92,7 +91,7 @@ class Client:
"""Process events forever."""
self.run_until(lambda _: False)
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
"""Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the
@@ -358,3 +358,34 @@ def test_openrpc_command_line() -> None:
openrpc = json.loads(out)
assert "openrpc" in openrpc
assert "methods" in openrpc
def test_provider_info(rpc) -> None:
account_id = rpc.add_account()
provider_info = rpc.get_provider_info(account_id, "example.org")
assert provider_info["id"] == "example.com"
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
assert provider_info is None
# Test MX record resolution.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_qr_setup_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
return
+2
View File
@@ -29,3 +29,5 @@ commands =
[pytest]
timeout = 60
log_cli = true
log_level = info
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.124.1"
version = "1.127.1"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,11 +15,12 @@ deltachat = { path = "..", default-features = false }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
futures-lite = "2.0.0"
log = "0.4"
num_cpus = "1"
serde_json = "1.0.105"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.32.0", features = ["io-std"] }
tokio = { version = "1.33.0", features = ["io-std"] }
tokio-util = "0.7.9"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
+12 -3
View File
@@ -20,9 +20,18 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let r = main_impl().await;
fn main() {
// Build multithreaded runtime with at least two threads.
// This ensures that on systems with one CPU
// such as CI runners there are at least two threads
// and it is more difficult to deadlock.
let r = tokio::runtime::Builder::new_multi_thread()
.worker_threads(std::cmp::max(2, num_cpus::get()))
.enable_all()
.build()
.unwrap()
.block_on(main_impl());
// From tokio documentation:
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
+4 -6
View File
@@ -11,6 +11,7 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
@@ -24,7 +25,7 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "indexmap", version = "<2.0.0" },
@@ -36,6 +37,7 @@ skip = [
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "ring", version = "0.16.20" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
@@ -45,15 +47,12 @@ skip = [
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
{ name = "windows_aarch64_msvc", version = "<0.48" },
{ name = "windows_i686_gnu", version = "<0.48" },
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
]
@@ -87,5 +86,4 @@ license-files = [
github = [
"async-email",
"deltachat",
"quinn-rs",
]
+1 -1
View File
@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
#### Upsides:
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
+1 -1
View File
@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
}
}
/** Opens a stanalone context (without an account manager)
/** Opens a standalone context (without an account manager)
* automatically starts the event handler */
static open(cwd: string): Context {
const dbFile = join(cwd, 'db.sqlite')
+1 -1
View File
@@ -270,7 +270,7 @@ describe('Basic offline Tests', function () {
'quota_exceeding',
'scan_all_folders_debounce_secs',
'selfavatar',
'send_sync_msgs',
'sync_msgs',
'sentbox_watch',
'show_emails',
'socks5_enabled',
+1 -1
View File
@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.124.1"
"version": "1.127.1"
}
+6 -4
View File
@@ -617,18 +617,18 @@ class Account:
# meta API for start/stop and event based processing
#
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
from .events import FFIEventLogger
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
"""get the account running, configure it if necessary. add plugins if provided.
:param addr: the email address of the account
:param password: the password of the account
:param account_plugins: a list of plugins to add
:param show_ffi: show low level ffi events
:param displayname: the display name of the account
"""
from .events import FFIEventLogger
if show_ffi:
self.set_config("displayname", "bot")
log = FFIEventLogger(self)
self.add_account_plugin(log)
@@ -644,6 +644,8 @@ class Account:
configtracker = self.configure()
configtracker.wait_finish()
if displayname:
self.set_config("displayname", displayname)
# start IO threads and configure if necessary
self.start_io()
+64 -3
View File
@@ -542,8 +542,6 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.text == msg_out.text
assert msg_in.get_sender_contact().addr == ac2_addr
ac1.set_config("bcc_self", "0")
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
"""Another test for the bug #3836:
@@ -592,4 +590,67 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.text == msg_out.text
ac2.set_config("bcc_self", "0")
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
"""Test for the issue #4346:
- User is added to a verified group.
- First device of the user downloads "member added" from the group.
- First device removes "member added" from the server.
- Some new messages are sent to the group.
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
- First device re-gossips Autocrypt keys to the group.
- Now the seconds device has all members verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
ac2.set_config("delete_server_after", "1")
ac2.set_config("gossip_period", "0") # Re-gossip in every message
acfactory.bring_accounts_online()
dir = tmp_path / "exportdir"
dir.mkdir()
ac2.export_self_keys(str(dir))
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
# Wait for "Member Me (<addr>) added by <addr>." message.
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
lp.sec("ac1: sending 'hi' to the group")
ac2.set_config("delete_server_after", "0")
chat1.send_text("hi")
lp.sec("ac2_offl: going online, checking the 'hi' message")
ac2_offl.start_io()
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert not msg_in.is_system_message()
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
ac2_offl_ac1_contact = msg_in.get_sender_contact()
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert ac2_offl_ac1_contact.is_verified()
+5 -5
View File
@@ -1718,12 +1718,10 @@ def test_qr_new_group_unblocked(acfactory, lp):
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
ac1_new_chat.send_text("Hello!")
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
# Receive "Hello!" message.
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
@@ -1946,13 +1944,15 @@ def test_set_get_group_image(acfactory, data, lp):
lp.sec("ac1: add ac2 to promoted group chat")
chat.add_contact(ac2) # sends one message
lp.sec("ac2: wait for receiving member added message from ac1")
msg1 = ac2._evtracker.wait_next_incoming_message()
assert msg1.is_system_message() # Member added
lp.sec("ac1: send a first message to ac2")
chat.send_text("hi") # sends another message
assert chat.is_promoted()
lp.sec("ac2: wait for receiving message from ac1")
msg1 = ac2._evtracker.wait_next_incoming_message()
assert msg1.is_system_message() # Member added
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "hi"
assert msg1.chat.id == msg2.chat.id
+1 -1
View File
@@ -1 +1 @@
2023-10-05
2023-10-27
+13 -4
View File
@@ -10,18 +10,27 @@ and an own build machine.
- `deny.sh` runs `cargo deny` for all Rust code in the project.
- `codespell.sh` spellchecks the source code using `codespell` tool.
- `../.github/workflows` contains jobs run by GitHub Actions.
- `run-python-test.sh` runs CFFI Python tests.
- `run-rpc-test.sh` runs JSON-RPC Python tests.
- `make-python-testenv.sh` creates a local python test development environment with CFFI bindings.
Reusing the same environment is faster than running `run-python-test.sh` which always
recreates environment from scratch and runs additional lints.
- `make-rpc-testenv.sh` creates a local python development environment with JSON-RPC bindings,
i.e. `deltachat-rpc-client` and `deltachat-rpc-server`.
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
- `make-python-testenv.sh` creates or updates local python test development environment.
Reusing the same environment is faster than running `run-python-test.sh` which always
recreates environment from scratch and runs additional lints.
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
- `run_all.sh` builds Python wheels
+2 -2
View File
@@ -3,14 +3,14 @@ resources:
type: git
icon: github
source:
branch: master
branch: main
uri: https://github.com/deltachat/deltachat-core-rust.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: master
branch: main
uri: https://github.com/deltachat/deltachat-core-rust.git
tag_filter: "v*"
+4 -8
View File
@@ -4,8 +4,8 @@
# It rebuilds the core and bindings as needed.
#
# After running the script, you can either
# run `pytest` directly with `env/bin/pytest python/`
# or activate the environment with `. env/bin/activacte`
# run `pytest` directly with `venv/bin/pytest python/`
# or activate the environment with `. venv/bin/activate`
# and run `pytest` from there.
set -euo pipefail
@@ -13,9 +13,5 @@ export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
if test -d env; then
env/bin/pip install -e python --force-reinstall
else
tox -e py --devenv env
env/bin/pip install --upgrade pip
fi
tox -c python -e py --devenv venv
venv/bin/pip install --upgrade pip
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
tox -c deltachat-rpc-client -e py --devenv venv
venv/bin/pip install --upgrade pip
cargo install --path deltachat-rpc-server/ --root "$PWD/venv"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cargo install --path deltachat-rpc-server/ --root "$PWD/venv"
PATH="$PWD/venv/bin:$PATH" tox -c deltachat-rpc-client
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Build Python wheels for deltachat-rpc-server.
Run scripts/zig-rpc-server.sh first."""
from pathlib import Path
from wheel.wheelfile import WheelFile
import tomllib
import tarfile
from io import BytesIO
def metadata_contents(version):
return f"""Metadata-Version: 2.1
Name: deltachat-rpc-server
Version: {version}
Summary: Delta Chat JSON-RPC server
"""
def build_source_package(version):
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
contents = contents.encode()
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
tar_info.mode = 0o644
tar_info.size = len(contents)
pkg.addfile(tar_info, BytesIO(contents))
pack("PKG-INFO", metadata_contents(version))
pack(
"pyproject.toml",
f"""[build-system]
requires = ["setuptools==68.2.2", "pip"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-server"
version = "{version}"
[project.scripts]
deltachat-rpc-server = "deltachat_rpc_server:main"
""",
)
pack(
"setup.py",
f"""
import sys
from setuptools import setup, find_packages
from distutils.cmd import Command
from setuptools.command.install import install
from setuptools.command.build import build
import subprocess
import platform
import tempfile
from zipfile import ZipFile
from pathlib import Path
import shutil
class BuildCommand(build):
def run(self):
tmpdir = tempfile.mkdtemp()
subprocess.run(
[
sys.executable,
"-m",
"pip",
"download",
"--no-input",
"--timeout",
"1000",
"--platform",
"musllinux_1_1_" + platform.machine(),
"--only-binary=:all:",
"deltachat-rpc-server=={version}",
],
cwd=tmpdir,
)
wheel_path = next(Path(tmpdir).glob("*.whl"))
with ZipFile(wheel_path, "r") as wheel:
exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
Path(exe_path).chmod(0o700)
wheel.extract("deltachat_rpc_server/__init__.py", "src")
shutil.rmtree(tmpdir)
return super().run()
setup(
cmdclass={{"build": BuildCommand}},
package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}},
)
""",
)
pack("src/deltachat_rpc_server/__init__.py", "")
def build_wheel(version, binary, tag, windows=False):
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
with WheelFile(filename, "w") as wheel:
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
wheel.write("deltachat-rpc-server/README.md", "deltachat_rpc_server/README.md")
if windows:
wheel.writestr(
"deltachat_rpc_server/__init__.py",
"""import os, sys, subprocess
def main():
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server.exe"), *sys.argv[1:]]
sys.exit(subprocess.call(argv))
""",
)
else:
wheel.writestr(
"deltachat_rpc_server/__init__.py",
"""import os, sys
def main():
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server"), *sys.argv[1:]]
os.execv(argv[0], argv)
""",
)
Path(binary).chmod(0o755)
wheel.write(
binary,
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/METADATA",
metadata_contents(version),
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/WHEEL",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/entry_points.txt",
"[console_scripts]\ndeltachat-rpc-server = deltachat_rpc_server:main",
)
def main():
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
cargo_toml = tomllib.load(f)
version = cargo_toml["package"]["version"]
Path("dist").mkdir(exist_ok=True)
build_source_package(version)
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-linux",
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv7-linux",
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-linux",
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-i686-linux",
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
)
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-macos",
"py3-none-macosx_10_7_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-macos",
"py3-none-macosx_11_0_arm64",
)
build_wheel(
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
)
build_wheel(
version,
"dist/deltachat-rpc-server-win64.exe",
"py3-none-win_amd64",
windows=True,
)
main()
+27 -2
View File
@@ -1,10 +1,20 @@
#!/usr/bin/env python
# /// pyproject
# [run]
# dependencies = [
# "ziglang==0.11.0"
# ]
# ///
import os
import subprocess
import sys
import os
def flag_filter(flag: str) -> bool:
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
if flag == "-latomic":
return False
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
@@ -24,8 +34,23 @@ def main():
else:
zig_cpu_args = []
# Disable atomics and use locks instead in OpenSSL.
# Zig toolchains do not provide atomics.
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
args += ["-DBROKEN_CLANG_ATOMICS"]
subprocess.run(
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
[
sys.executable,
"-m",
"ziglang",
"cc",
"-target",
zig_target,
*zig_cpu_args,
*args,
],
check=True,
)
-28
View File
@@ -1,28 +0,0 @@
#!/bin/sh
#
# Run `cargo check` with musl libc.
# This requires `zig` to compile vendored openssl.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
ZIG_VERSION=0.11.0
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo check --release --target x86_64-unknown-linux-musl -p deltachat_ffi --features jsonrpc
+6 -8
View File
@@ -10,14 +10,6 @@ unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
ZIG_VERSION=0.11.0
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
@@ -50,3 +42,9 @@ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
mkdir -p dist
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux
+1 -1
View File
@@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from <sender name>`.
Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain`
or `mulipart/alternative` containing `text/plain`.
or `multipart/alternative` containing `text/plain`.
The text MAY be divided into a user-text-part and a footer-part using the
line `-- ` (minus, minus, space, lineend).
+36 -4
View File
@@ -1,6 +1,7 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
@@ -155,7 +156,7 @@ impl Accounts {
if let Some(cfg) = self.config.get_account(id) {
let account_path = self.dir.join(cfg.dir);
fs::remove_dir_all(&account_path)
try_many_times(|| fs::remove_dir_all(&account_path))
.await
.context("failed to remove account data")?;
}
@@ -191,10 +192,10 @@ impl Accounts {
fs::create_dir_all(self.dir.join(&account_config.dir))
.await
.context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile)
try_many_times(|| fs::rename(&dbfile, &new_dbfile))
.await
.context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir)
try_many_times(|| fs::rename(&blobdir, &new_blobdir))
.await
.context("failed to rename blobdir")?;
if walfile.exists() {
@@ -219,7 +220,7 @@ impl Accounts {
}
Err(err) => {
let account_path = std::path::PathBuf::from(&account_config.dir);
fs::remove_dir_all(&account_path)
try_many_times(|| fs::remove_dir_all(&account_path))
.await
.context("failed to remove account data")?;
self.config.remove_account(account_config.id).await?;
@@ -560,6 +561,37 @@ impl Config {
}
}
/// Spend up to 1 minute trying to do the operation.
///
/// Even if Delta Chat itself does not hold the file lock,
/// there may be other processes such as antivirus,
/// or the filesystem may be network-mounted.
///
/// Without this workaround removing account may fail on Windows with an error
/// "The process cannot access the file because it is being used by another process. (os error 32)".
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
where
F: Fn() -> Fut,
Fut: Future<Output = std::result::Result<(), T>>,
{
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = f().await {
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
Ok(())
}
/// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig {
+81 -15
View File
@@ -2208,9 +2208,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
if msg.viewtype == Viewtype::Image || maybe_sticker {
if msg.viewtype == Viewtype::Image
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
{
blob.recode_to_image_size(context, &mut maybe_sticker)
.await?;
if !maybe_sticker {
msg.viewtype = Viewtype::Image;
}
@@ -5679,6 +5682,51 @@ mod tests {
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_jpeg_force() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let file = alice.get_blobdir().join("sticker.jpg");
tokio::fs::write(
&file,
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
)
.await
.unwrap();
// Images without force_sticker should be turned into [Viewtype::Image]
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Image);
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
msg.force_sticker();
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
// even on drafted messages
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
msg.force_sticker();
alice_chat
.id
.set_draft(&alice, Some(&mut msg))
.await
.unwrap();
let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap();
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_gif() -> Result<()> {
test_sticker(
@@ -6078,22 +6126,40 @@ mod tests {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
assert_eq!(chat.typ, Chattype::Broadcast);
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
assert!(!chat.is_self_talk());
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
{
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
assert_eq!(chat.typ, Chattype::Broadcast);
assert_eq!(chat.name, "Broadcast list");
assert!(!chat.is_self_talk());
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, chat.id);
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, chat.id);
}
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "ola!");
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(chat.id, chat_bob.id);
assert!(!chat.is_self_talk());
{
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "ola!");
assert_eq!(msg.subject, "Broadcast list");
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_ne!(chat.id, chat_bob.id);
assert_eq!(chat.name, "Broadcast list");
assert!(!chat.is_self_talk());
}
{
// Alice changes the name:
set_chat_name(&alice, broadcast_id, "My great broadcast").await?;
let sent = alice.send_text(broadcast_id, "I changed the title!").await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.subject, "Re: My great broadcast");
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(bob_chat.name, "My great broadcast");
}
Ok(())
}
+41 -6
View File
@@ -1,6 +1,7 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
use once_cell::sync::Lazy;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -15,6 +16,10 @@ use crate::stock_str;
use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty;
/// Regex to find out if a query should filter by unread messages.
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
/// An object representing a single chatlist in memory.
///
/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
@@ -78,7 +83,8 @@ impl Chatlist {
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
/// is added as needed.
/// `query`: An optional query for filtering the list. Only chats matching this query
/// are returned.
/// are returned. When `is:unread` is contained in the query, the chatlist is
/// filtered such that only chats with unread messages show up.
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned.
pub async fn try_load(
@@ -172,8 +178,10 @@ impl Chatlist {
)
.await?
} else if let Some(query) = query {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
let mut query = query.trim().to_string();
ensure!(!query.is_empty(), "query mustn't be empty");
let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
@@ -198,9 +206,10 @@ impl Chatlist {
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
AND c.name LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd),
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row,
process_rows,
)
@@ -462,7 +471,8 @@ pub async fn get_last_message_for_chat(
mod tests {
use super::*;
use crate::chat::{
create_group_chat, get_chat_contacts, remove_contact_from_chat, ProtectionStatus,
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
@@ -471,7 +481,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
let t = TestContext::new().await;
let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
@@ -510,6 +520,31 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1);
// receive a message from alice
let alice = TestContext::new_alice().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", "bob@example.net")
.await
.unwrap(),
)
.await
.unwrap();
send_text_msg(&alice, alice_chat_id, "hi".into())
.await
.unwrap();
let sent_msg = alice.pop_sent_msg().await;
t.recv_msg(&sent_msg).await;
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
.await
.unwrap();
assert!(chats.len() == 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
+49 -3
View File
@@ -286,15 +286,20 @@ pub enum Config {
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
/// Whether to avoid using IMAP IDLE even if the server supports it.
///
/// This is a developer option for testing "fake idle".
#[strum(props(default = "0"))]
DisableIdle,
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]
DownloadLimit,
/// Send sync messages, requires `BccSelf` to be set as well.
/// In a future versions, this switch may be removed.
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
#[strum(props(default = "0"))]
SendSyncMsgs,
SyncMsgs,
/// Space-separated list of all the authserv-ids which we believe
/// may be the one of our email server.
@@ -313,6 +318,13 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
/// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by
/// default.
///
/// This is not supposed to be changed by UIs and only used for testing.
#[strum(props(default = "172800"))]
GossipPeriod,
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
@@ -475,6 +487,28 @@ impl Context {
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
}
Config::Socks5Enabled
| Config::BccSelf
| Config::E2eeEnabled
| Config::MdnsEnabled
| Config::SentboxWatch
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::FetchExistingMsgs
| Config::DeleteToTrash
| Config::SaveMimeHeaders
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw
| Config::SyncMsgs
| Config::SignUnencrypted
| Config::DisableIdle => {
ensure!(
matches!(value, None | Some("0") | Some("1")),
"Boolean value must be either 0 or 1"
);
self.sql.set_raw_config(key.as_ref(), value).await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -624,6 +658,18 @@ mod tests {
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {
let t = TestContext::new().await;
assert!(t.set_config(Config::Bot, None).await.is_ok());
assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
+1 -2
View File
@@ -26,7 +26,6 @@ use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::imap::Imap;
use crate::job;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
@@ -466,7 +465,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
if configured_addr != param.addr {
// Switched account, all server UIDs we know are invalid
info!(ctx, "Scheduling resync because the address has changed.");
job::schedule_resync(ctx).await?;
ctx.schedule_resync().await?;
}
}
+31 -8
View File
@@ -139,6 +139,15 @@ impl ContactId {
pub const fn to_u32(&self) -> u32 {
self.0
}
/// Mark contact as bot.
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
context
.sql
.execute("UPDATE contacts SET is_bot=? WHERE id=?;", (is_bot, self.0))
.await?;
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -223,6 +232,9 @@ pub struct Contact {
/// Last seen message signature for this contact, to be displayed in the profile.
status: String,
/// If the contact is a bot.
is_bot: bool,
}
/// Possible origins of a contact.
@@ -366,7 +378,7 @@ impl Contact {
.sql
.query_row_optional(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
c.authname, c.param, c.status
c.authname, c.param, c.status, c.is_bot
FROM contacts c
WHERE c.id=?;",
(contact_id,),
@@ -379,6 +391,7 @@ impl Contact {
let authname: String = row.get(5)?;
let param: String = row.get(6)?;
let status: Option<String> = row.get(7)?;
let is_bot: bool = row.get(8)?;
let contact = Self {
id: contact_id,
name,
@@ -389,6 +402,7 @@ impl Contact {
origin,
param: param.parse().unwrap_or_default(),
status: status.unwrap_or_default(),
is_bot,
};
Ok(contact)
},
@@ -498,6 +512,11 @@ impl Contact {
Ok(())
}
/// Returns whether contact is a bot.
pub fn is_bot(&self) -> bool {
self.is_bot
}
/// Check if an e-mail address belongs to a known and unblocked contact.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
@@ -812,7 +831,11 @@ impl Contact {
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
Origin::Unknown
} else {
Origin::IncomingReplyTo
};
if flag_verified_only || query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
@@ -832,7 +855,7 @@ impl Contact {
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo,
minimal_origin,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 }
@@ -882,10 +905,10 @@ impl Contact {
ORDER BY last_seen DESC, id DESC;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo
])),
rusqlite::params_from_iter(
params_iter(&self_addrs)
.chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]),
),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
@@ -1245,7 +1268,7 @@ impl Contact {
return Ok(Some(ContactId::SELF));
}
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::AddressBook).await? {
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
Some(contact_id) => Ok(Some(contact_id)),
None => {
let addr = &self.addr;
+21 -10
View File
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
@@ -23,7 +23,7 @@ use crate::key::{load_self_public_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::SchedulerState;
use crate::scheduler::{InterruptInfo, SchedulerState};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -38,7 +38,7 @@ use crate::tools::{duration_to_str, time};
///
/// # Examples
///
/// Creating a new unecrypted database:
/// Creating a new unencrypted database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
@@ -211,9 +211,6 @@ pub struct InnerContext {
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// Set to true if quota update is requested.
pub(crate) quota_update_request: AtomicBool,
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
@@ -384,7 +381,6 @@ impl Context {
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
@@ -426,6 +422,16 @@ impl Context {
self.scheduler.maybe_network().await;
}
pub(crate) async fn schedule_resync(&self) -> Result<()> {
self.resync_request.store(true, Ordering::Relaxed);
self.scheduler
.interrupt_inbox(InterruptInfo {
probe_network: false,
})
.await;
Ok(())
}
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
@@ -578,7 +584,8 @@ impl Context {
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
@@ -690,7 +697,8 @@ impl Context {
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("send_sync_msgs", send_sync_msgs.to_string());
res.insert("sync_msgs", sync_msgs.to_string());
res.insert("disable_idle", disable_idle.to_string());
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);
@@ -752,7 +760,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -761,6 +768,10 @@ impl Context {
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
res.insert(
"gossip_period",
self.get_config_int(Config::GossipPeriod).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
+49 -52
View File
@@ -10,11 +10,11 @@ use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::scheduler::InterruptInfo;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use crate::{stock_str, EventType};
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
@@ -23,7 +23,7 @@ use crate::{job_try, stock_str, EventType};
/// eg. to assign them to the correct chat.
/// As these messages are typically small,
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
/// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
@@ -90,7 +90,14 @@ impl MsgId {
DownloadState::Available | DownloadState::Failure => {
self.update_download_state(context, DownloadState::InProgress)
.await?;
job::add(context, Job::new(Action::DownloadMsg, self.to_u32())).await?;
context
.sql
.execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
.await?;
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
}
}
Ok(())
@@ -124,59 +131,49 @@ impl Message {
}
}
impl Job {
/// Actually download a message.
/// Called in response to `Action::DownloadMsg`.
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "download: could not connect: {:#}", err);
return Status::RetryNow;
}
/// Actually download a message partially downloaded before.
///
/// Most messages are downloaded automatically on fetch instead.
pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Imap) -> Result<()> {
imap.prepare(context).await?;
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let row = job_try!(
context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
}
)
.await
);
let msg = Message::load_from_db(context, msg_id).await?;
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
},
)
.await?;
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
job_try!(
msg.id
.update_download_state(context, DownloadState::Failure)
.await
);
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
}
ImapActionResult::Success => {
// update_download_state() not needed as receive_imf() already
// set the state and emitted the event.
Status::Finished(Ok(()))
}
}
} else {
// No IMAP record found, we don't know the UID and folder.
job_try!(
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
msg.id
.update_download_state(context, DownloadState::Failure)
.await
);
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
.await?;
Err(anyhow!("Call download_full() again to try over."))
}
ImapActionResult::Success => {
// update_download_state() not needed as receive_imf() already
// set the state and emitted the event.
Ok(())
}
}
} else {
// No IMAP record found, we don't know the UID and folder.
msg.id
.update_download_state(context, DownloadState::Failure)
.await?;
Err(anyhow!("Call download_full() again to try over."))
}
}
+5 -3
View File
@@ -1,6 +1,7 @@
//! # Events specification.
use async_channel::{self as channel, Receiver, Sender, TrySendError};
use pin_project::pin_project;
mod payload;
@@ -64,7 +65,8 @@ impl Events {
/// [`Context::get_event_emitter`]: crate::context::Context::get_event_emitter
/// [`Stream`]: futures::stream::Stream
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
#[pin_project]
pub struct EventEmitter(#[pin] Receiver<Event>);
impl EventEmitter {
/// Async recv of an event. Return `None` if the `Sender` has been dropped.
@@ -77,10 +79,10 @@ impl futures::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
self.project().0.poll_next(cx)
}
}
+29 -32
View File
@@ -26,7 +26,6 @@ use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifie
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
@@ -82,7 +81,6 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])";
const JUST_UID: &str = "(UID)";
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
@@ -391,6 +389,7 @@ impl Imap {
"IMAP-LOGIN as {}",
self.config.lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
Ok(())
}
@@ -614,7 +613,7 @@ impl Imap {
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, uid_next).await?;
job::schedule_resync(context).await?;
context.schedule_resync().await?;
}
uid_next != old_uid_next // If uid_next changed, there are new emails
} else {
@@ -626,18 +625,6 @@ impl Imap {
// UIDVALIDITY is modified, reset highest seen MODSEQ.
set_modseq(context, folder, 0).await?;
if mailbox.exists == 0 {
info!(context, "Folder {folder:?} is empty.");
// set uid_next=1 for empty folders.
// If we do not do this here, we'll miss the first message
// as we will get in here again and fetch from uid_next then.
// Also, the "fall back to fetching" below would need a non-zero mailbox.exists to work.
set_uid_next(context, folder, 1).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
return Ok(false);
}
// ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = match mailbox.uid_next {
@@ -645,25 +632,35 @@ impl Imap {
None => {
warn!(
context,
"IMAP folder {folder:?} has no uid_next, fall back to fetching."
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
);
// note that we use fetch by sequence number
// and thus we only need to get exactly the
// last-index message.
let set = format!("{}", mailbox.exists);
let mut list = session
.inner
.fetch(set, JUST_UID)
.await
.context("Error fetching UID")?;
let mut new_last_seen_uid = None;
while let Some(fetch) = list.try_next().await? {
if fetch.message == mailbox.exists && fetch.uid.is_some() {
new_last_seen_uid = fetch.uid;
}
// RFC 3501 says STATUS command SHOULD NOT be used
// on the currently selected mailbox because the same
// information can be obtained by other means,
// such as reading SELECT response.
//
// However, it also says that UIDNEXT is REQUIRED
// in the SELECT response and if we are here,
// it is actually not returned.
//
// In particular, Winmail Pro Mail Server 5.1.0616
// never returns UIDNEXT in SELECT response,
// but responds to "SELECT INBOX (UIDNEXT)" command.
let status = session
.inner
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
if let Some(uid_next) = status.uid_next {
uid_next
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Set UIDNEXT to 1 as a last resort fallback.
1
}
new_last_seen_uid.context("select: failed to fetch")? + 1
}
};
@@ -680,7 +677,7 @@ impl Imap {
.await?;
if old_uid_validity != 0 || old_uid_next != 0 {
job::schedule_resync(context).await?;
context.schedule_resync().await?;
}
info!(
context,
+27 -23
View File
@@ -15,6 +15,7 @@ use crate::net::connect_tcp;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::socks::Socks5Config;
use fast_socks5::client::Socks5Stream;
/// IMAP write and read timeout.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
@@ -64,6 +65,12 @@ async fn determine_capabilities(
}
impl Client {
fn new(stream: Box<dyn SessionStream>) -> Self {
Self {
inner: ImapClient::new(stream),
}
}
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
let Client { inner, .. } = self;
let mut session = inner
@@ -98,27 +105,24 @@ impl Client {
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(Client { inner: client })
Ok(client)
}
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(Client { inner: client })
Ok(client)
}
pub async fn connect_starttls(
@@ -130,7 +134,8 @@ impl Client {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
// Run STARTTLS command and convert the client back into a stream.
let mut client = ImapClient::new(tcp_stream);
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = ImapClient::new(buffered_tcp_stream);
let _greeting = client
.read_response()
.await
@@ -139,7 +144,8 @@ impl Client {
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let tcp_stream = client.into_inner();
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
@@ -147,9 +153,8 @@ impl Client {
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = ImapClient::new(session_stream);
Ok(Client { inner: client })
let client = Client::new(session_stream);
Ok(client)
}
pub async fn connect_secure_socks5(
@@ -165,13 +170,12 @@ impl Client {
let tls_stream = wrap_tls(strict_tls, domain, socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(Client { inner: client })
Ok(client)
}
pub async fn connect_insecure_socks5(
@@ -185,13 +189,12 @@ impl Client {
.await?;
let buffered_stream = BufWriter::new(socks5_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(Client { inner: client })
Ok(client)
}
pub async fn connect_starttls_socks5(
@@ -206,7 +209,8 @@ impl Client {
.await?;
// Run STARTTLS command and convert the client back into a stream.
let mut client = ImapClient::new(socks5_stream);
let buffered_socks5_stream = BufWriter::new(socks5_stream);
let mut client = ImapClient::new(buffered_socks5_stream);
let _greeting = client
.read_response()
.await
@@ -215,15 +219,15 @@ impl Client {
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let socks5_stream = client.into_inner();
let buffered_socks5_stream = client.into_inner();
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = ImapClient::new(session_stream);
Ok(Client { inner: client })
let client = Client::new(session_stream);
Ok(client)
}
}
+14 -1
View File
@@ -7,7 +7,9 @@ use futures_lite::FutureExt;
use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::log::LogExt;
use crate::{context::Context, scheduler::InterruptInfo};
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
@@ -21,6 +23,10 @@ impl Session {
) -> Result<(Self, InterruptInfo)> {
use futures::future::FutureExt;
if context.get_config_bool(Config::DisableIdle).await? {
bail!("IMAP IDLE is disabled");
}
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
@@ -163,7 +169,14 @@ impl Imap {
continue;
}
if let Some(session) = &self.session {
if session.can_idle() {
if session.can_idle()
&& !context
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(context)
.unwrap_or_default()
{
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
}
+12 -8
View File
@@ -60,8 +60,7 @@ pub enum ImexMode {
/// Export a backup to the directory given as `path` with the given `passphrase`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
/// the format is `delta-chat-<day>-<number>.tar`
/// The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
ExportBackup = 11,
/// `path` is the file (not: directory) to import. The file is normally
@@ -130,7 +129,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
// This works fine because the filenames have the form `delta-chat-backup-2023-10-18-00-foo@example.com.tar`
newest_backup_path = Some(path);
newest_backup_name = name;
}
@@ -486,7 +485,11 @@ async fn import_backup(
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> {
fn get_next_backup_path(
folder: &Path,
addr: &str,
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
.context("can't get next backup path")?
@@ -497,13 +500,13 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut tempdbfile = folder.clone();
tempdbfile.push(format!("{stem}-{i:02}.db"));
tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
let mut tempfile = folder.clone();
tempfile.push(format!("{stem}-{i:02}.tar.part"));
tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
let mut destfile = folder.clone();
destfile.push(format!("{stem}-{i:02}.tar"));
destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
return Ok((tempdbfile, tempfile, destfile));
@@ -518,7 +521,8 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now)?;
let self_addr = context.get_primary_self_addr().await?;
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
let _d1 = DeleteOnDrop(temp_db_path.clone());
let _d2 = DeleteOnDrop(temp_path.clone());
-390
View File
@@ -1,390 +0,0 @@
//! # Job module.
//!
//! This module implements a job queue maintained in the SQLite database
//! and job types.
#![allow(missing_docs)]
use std::fmt;
use std::sync::atomic::Ordering;
use anyhow::{Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::imap::Imap;
use crate::scheduler::InterruptInfo;
use crate::tools::time;
// results in ~3 weeks for the last backoff timespan
const JOB_RETRIES: u32 = 17;
/// Job try result.
#[derive(Debug, Display)]
pub enum Status {
Finished(Result<()>),
RetryNow,
}
#[macro_export]
macro_rules! job_try {
($expr:expr) => {
match $expr {
std::result::Result::Ok(val) => val,
std::result::Result::Err(err) => {
return $crate::job::Status::Finished(Err(err.into()));
}
}
};
($expr:expr,) => {
$crate::job_try!($expr)
};
}
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
)]
#[repr(u32)]
pub enum Action {
// This job will download partially downloaded messages completely
// and is added when download_full() is called.
// Most messages are downloaded automatically on fetch
// and do not go through this job.
DownloadMsg = 250,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Job {
pub job_id: u32,
pub action: Action,
pub foreign_id: u32,
pub desired_timestamp: i64,
pub added_timestamp: i64,
pub tries: u32,
}
impl fmt::Display for Job {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{}, action {}", self.job_id, self.action)
}
}
impl Job {
pub fn new(action: Action, foreign_id: u32) -> Self {
let timestamp = time();
Self {
job_id: 0,
action,
foreign_id,
desired_timestamp: timestamp,
added_timestamp: timestamp,
tries: 0,
}
}
/// Deletes the job from the database.
async fn delete(self, context: &Context) -> Result<()> {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,))
.await?;
}
Ok(())
}
/// Saves the job to the database, creating a new entry if necessary.
///
/// The Job is consumed by this method.
pub(crate) async fn save(self, context: &Context) -> Result<()> {
info!(context, "saving job {:?}", self);
if self.job_id != 0 {
context
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
(
self.desired_timestamp,
i64::from(self.tries),
self.job_id as i32,
),
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
(
self.added_timestamp,
self.action,
self.foreign_id,
self.desired_timestamp
)
).await?;
}
Ok(())
}
}
pub(crate) enum Connection<'a> {
Inbox(&'a mut Imap),
}
impl<'a> Connection<'a> {
fn inbox(&mut self) -> &mut Imap {
match self {
Connection::Inbox(imap) => imap,
}
}
}
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
info!(context, "Job {} started...", &job);
let try_res = match perform_job_action(context, &job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &job, &mut connection, 1).await,
x => x,
};
match try_res {
Status::RetryNow => {
let tries = job.tries + 1;
if tries < JOB_RETRIES {
info!(context, "Increase job {job} tries to {tries}.");
job.tries = tries;
let time_offset = get_backoff_time_offset(tries);
job.desired_timestamp = time() + time_offset;
info!(
context,
"job #{} not succeeded on try #{}, retry in {} seconds.",
job.job_id,
tries,
time_offset
);
job.save(context).await.unwrap_or_else(|err| {
error!(context, "Failed to save job: {err:#}.");
});
} else {
info!(
context,
"Remove job {job} as it exhausted {JOB_RETRIES} retries."
);
job.delete(context).await.unwrap_or_else(|err| {
error!(context, "Failed to delete job: {err:#}.");
});
}
}
Status::Finished(res) => {
if let Err(err) = res {
warn!(context, "Remove job {job} as it failed with error {err:#}.");
} else {
info!(context, "Remove job {job} as it succeeded.");
}
job.delete(context).await.unwrap_or_else(|err| {
error!(context, "failed to delete job: {:#}", err);
});
}
}
}
async fn perform_job_action(
context: &Context,
job: &Job,
connection: &mut Connection<'_>,
tries: u32,
) -> Status {
info!(context, "Begin immediate try {tries} of job {job}.");
let try_res = match job.action {
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
};
info!(context, "Finished immediate try {tries} of job {job}.");
try_res
}
fn get_backoff_time_offset(tries: u32) -> i64 {
// Exponential backoff
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
let r: i32 = rng.gen();
let mut seconds = r % (n + 1);
if seconds < 1 {
seconds = 1;
}
i64::from(seconds)
}
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
context.resync_request.store(true, Ordering::Relaxed);
context
.scheduler
.interrupt_inbox(InterruptInfo {
probe_network: false,
})
.await;
Ok(())
}
/// Adds a job to the database, scheduling it.
pub async fn add(context: &Context, job: Job) -> Result<()> {
job.save(context).await.context("failed to save job")?;
info!(context, "Interrupt: IMAP.");
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
Ok(())
}
/// Load jobs from the database.
///
/// The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
pub(crate) async fn load_next(context: &Context, info: &InterruptInfo) -> Result<Option<Job>> {
info!(context, "Loading job.");
let query;
let params;
let t = time();
if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = vec![t];
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = vec![];
};
loop {
let job_res = context
.sql
.query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
foreign_id: row.get("foreign_id")?,
desired_timestamp: row.get("desired_timestamp")?,
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
};
Ok(job)
})
.await;
match job_res {
Ok(job) => return Ok(job),
Err(err) => {
// Remove invalid job from the DB
info!(context, "Cleaning up job, because of {err:#}.");
// TODO: improve by only doing a single query
let id = context
.sql
.query_row(query, rusqlite::params_from_iter(params.clone()), |row| {
row.get::<_, i32>(0)
})
.await
.context("failed to retrieve invalid job ID from the database")?;
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", (id,))
.await
.with_context(|| format!("failed to delete invalid job {id}"))?;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {
let now = time();
context
.sql
.execute(
"INSERT INTO jobs
(added_timestamp, action, foreign_id, desired_timestamp)
VALUES (?, ?, ?, ?);",
(
now,
if valid {
Action::DownloadMsg as i32
} else {
-1
},
foreign_id,
now,
),
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_next_job_two() -> Result<()> {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
// all jobs.
let t = TestContext::new().await;
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
assert!(jobs.is_none());
insert_job(&t, 1, true).await;
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
assert!(jobs.is_some());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_next_job_one() -> Result<()> {
let t = TestContext::new().await;
insert_job(&t, 1, true).await;
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
assert!(jobs.is_some());
Ok(())
}
}
+2 -4
View File
@@ -65,10 +65,6 @@ mod e2ee;
pub mod ephemeral;
mod imap;
pub mod imex;
pub mod release;
mod scheduler;
#[macro_use]
mod job;
pub mod key;
pub mod location;
mod login_param;
@@ -83,6 +79,8 @@ pub mod provider;
pub mod qr;
pub mod qr_code_generator;
pub mod quota;
pub mod release;
mod scheduler;
pub mod securejoin;
mod simplify;
mod smtp;
+10
View File
@@ -664,6 +664,12 @@ impl Message {
self.viewtype
}
/// Forces the message to **keep** [Viewtype::Sticker]
/// e.g the message will not be converted to a [Viewtype::Image].
pub fn force_sticker(&mut self) {
self.param.set_int(Param::ForceSticker, 1);
}
/// Returns the state of the message.
pub fn get_state(&self) -> MessageState {
self.state
@@ -2333,6 +2339,8 @@ mod tests {
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text(), "hello".to_string());
assert!(msg.is_bot());
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
assert!(contact.is_bot());
// Alice receives a message from Bob who is not the bot anymore.
receive_imf(
@@ -2350,6 +2358,8 @@ mod tests {
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text(), "hello again".to_string());
assert!(!msg.is_bot());
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
assert!(!contact.is_bot());
Ok(())
}
+23 -13
View File
@@ -359,9 +359,10 @@ impl<'a> MimeFactory<'a> {
async fn should_do_gossip(&self, context: &Context) -> Result<bool> {
match &self.loaded {
Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
if time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else {
let cmd = self.msg.param.get_cmd();
@@ -414,7 +415,9 @@ impl<'a> MimeFactory<'a> {
return Ok(self.msg.subject.clone());
}
if chat.typ == Chattype::Group && quoted_msg_subject.is_none_or_empty() {
if (chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast)
&& quoted_msg_subject.is_none_or_empty()
{
let re = if self.in_reply_to.is_empty() {
""
} else {
@@ -423,15 +426,13 @@ impl<'a> MimeFactory<'a> {
return Ok(format!("{}{}", re, chat.name));
}
if chat.typ != Chattype::Broadcast {
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
let self_name = &match context.get_config(Config::Displayname).await? {
@@ -593,6 +594,15 @@ impl<'a> MimeFactory<'a> {
));
}
if let Loaded::Message { chat } = &self.loaded {
if chat.typ == Chattype::Broadcast {
headers.protected.push(Header::new(
"List-ID".into(),
format!("{} <{}>", chat.name, chat.grpid),
));
}
}
// Non-standard headers.
headers
.unprotected
@@ -2340,7 +2350,7 @@ mod tests {
// Now Bob can send an encrypted message to Alice.
let mut msg = Message::new(Viewtype::File);
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
// decoded_data to check presense of the necessary headers.
// 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)
.await?;
+24 -28
View File
@@ -109,11 +109,15 @@ pub(crate) struct MimeMessage {
/// The decrypted, raw mime structure.
///
/// This is non-empty only if the message was actually encrypted. It is used
/// This is non-empty iff `is_mime_modified` and the message was actually encrypted. It is used
/// for e.g. late-parsing HTML.
pub decoded_data: Vec<u8>,
/// Hop info for debugging.
pub(crate) hop_info: String,
/// Whether the contact sending this should be marked as bot.
pub(crate) is_bot: bool,
}
#[derive(Debug, PartialEq)]
@@ -122,23 +126,6 @@ pub(crate) enum AvatarAction {
Change(String),
}
#[derive(Debug, PartialEq)]
pub(crate) enum MailinglistType {
/// The message belongs to a mailing list and has a `ListId:`-header
/// that should be used to get a unique id.
ListIdBased,
/// The message belongs to a mailing list, but there is no `ListId:`-header;
/// `Sender:`-header should be used to get a unique id.
/// This method is used by implementations as Majordomo.
/// Note, that the `Sender:` header alone is not sufficient to detect these lists,
/// `get_mailinglist_type()` check additional conditions therefore.
SenderBased,
/// The message does not belong to a mailing list.
None,
}
/// System message type.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
@@ -390,6 +377,9 @@ impl MimeMessage {
signatures.clear();
}
// Auto-submitted is also set by holiday-notices so we also check `chat-version`
let is_bot = headers.contains_key("auto-submitted") && headers.contains_key("chat-version");
let mut parser = MimeMessage {
parts: Vec::new(),
headers,
@@ -418,6 +408,7 @@ impl MimeMessage {
is_mime_modified: false,
decoded_data: Vec::new(),
hop_info,
is_bot,
};
match partial {
@@ -1217,6 +1208,9 @@ impl MimeMessage {
}
msg_type
} else if filename == "multi-device-sync.json" {
if !context.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
@@ -1340,26 +1334,28 @@ impl MimeMessage {
self.parts.push(part);
}
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType {
if self.get_header(HeaderDef::ListId).is_some() {
return MailinglistType::ListIdBased;
} else if self.get_header(HeaderDef::Sender).is_some() {
pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
if let Some(list_id) = self.get_header(HeaderDef::ListId) {
// The message belongs to a mailing list and has a `ListId:`-header
// that should be used to get a unique id.
return Some(list_id);
} else if let Some(sender) = self.get_header(HeaderDef::Sender) {
// the `Sender:`-header alone is no indicator for mailing list
// as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
if precedence == "list" || precedence == "bulk" {
return MailinglistType::SenderBased;
// The message belongs to a mailing list, but there is no `ListId:`-header;
// `Sender:`-header is be used to get a unique id.
// This method is used by implementations as Majordomo.
return Some(sender);
}
}
}
MailinglistType::None
None
}
pub(crate) fn is_mailinglist_message(&self) -> bool {
match self.get_mailinglist_type() {
MailinglistType::ListIdBased | MailinglistType::SenderBased => true,
MailinglistType::None => false,
}
self.get_mailinglist_header().is_some()
}
pub fn repl_msg_by_error(&mut self, error_msg: &str) {
+3
View File
@@ -187,6 +187,9 @@ pub enum Param {
/// For Webxdc Message Instances: timestamp of summary update.
WebxdcSummaryTimestamp = b'Q',
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
ForceSticker = b'X',
}
/// An object for handling key=value parameter lists.
+1 -1
View File
@@ -21,7 +21,7 @@ use crate::stock_str;
/// Type of the public key stored inside the peerstate.
#[derive(Debug)]
pub enum PeerstateKeyType {
/// Pubilc key sent in the `Autocrypt-Gossip` header.
/// Public key sent in the `Autocrypt-Gossip` header.
GossipKey,
/// Public key sent in the `Autocrypt` header.
+1 -1
View File
@@ -3,7 +3,7 @@
mod data;
use anyhow::Result;
use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
use hickory_resolver::{config, AsyncResolver, TokioAsyncResolver};
use crate::config::Config;
use crate::context::Context;
-24
View File
@@ -1,7 +1,6 @@
//! # Support for IMAP QUOTA extension.
use std::collections::BTreeMap;
use std::sync::atomic::Ordering;
use anyhow::{anyhow, Context as _, Result};
use async_imap::types::{Quota, QuotaResource};
@@ -13,7 +12,6 @@ use crate::imap::scan_folders::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::imap::Imap;
use crate::message::{Message, Viewtype};
use crate::scheduler::InterruptInfo;
use crate::tools::time;
use crate::{stock_str, EventType};
@@ -34,17 +32,12 @@ pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
/// providers report bad values and we would then spam the user.
pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
/// if recent quota is older,
/// it is re-fetched on dc_get_connectivity_html()
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
/// Server quota information with an update timestamp.
#[derive(Debug)]
pub struct QuotaInfo {
/// Recently loaded quota information.
/// set to `Err()` if the provider does not support quota or on other errors,
/// set to `Ok()` for valid quota information.
/// Updated by `Action::UpdateRecentQuota`
pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
/// Timestamp when structure was modified.
@@ -110,18 +103,6 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
}
impl Context {
// Adds a job to update `quota.recent`
pub(crate) async fn schedule_quota_update(&self) -> Result<()> {
let requested = self.quota_update_request.swap(true, Ordering::Relaxed);
if !requested {
// Quota update was not requested before.
self.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
}
Ok(())
}
/// Updates `quota.recent`, sets `quota.modified` to the current time
/// and emits an event to let the UIs update connectivity view.
///
@@ -130,8 +111,6 @@ impl Context {
/// As the message is added only once, the user is not spammed
/// in case for some providers the quota is always at ~100%
/// and new space is allocated as needed.
///
/// Called in response to `Action::UpdateRecentQuota`.
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<()> {
if let Err(err) = imap.prepare(self).await {
warn!(self, "could not connect: {:#}", err);
@@ -166,9 +145,6 @@ impl Context {
}
}
// Clear the request to update quota.
self.quota_update_request.store(false, Ordering::Relaxed);
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: time(),
+123 -91
View File
@@ -28,9 +28,7 @@ use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
};
use crate::mimeparser::{
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
};
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
@@ -77,6 +75,24 @@ pub async fn receive_imf(
let mail = parse_mail(imf_raw).context("can't parse mail")?;
let rfc724_mid =
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
if let Some(download_limit) = context.download_limit().await? {
let download_limit: usize = download_limit.try_into()?;
if imf_raw.len() > download_limit {
let head = std::str::from_utf8(imf_raw)?
.split("\r\n\r\n")
.next()
.context("No empty line in the message")?;
return receive_imf_inner(
context,
&rfc724_mid,
head.as_bytes(),
seen,
Some(imf_raw.len().try_into()?),
false,
)
.await;
}
}
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
}
@@ -361,6 +377,8 @@ pub(crate) async fn receive_imf_inner(
.handle_reports(context, from_id, sent_timestamp, &mime_parser.parts)
.await;
from_id.mark_bot(context, mime_parser.is_bot).await?;
Ok(Some(received_msg))
}
@@ -582,6 +600,7 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
is_partial_download.is_some(),
if test_normal_chat.is_none() {
allow_creation
} else {
@@ -644,45 +663,23 @@ async fn add_parts(
if chat_id.is_none() {
// check if the message belongs to a mailing list
match mime_parser.get_mailinglist_type() {
MailinglistType::ListIdBased => {
if let Some(list_id) = mime_parser.get_header(HeaderDef::ListId) {
if let Some((new_chat_id, new_chat_id_blocked)) =
create_or_lookup_mailinglist(
context,
allow_creation,
list_id,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
context,
allow_creation,
mailinglist_header,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
MailinglistType::SenderBased => {
if let Some(sender) = mime_parser.get_header(HeaderDef::Sender) {
if let Some((new_chat_id, new_chat_id_blocked)) =
create_or_lookup_mailinglist(
context,
allow_creation,
sender,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
MailinglistType::None => {}
}
}
if let Some(chat_id) = chat_id {
apply_mailinglist_changes(context, mime_parser, chat_id).await?;
apply_mailinglist_changes(context, mime_parser, sent_timestamp, chat_id).await?;
}
// if contact renaming is prevented (for mailinglists and bots),
@@ -858,6 +855,7 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
is_partial_download.is_some(),
allow_creation,
Blocked::Not,
from_id,
@@ -1213,8 +1211,8 @@ INSERT INTO msgs
)
ON CONFLICT (id) DO UPDATE
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp=excluded.timestamp, timestamp_sent=excluded.timestamp_sent,
timestamp_rcvd=excluded.timestamp_rcvd, type=excluded.type, state=excluded.state, msgrmsg=excluded.msgrmsg,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
type=excluded.type, msgrmsg=excluded.msgrmsg,
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
bytes=excluded.bytes, mime_headers=excluded.mime_headers,
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
@@ -1545,6 +1543,7 @@ async fn is_probably_private_reply(
async fn create_or_lookup_group(
context: &Context,
mime_parser: &mut MimeMessage,
is_partial_download: bool,
allow_creation: bool,
create_blocked: Blocked,
from_id: ContactId,
@@ -1677,7 +1676,7 @@ async fn create_or_lookup_group(
if let Some(chat_id) = chat_id {
Ok(Some((chat_id, chat_id_blocked)))
} else if mime_parser.decrypting_failed {
} else if is_partial_download || mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
// Chat-Group-Name, which is in the encrypted part, was
@@ -1949,6 +1948,8 @@ async fn apply_group_changes(
Ok(better_msg)
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
/// Create or lookup a mailing list chat.
///
/// `list_id_header` contains the Id that must be used for the mailing list
@@ -1965,23 +1966,71 @@ async fn create_or_lookup_mailinglist(
list_id_header: &str,
mime_parser: &MimeMessage,
) -> Result<Option<(ChatId, Blocked)>> {
static LIST_ID: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
let (mut name, listid) = match LIST_ID.captures(list_id_header) {
Some(cap) => (cap[1].trim().to_string(), cap[2].trim().to_string()),
None => (
"".to_string(),
list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
),
let listid = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[2].trim().to_string(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
};
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked)));
}
let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
if allow_creation {
// list does not exist but should be created
let param = mime_parser.list_post.as_ref().map(|list_post| {
let mut p = Params::new();
p.set(Param::ListPost, list_post);
p.to_string()
});
let is_bot = context.get_config_bool(Config::Bot).await?;
let blocked = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let chat_id = ChatId::create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
&name,
blocked,
ProtectionStatus::Unprotected,
param,
)
.await
.with_context(|| {
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
} else {
info!(context, "Creating list forbidden by caller.");
Ok(None)
}
}
#[allow(clippy::indexing_slicing)]
fn compute_mailinglist_name(
list_id_header: &str,
listid: &str,
mime_parser: &MimeMessage,
) -> String {
let mut name = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[1].trim().to_string(),
None => "".to_string(),
};
// for mailchimp lists, the name in `ListId` is just a long number.
// a usable name for these lists is in the `From` header
// and we can detect these lists by a unique `ListId`-suffix.
@@ -2025,50 +2074,14 @@ async fn create_or_lookup_mailinglist(
// 51231231231231231231231232869f58.xing.com -> xing.com
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(&listid) {
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(listid) {
name = cap[2].to_string();
} else {
name = listid.clone();
name = listid.to_string();
}
}
if allow_creation {
// list does not exist but should be created
let param = mime_parser.list_post.as_ref().map(|list_post| {
let mut p = Params::new();
p.set(Param::ListPost, list_post);
p.to_string()
});
let is_bot = context.get_config_bool(Config::Bot).await?;
let blocked = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let chat_id = ChatId::create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
&name,
blocked,
ProtectionStatus::Unprotected,
param,
)
.await
.with_context(|| {
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
} else {
info!(context, "Creating list forbidden by caller.");
Ok(None)
}
strip_rtlo_characters(&name)
}
/// Set ListId param on the contact and ListPost param the chat.
@@ -2077,9 +2090,10 @@ async fn create_or_lookup_mailinglist(
async fn apply_mailinglist_changes(
context: &Context,
mime_parser: &MimeMessage,
sent_timestamp: i64,
chat_id: ChatId,
) -> Result<()> {
let Some(list_post) = &mime_parser.list_post else {
let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
return Ok(());
};
@@ -2089,6 +2103,24 @@ async fn apply_mailinglist_changes(
}
let listid = &chat.grpid;
let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
if chat.name != new_name
&& chat_id
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
.await?
{
info!(context, "Updating listname for chat {chat_id}.");
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}
let Some(list_post) = &mime_parser.list_post else {
return Ok(());
};
let list_post = match ContactAddress::new(list_post) {
Ok(list_post) => list_post,
Err(err) => {
+114 -1
View File
@@ -10,8 +10,9 @@ use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::download::{DownloadState, MIN_DOWNLOAD_LIMIT};
use crate::imap::prefetch_should_download;
use crate::message::Message;
use crate::message::{self, Message};
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2973,6 +2974,7 @@ async fn test_auto_accept_for_bots() -> Result<()> {
let msg = t.get_last_msg().await;
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(!chat.is_contact_request());
assert!(Contact::get_all(&t, 0, None).await?.len() == 1);
Ok(())
}
@@ -3697,3 +3699,114 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_download_later() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
let bob = tcm.bob().await;
let bob_chat = bob.create_chat(&alice).await;
let text = String::from_utf8(vec![b'a'; MIN_DOWNLOAD_LIMIT as usize])?;
let sent_msg = bob.send_text(bob_chat.id, &text).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(msg.state, MessageState::InFresh);
let hi_msg = tcm.send_recv(&bob, &alice, "hi").await;
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(alice.get_last_msg_in(msg.chat_id).await.id, hi_msg.id);
assert!(msg.timestamp_sort <= hi_msg.timestamp_sort);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_with_big_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let ba_contact = Contact::create(
&bob,
"alice",
&alice.get_config(Config::Addr).await?.unwrap(),
)
.await?;
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
assert!(!msg.get_showpadlock());
alice.set_config(Config::DownloadLimit, Some("1")).await?;
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_grp.typ, Chattype::Group);
assert_eq!(alice_grp.name, "Group");
assert_eq!(
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
2
);
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.viewtype, Viewtype::Image);
assert_eq!(msg.chat_id, alice_grp.id);
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_grp.typ, Chattype::Group);
assert_eq!(alice_grp.name, "Group");
assert_eq!(
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
2
);
let ab_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
// Now Bob can send encrypted messages to Alice.
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?;
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
assert!(msg.get_showpadlock());
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
// Until fully downloaded, an encrypted message must sit in the 1:1 chat.
assert_eq!(msg.chat_id, ab_chat_id);
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.viewtype, Viewtype::Image);
assert_ne!(msg.chat_id, ab_chat_id);
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_grp.typ, Chattype::Group);
assert_eq!(alice_grp.name, "Group1");
assert_eq!(
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
2
);
// The big message must go away from the 1:1 chat.
assert_eq!(alice.get_last_msg_in(ab_chat_id).await.text, "hi");
Ok(())
}
+112 -71
View File
@@ -13,16 +13,16 @@ use self::connectivity::ConnectivityStore;
use crate::config::Config;
use crate::contact::{ContactId, RecentlySeenLoop};
use crate::context::Context;
use crate::download::download_msg;
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::events::EventType;
use crate::imap::{FolderMeaning, Imap};
use crate::job;
use crate::location;
use crate::log::LogExt;
use crate::message::MsgId;
use crate::smtp::{send_smtp_messages, Smtp};
use crate::sql;
use crate::tools::time;
use crate::tools::{duration_to_str, maybe_add_time_based_warnings};
use crate::tools::{duration_to_str, maybe_add_time_based_warnings, time};
pub(crate) mod connectivity;
@@ -323,6 +323,37 @@ pub(crate) struct Scheduler {
recently_seen_loop: RecentlySeenLoop,
}
async fn download_msgs(context: &Context, imap: &mut Imap) -> Result<()> {
let msg_ids = context
.sql
.query_map(
"SELECT msg_id FROM download",
(),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
|rowids| {
rowids
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for msg_id in msg_ids {
if let Err(err) = download_msg(context, msg_id, imap).await {
warn!(context, "Failed to download message {msg_id}: {:#}.", err);
}
context
.sql
.execute("DELETE FROM download WHERE msg_id=?", (msg_id,))
.await?;
}
Ok(())
}
async fn inbox_loop(
ctx: Context,
started: oneshot::Sender<()>,
@@ -344,79 +375,76 @@ async fn inbox_loop(
return;
};
let mut info = InterruptInfo::default();
loop {
let job = match job::load_next(&ctx, &info).await {
Err(err) => {
error!(ctx, "Failed loading job from the database: {:#}.", err);
None
}
Ok(job) => job,
};
{
// Update quota no more than once a minute.
let quota_needs_update = {
let quota = ctx.quota.read().await;
quota
.as_ref()
.filter(|quota| quota.modified + 60 > time())
.is_none()
};
match job {
Some(job) => {
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
info = Default::default();
}
None => {
let quota_requested = ctx.quota_update_request.swap(false, Ordering::Relaxed);
if quota_requested {
if let Err(err) = ctx.update_recent_quota(&mut connection).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
if quota_needs_update {
if let Err(err) = ctx.update_recent_quota(&mut connection).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Err(err) = connection.resync_folders(&ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
}
maybe_add_time_based_warnings(&ctx).await;
match ctx.get_config_i64(Config::LastHousekeeping).await {
Ok(last_housekeeping_time) => {
let next_housekeeping_time =
last_housekeeping_time.saturating_add(60 * 60 * 24);
if next_housekeeping_time <= time() {
sql::housekeeping(&ctx).await.log_err(&ctx).ok();
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
}
};
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
Ok(fetched_existing_msgs) => {
if !fetched_existing_msgs {
// Consider it done even if we fail.
//
// This operation is not critical enough to retry,
// especially if the error is persistent.
if let Err(err) =
ctx.set_config_bool(Config::FetchedExistingMsgs, true).await
{
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
}
if let Err(err) = connection.fetch_existing_msgs(&ctx).await {
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
connection.trigger_reconnect(&ctx);
}
}
}
Err(err) => {
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
}
}
info = fetch_idle(&ctx, &mut connection, FolderMeaning::Inbox).await;
}
}
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Err(err) = connection.resync_folders(&ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
}
maybe_add_time_based_warnings(&ctx).await;
match ctx.get_config_i64(Config::LastHousekeeping).await {
Ok(last_housekeeping_time) => {
let next_housekeeping_time =
last_housekeeping_time.saturating_add(60 * 60 * 24);
if next_housekeeping_time <= time() {
sql::housekeeping(&ctx).await.log_err(&ctx).ok();
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
}
};
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
Ok(fetched_existing_msgs) => {
if !fetched_existing_msgs {
// Consider it done even if we fail.
//
// This operation is not critical enough to retry,
// especially if the error is persistent.
if let Err(err) =
ctx.set_config_bool(Config::FetchedExistingMsgs, true).await
{
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
}
if let Err(err) = connection.fetch_existing_msgs(&ctx).await {
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
connection.trigger_reconnect(&ctx);
}
}
}
Err(err) => {
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
}
}
if let Err(err) = download_msgs(&ctx, &mut connection).await {
warn!(ctx, "Failed to download messages: {:#}", err);
}
fetch_idle(&ctx, &mut connection, FolderMeaning::Inbox).await;
}
};
@@ -574,6 +602,19 @@ async fn fetch_idle(
.await;
}
if ctx
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(ctx)
.unwrap_or_default()
{
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
return connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
}
info!(ctx, "IMAP session supports IDLE, using it.");
match session
.idle(
+1 -9
View File
@@ -8,10 +8,7 @@ use tokio::sync::Mutex;
use crate::events::EventType;
use crate::imap::{scan_folders::get_watched_folder_configs, FolderMeaning};
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
use crate::tools::time;
use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
use crate::{context::Context, log::LogExt};
use crate::{stock_str, tools};
@@ -472,14 +469,9 @@ impl Context {
ret += format!("<li>{e}</li>").as_str();
}
}
if quota.modified + QUOTA_MAX_AGE_SECONDS < time() {
self.schedule_quota_update().await?;
}
} else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{not_connected}</li>");
self.schedule_quota_update().await?;
}
ret += "</ul>";
+14 -7
View File
@@ -674,12 +674,14 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
/// On failure returns an error without removing any `smtp_mdns` entries, the caller is responsible
/// for removing the corresponding entry to prevent endless loop in case the entry is invalid, e.g.
/// points to non-existent message or contact.
///
/// Returns true on success, false on temporary error.
async fn send_mdn_msg_id(
context: &Context,
msg_id: MsgId,
contact_id: ContactId,
smtp: &mut Smtp,
) -> Result<()> {
) -> Result<bool> {
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.is_blocked() {
return Err(format_err!("Contact is blocked"));
@@ -731,14 +733,14 @@ async fn send_mdn_msg_id(
.execute(&q, rusqlite::params_from_iter(additional_msg_ids))
.await?;
}
Ok(())
Ok(true)
}
SendResult::Retry => {
info!(
context,
"Temporary SMTP failure while sending an MDN for {}", msg_id
);
Ok(())
Ok(false)
}
SendResult::Failure(err) => Err(err),
}
@@ -785,15 +787,20 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
.await
.context("failed to update MDN retries count")?;
if let Err(err) = send_mdn_msg_id(context, msg_id, contact_id, smtp).await {
let res = send_mdn_msg_id(context, msg_id, contact_id, smtp).await;
if let Err(ref err) = res {
// If there is an error, for example there is no message corresponding to the msg_id in the
// database, do not try to send this MDN again.
warn!(
context,
"Error sending MDN for {msg_id}, removing it: {err:#}."
);
context
.sql
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
.await?;
Err(err)
} else {
Ok(true)
}
// If there's a temporary error, pretend there are no more MDNs to send. It's unlikely that
// other MDNs could be sent successfully in case of connectivity problems.
res
}
+1 -2
View File
@@ -686,6 +686,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
PRAGMA secure_delete=on;
PRAGMA busy_timeout = 0; -- fail immediately
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
PRAGMA foreign_keys=on;
",
)?;
@@ -741,8 +742,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
warn!(context, "Failed to deduplicate peerstates: {:#}.", err)
}
context.schedule_quota_update().await?;
// Try to clear the freelist to free some space on the disk. This
// only works if auto_vacuum is enabled.
match context
+19
View File
@@ -731,6 +731,25 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 102 {
sql.execute_migration(
"CREATE TABLE download (
msg_id INTEGER NOT NULL -- id of the message stub in msgs table
)",
102,
)
.await?;
}
// Add is_bot column to contacts table with default false.
if dbversion < 103 {
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN is_bot INTEGER NOT NULL DEFAULT 0",
103,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
+1 -1
View File
@@ -1546,7 +1546,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// a subsequent call to update_device_chats() must not re-add manally deleted messages or chats
// a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
+12 -18
View File
@@ -43,13 +43,6 @@ pub(crate) struct SyncItems {
}
impl Context {
/// Checks if sync messages shall be sent.
/// Receiving sync messages is currently always enabled;
/// the messages are force-encrypted anyway.
async fn is_sync_sending_enabled(&self) -> Result<bool> {
self.get_config_bool(Config::SendSyncMsgs).await
}
/// Adds an item to the list of items that should be synchronized to other devices.
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
self.add_sync_item_with_timestamp(data, time()).await
@@ -58,7 +51,7 @@ impl Context {
/// Adds item and timestamp to the list of items that should be synchronized to other devices.
/// If device synchronization is disabled, the function does nothing.
async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
if !self.is_sync_sending_enabled().await? {
if !self.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
@@ -75,7 +68,7 @@ impl Context {
/// If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
if !self.is_sync_sending_enabled().await? {
if !self.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
@@ -267,20 +260,20 @@ mod tests {
use crate::token::Namespace;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_is_sync_sending_enabled() -> Result<()> {
async fn test_config_sync_msgs() -> Result<()> {
let t = TestContext::new_alice().await;
assert!(!t.is_sync_sending_enabled().await?);
t.set_config_bool(Config::SendSyncMsgs, true).await?;
assert!(t.is_sync_sending_enabled().await?);
t.set_config_bool(Config::SendSyncMsgs, false).await?;
assert!(!t.is_sync_sending_enabled().await?);
assert!(!t.get_config_bool(Config::SyncMsgs).await?);
t.set_config_bool(Config::SyncMsgs, true).await?;
assert!(t.get_config_bool(Config::SyncMsgs).await?);
t.set_config_bool(Config::SyncMsgs, false).await?;
assert!(!t.get_config_bool(Config::SyncMsgs).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_sync_json() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::SendSyncMsgs, true).await?;
t.set_config_bool(Config::SyncMsgs, true).await?;
assert!(t.build_sync_json().await?.is_none());
@@ -325,7 +318,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::SendSyncMsgs, false).await?;
t.set_config_bool(Config::SyncMsgs, false).await?;
t.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber: "testinvite".to_string(),
auth: "testauth".to_string(),
@@ -453,7 +446,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_sync_msg() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config_bool(Config::SendSyncMsgs, true).await?;
alice.set_config_bool(Config::SyncMsgs, true).await?;
alice
.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber: "in".to_string(),
@@ -480,6 +473,7 @@ mod tests {
// also here, self-talk should stay hidden
let sent_msg = alice.pop_sent_msg().await;
let alice2 = TestContext::new_alice().await;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
alice2.recv_msg(&sent_msg).await;
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await);
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
+98 -10
View File
@@ -20,6 +20,7 @@ use mailparse::headers::Headers;
use mailparse::MailHeaderMap;
use rand::{thread_rng, Rng};
use tokio::{fs, io};
use url::Url;
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
@@ -268,7 +269,7 @@ pub(crate) fn create_id() -> String {
/// Function generates a Message-ID that can be used for a new outgoing message.
/// - this function is called for all outgoing messages.
/// - the message ID should be globally unique
/// - do not add a counter or any private data as this leaks information unncessarily
/// - do not add a counter or any private data as this leaks information unnecessarily
pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
@@ -481,7 +482,43 @@ pub(crate) fn time() -> i64 {
.as_secs() as i64
}
/// Very simple email address wrapper.
/// Struct containing all mailto information
#[derive(Debug, Default, Eq, PartialEq)]
pub struct MailTo {
pub to: Vec<EmailAddress>,
pub subject: Option<String>,
pub body: Option<String>,
}
/// Parse mailto urls
pub fn parse_mailto(mailto_url: &str) -> Option<MailTo> {
if let Ok(url) = Url::parse(mailto_url) {
if url.scheme() == "mailto" {
let mut mailto: MailTo = Default::default();
// Extract the email address
url.path().split(',').for_each(|email| {
if let Ok(email) = EmailAddress::new(email) {
mailto.to.push(email);
}
});
// Extract query parameters
for (key, value) in url.query_pairs() {
if key == "subject" {
mailto.subject = Some(value.to_string());
} else if key == "body" {
mailto.body = Some(value.to_string());
}
}
Some(mailto)
} else {
None
}
} else {
None
}
}
///
/// Represents an email address, right now just the `name@domain` portion.
///
@@ -724,18 +761,18 @@ mod tests {
let raw = include_bytes!("../test-data/message/wrong-html.eml");
let expected =
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 06 Aug 2020 16:40:31 +0000\n\
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 06 Aug 2020 16:40:32 +0000";
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 6 Aug 2020 16:40:31 +0000\n\
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 6 Aug 2020 16:40:32 +0000";
check_parse_receive_headers(raw, expected);
let raw = include_bytes!("../test-data/message/posteo_ndn.eml");
let expected =
"Hop: By: mout01.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 09 Jun 2020 18:44:24 +0000";
"Hop: By: mout01.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 9 Jun 2020 18:44:24 +0000";
check_parse_receive_headers(raw, expected);
}
@@ -1283,4 +1320,55 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
assert_eq!(remove_subject_prefix("Fwd: Subject"), "Subject");
assert_eq!(remove_subject_prefix("Fw: Subject"), "Subject");
}
#[test]
fn test_parse_mailto() {
let mailto_url = "mailto:someone@example.com";
let reps = parse_mailto(mailto_url);
assert_eq!(
Some(MailTo {
to: vec![EmailAddress {
local: "someone".to_string(),
domain: "example.com".to_string()
}],
subject: None,
body: None
}),
reps
);
let mailto_url = "mailto:someone@example.com?subject=Hello%20World";
let reps = parse_mailto(mailto_url);
assert_eq!(
Some(MailTo {
to: vec![EmailAddress {
local: "someone".to_string(),
domain: "example.com".to_string()
}],
subject: Some("Hello World".to_string()),
body: None
}),
reps
);
let mailto_url = "mailto:someone@example.com,someoneelse@example.com?subject=Hello%20World&body=This%20is%20a%20test";
let reps = parse_mailto(mailto_url);
assert_eq!(
Some(MailTo {
to: vec![
EmailAddress {
local: "someone".to_string(),
domain: "example.com".to_string()
},
EmailAddress {
local: "someoneelse".to_string(),
domain: "example.com".to_string()
}
],
subject: Some("Hello World".to_string()),
body: Some("This is a test".to_string())
}),
reps
);
}
}