mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 15:02:11 +03:00
Compare commits
132 Commits
link2xt/tc
...
py-1.107.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e004c12b | ||
|
|
52a4b0c2b8 | ||
|
|
74abb82de2 | ||
|
|
37f20c6889 | ||
|
|
5dfe7bea8e | ||
|
|
6067622c19 | ||
|
|
fac7b064b4 | ||
|
|
ef6f252842 | ||
|
|
b8da19e49f | ||
|
|
a483df8b20 | ||
|
|
41ccc13394 | ||
|
|
0978357c5f | ||
|
|
7935085e74 | ||
|
|
7a47c9e38b | ||
|
|
20124bfca0 | ||
|
|
eaeaa297c7 | ||
|
|
9adb9ab5f4 | ||
|
|
c4c4c977a6 | ||
|
|
7d508dcb52 | ||
|
|
773754d74f | ||
|
|
ed20a23297 | ||
|
|
4615c84f31 | ||
|
|
677136f4ab | ||
|
|
3c3710420b | ||
|
|
de268b8225 | ||
|
|
42c709e7b1 | ||
|
|
cf0349acc8 | ||
|
|
e43b36b61f | ||
|
|
ed24867309 | ||
|
|
3cf78749df | ||
|
|
badbf766bb | ||
|
|
5b265dbc1c | ||
|
|
0053e143e7 | ||
|
|
1c44135b41 | ||
|
|
5f883a4445 | ||
|
|
8dc6ff268d | ||
|
|
57d7df530b | ||
|
|
13b2fe8d30 | ||
|
|
d644988845 | ||
|
|
27c6cfc958 | ||
|
|
3b9a48ff5f | ||
|
|
a5354ded3f | ||
|
|
0b07dafe77 | ||
|
|
f0e900b885 | ||
|
|
f460043e87 | ||
|
|
8c6b804d73 | ||
|
|
790512d52a | ||
|
|
89c8d26968 | ||
|
|
6d9d31cad1 | ||
|
|
6642083f52 | ||
|
|
554090b03e | ||
|
|
1cde09c312 | ||
|
|
e215b4d919 | ||
|
|
5ae6c25394 | ||
|
|
68cd8dbc3e | ||
|
|
01fe88e337 | ||
|
|
2b8923931e | ||
|
|
8d119713bc | ||
|
|
07f2e28eca | ||
|
|
0e849609f4 | ||
|
|
120a7a3090 | ||
|
|
872cd10b8b | ||
|
|
7e673fee90 | ||
|
|
847611aed4 | ||
|
|
15674111b4 | ||
|
|
1b2feeb99c | ||
|
|
f2f211908d | ||
|
|
ac9e3c6260 | ||
|
|
58ba107d01 | ||
|
|
087b4289e5 | ||
|
|
3df5f6110c | ||
|
|
6efb2a2054 | ||
|
|
4f8593f46a | ||
|
|
9eb7a84d83 | ||
|
|
8e65e794bc | ||
|
|
ecc7758788 | ||
|
|
6e40fd8000 | ||
|
|
f4c674fa98 | ||
|
|
eba96eb904 | ||
|
|
3986bb6c4e | ||
|
|
10349b7be4 | ||
|
|
d8f5e81880 | ||
|
|
ea81d08c01 | ||
|
|
f69acaa13d | ||
|
|
754c7324f5 | ||
|
|
2b4e32d2cf | ||
|
|
9ed933f835 | ||
|
|
c4b3579c60 | ||
|
|
c5d1802346 | ||
|
|
d873f88b56 | ||
|
|
17781066a2 | ||
|
|
ac0fbaad21 | ||
|
|
8ac7f639d8 | ||
|
|
3ac1d30a3a | ||
|
|
1f420777af | ||
|
|
37a212ddc4 | ||
|
|
138e62e1ef | ||
|
|
5b12784589 | ||
|
|
c9ab9d59c2 | ||
|
|
ac15b3a5af | ||
|
|
468356b120 | ||
|
|
f0a28b9168 | ||
|
|
c8f0c6b5f6 | ||
|
|
e653531934 | ||
|
|
3444c2aadd | ||
|
|
7aa7548a51 | ||
|
|
5b3596987b | ||
|
|
1e5c90ed65 | ||
|
|
f694d2e150 | ||
|
|
9738d53a82 | ||
|
|
e1d9dac70c | ||
|
|
e6324e3a19 | ||
|
|
4489db76c9 | ||
|
|
67ffada4b3 | ||
|
|
9aaf5cf914 | ||
|
|
08af7419af | ||
|
|
035b711ee3 | ||
|
|
5ad25dedf8 | ||
|
|
de47aa8466 | ||
|
|
9a78bd6e3f | ||
|
|
08cbb66a55 | ||
|
|
824cf93494 | ||
|
|
00d2f2e7b4 | ||
|
|
968ad2859e | ||
|
|
11ca12e43c | ||
|
|
15fad5476e | ||
|
|
4e468fdf24 | ||
|
|
cb4b9fce30 | ||
|
|
a562348dfa | ||
|
|
bcef1c7a76 | ||
|
|
4bbb83826c | ||
|
|
b9dbf1873d |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -67,9 +67,10 @@ jobs:
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.64.0
|
||||
python: 3.9
|
||||
@@ -143,7 +144,7 @@ jobs:
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e py3
|
||||
run: tox -e py3,lint
|
||||
|
||||
- name: install pypy
|
||||
if: ${{ matrix.python }}
|
||||
|
||||
5
.github/workflows/jsonrpc.yml
vendored
5
.github/workflows/jsonrpc.yml
vendored
@@ -19,11 +19,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: npm install
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.50.0
|
||||
toolchain: 1.66.0
|
||||
override: true
|
||||
|
||||
- name: build
|
||||
|
||||
1
.github/workflows/upload-docs.yml
vendored
1
.github/workflows/upload-docs.yml
vendored
@@ -13,7 +13,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat --no-deps
|
||||
|
||||
1
.github/workflows/upload-ffi-docs.yml
vendored
1
.github/workflows/upload-ffi-docs.yml
vendored
@@ -13,7 +13,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -2,6 +2,57 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## Changes
|
||||
|
||||
## Fixes
|
||||
|
||||
## API-Changes
|
||||
|
||||
|
||||
## 1.107.1
|
||||
|
||||
### Changes
|
||||
- Log server security (TLS/STARTTLS/plain) type #4005
|
||||
|
||||
### Fixes
|
||||
- Disable SMTP pipelining #4006
|
||||
|
||||
|
||||
## 1.107.0
|
||||
|
||||
### Changes
|
||||
- Pipeline SMTP commands #3924
|
||||
- Cache DNS results #3970
|
||||
|
||||
### Fixes
|
||||
- Securejoin: Fix adding and handling Autocrypt-Gossip headers #3914
|
||||
- fix verifier-by addr was empty string intead of None #3961
|
||||
- Emit DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived chats with
|
||||
unread messages increases #3959
|
||||
- Fix Peerstate comparison #3962
|
||||
- Log SOCKS5 configuration for IMAP like already done for SMTP #3964
|
||||
- Fix SOCKS5 usage for IMAP #3965
|
||||
- Exit from recently seen loop on interrupt channel errors to avoid busy looping #3966
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add verified-by information to `Contact`-Object
|
||||
- Remove `attach_selfavatar` config #3951
|
||||
|
||||
|
||||
## 1.106.0
|
||||
|
||||
### Changes
|
||||
- Only send IncomingMsgBunch if there are more than 0 new messages #3941
|
||||
|
||||
### Fixes
|
||||
- fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938
|
||||
- Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943
|
||||
- Do not treat invalid email addresses as an exception #3942
|
||||
- Add timeouts to HTTP requests #3948
|
||||
|
||||
|
||||
## 1.105.0
|
||||
|
||||
### Changes
|
||||
- Validate signatures in try_decrypt() even if the message isn't encrypted #3859
|
||||
- Don't parse the message again after detached signatures validation #3862
|
||||
@@ -9,16 +60,26 @@
|
||||
- cargo: bump quick-xml from 0.23.0 to 0.26.0 #3722
|
||||
- Add fuzzing tests #3853
|
||||
- Add mappings for some file types to Viewtype / MIME type #3881
|
||||
- Set `TCP_NODELAY` on IMAP sockets #3883
|
||||
- Buffer IMAP client writes #3888
|
||||
- move `DC_CHAT_ID_ARCHIVED_LINK` to the top of chat lists
|
||||
and make `dc_get_fresh_msg_cnt()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3918
|
||||
- make `dc_marknoticed_chat()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3919
|
||||
- Update provider database
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add python API for webxdc updates #3872
|
||||
- jsonrpc: add fresh message count to ChatListItemFetchResult::ArchiveLink
|
||||
- Add ffi functions to retrieve `verified by` information #3786
|
||||
- resultify `Message::get_filebytes()` #3925
|
||||
|
||||
### Fixes
|
||||
- Do not add an error if the message is encrypted but not signed #3860
|
||||
- Do not strip leading spaces from message lines #3867
|
||||
- Fix uncaught exception in JSON-RPC tests #3884
|
||||
- Fix STARTTLS connection and add a test for it #3907
|
||||
- Trigger reconnection when failing to fetch existing messages #3911
|
||||
- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913
|
||||
- Ensure format=flowed formatting is always reversible on the receiver side #3880
|
||||
|
||||
|
||||
## 1.104.0
|
||||
|
||||
115
Cargo.lock
generated
115
Cargo.lock
generated
@@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.17.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
|
||||
checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@@ -86,9 +86,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.66"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
||||
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
|
||||
|
||||
[[package]]
|
||||
name = "ascii_utils"
|
||||
@@ -340,15 +340,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.66"
|
||||
version = "0.3.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
|
||||
checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.5.3",
|
||||
"miniz_oxide 0.6.2",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
@@ -371,6 +371,12 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.5.1"
|
||||
@@ -861,13 +867,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.2"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
|
||||
checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -877,7 +883,7 @@ dependencies = [
|
||||
"async-smtp",
|
||||
"async_zip",
|
||||
"backtrace",
|
||||
"base64 0.13.1",
|
||||
"base64 0.20.0",
|
||||
"bitflags",
|
||||
"chrono",
|
||||
"criterion",
|
||||
@@ -912,6 +918,7 @@ dependencies = [
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rand 0.8.5",
|
||||
"ratelimit",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
@@ -941,7 +948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -963,7 +970,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat-jsonrpc",
|
||||
@@ -986,7 +993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1598,9 +1605,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.26.2"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
|
||||
checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
@@ -1752,9 +1759,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.2"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
@@ -2030,9 +2037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.137"
|
||||
version = "0.2.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2100,9 +2107,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
version = "0.13.8"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32"
|
||||
checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
|
||||
dependencies = [
|
||||
"charset",
|
||||
"data-encoding",
|
||||
@@ -2336,28 +2343,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
|
||||
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.19",
|
||||
"hermit-abi 0.2.6",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.29.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
|
||||
checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -2739,27 +2746,27 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.26.0"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
|
||||
checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.21"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
||||
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
|
||||
checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
@@ -2872,6 +2879,10 @@ dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratelimit"
|
||||
version = "1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.5.3"
|
||||
@@ -3184,18 +3195,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.148"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
|
||||
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.148"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
|
||||
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3204,9 +3215,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.89"
|
||||
version = "1.0.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
|
||||
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -3413,9 +3424,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.105"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
|
||||
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3482,18 +3493,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
|
||||
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.37"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
|
||||
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3538,9 +3549,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.22.0"
|
||||
version = "1.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
|
||||
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -3553,7 +3564,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"winapi",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3641,9 +3652,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||
checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.63"
|
||||
@@ -20,6 +20,7 @@ panic = 'abort'
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1"
|
||||
@@ -30,7 +31,7 @@ trust-dns-resolver = "0.22"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
base64 = "0.20"
|
||||
bitflags = "1.3"
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
dirs = { version = "4", optional=true }
|
||||
@@ -44,16 +45,16 @@ kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
log = {version = "0.4.16", optional = true }
|
||||
mailparse = "0.13"
|
||||
mailparse = "0.14"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.14"
|
||||
num_cpus = "1.15"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.16.0"
|
||||
once_cell = "1.17.0"
|
||||
percent-encoding = "2.2"
|
||||
pgp = { version = "0.9", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.26"
|
||||
quick-xml = "0.27"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.8"
|
||||
@@ -101,6 +102,7 @@ members = [
|
||||
"deltachat_derive",
|
||||
"deltachat-jsonrpc",
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-ratelimit",
|
||||
"format-flowed",
|
||||
]
|
||||
|
||||
|
||||
BIN
assets/icon-archive.png
Normal file
BIN
assets/icon-archive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
60
assets/icon-archive.svg
Normal file
60
assets/icon-archive.svg
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="60"
|
||||
height="60"
|
||||
viewBox="0 0 60 60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-archive"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="icon-archive.svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
inkscape:export-filename="icon-archive.png"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs12" />
|
||||
<sodipodi:namedview
|
||||
id="namedview10"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.4597151"
|
||||
inkscape:cx="24.459283"
|
||||
inkscape:cy="32.509174"
|
||||
inkscape:window-width="1457"
|
||||
inkscape:window-height="860"
|
||||
inkscape:window-x="55"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg8" />
|
||||
<g
|
||||
id="g846"
|
||||
transform="translate(0.558605,0.464417)">
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#808080;stroke-width:1.78186;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 38.749006,25.398867 V 38.843194 H 20.133784 V 25.398867"
|
||||
id="path847" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#808080;stroke-width:1.78186;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 18.065427,20.227972 h 22.751936 v 5.170894 H 18.065427 Z"
|
||||
id="path845" />
|
||||
<path
|
||||
style="fill:#ff0000;fill-opacity:1;stroke:#808080;stroke-width:1.78186;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 27.373036,29.535581 h 4.136718"
|
||||
id="line6" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::accounts::Accounts;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn create_accounts(n: u32) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
use std::path::Path;
|
||||
|
||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||
let id = 100;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -24,7 +24,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
once_cell = "1.16.0"
|
||||
once_cell = "1.17.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -1227,7 +1227,11 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
||||
* Get the number of _fresh_ messages in a chat.
|
||||
* Typically used to implement a badge with a number in the chatlist.
|
||||
*
|
||||
* If the specified chat is muted,
|
||||
* As muted archived chats are not unarchived automatically,
|
||||
* a similar information is needed for the @ref dc_get_chatlist() "archive link" as well:
|
||||
* here, the number of archived chats containing fresh messages is returned.
|
||||
*
|
||||
* If the specified chat is muted or the @ref dc_get_chatlist() "archive link",
|
||||
* the UI should show the badge counter "less obtrusive",
|
||||
* e.g. using "gray" instead of "red" color.
|
||||
*
|
||||
@@ -5807,7 +5811,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* @param data2 (int) The progress as:
|
||||
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
* 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
* 1000=Protocol finished for this contact.
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
||||
|
||||
@@ -23,13 +23,6 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
@@ -37,21 +30,28 @@ use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
mod dc_array;
|
||||
mod lot;
|
||||
|
||||
mod string;
|
||||
use self::string::*;
|
||||
use deltachat::chatlist::Chatlist;
|
||||
|
||||
use self::string::*;
|
||||
|
||||
// as C lacks a good and portable error handling,
|
||||
// in general, the C Interface is forgiving wrt to bad parameters.
|
||||
// - objects returned by some functions
|
||||
@@ -3309,6 +3309,8 @@ pub unsafe extern "C" fn dc_msg_get_filebytes(msg: *mut dc_msg_t) -> u64 {
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(ffi_msg.message.get_filebytes(ctx))
|
||||
.unwrap_or_log_default(ctx, "Cannot get file size")
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3973,13 +3975,10 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr(
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
let ctx = &*ffi_contact.context;
|
||||
block_on(Contact::get_verifier_addr(
|
||||
ctx,
|
||||
&ffi_contact.contact.get_id(),
|
||||
))
|
||||
.log_err(ctx, "failed to get verifier for contact")
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
block_on(ffi_contact.contact.get_verifier_addr(ctx))
|
||||
.log_err(ctx, "failed to get verifier for contact")
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3990,12 +3989,12 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
let ctx = &*ffi_contact.context;
|
||||
let contact_id = block_on(Contact::get_verifier_id(ctx, &ffi_contact.contact.get_id()))
|
||||
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx))
|
||||
.log_err(ctx, "failed to get verifier")
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
contact_id.to_u32()
|
||||
verifier_contact_id.to_u32()
|
||||
}
|
||||
// dc_lot_t
|
||||
|
||||
@@ -4578,11 +4577,12 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use super::*;
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! # Legacy generic return values for C API.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
use crate::message::MessageState;
|
||||
use crate::qr::Qr;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
use anyhow::Error;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
|
||||
@@ -287,9 +287,10 @@ fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libc::{free, strcmp};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_os_str_to_c_string_cwd() {
|
||||
let some_dir = std::env::current_dir().unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -20,10 +20,10 @@ tempfile = "3.3.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.25" }
|
||||
serde_json = "1.0.89"
|
||||
serde_json = "1.0.91"
|
||||
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.22.0" }
|
||||
tokio = { version = "1.23.1" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.2"
|
||||
|
||||
@@ -32,7 +32,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.22.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.23.1", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::{
|
||||
chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
|
||||
@@ -23,21 +28,14 @@ use deltachat::{
|
||||
webxdc::StatusUpdateSerial,
|
||||
};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use tokio::{fs, sync::RwLock};
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
pub use deltachat::accounts::Accounts;
|
||||
|
||||
pub mod events;
|
||||
pub mod types;
|
||||
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::QrObject;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::chat_list::ChatListEntry;
|
||||
@@ -53,8 +51,8 @@ use self::types::{
|
||||
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::QrObject;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommandApi {
|
||||
|
||||
@@ -48,12 +48,10 @@ pub enum ChatListItemFetchResult {
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
},
|
||||
ArchiveLink,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Error {
|
||||
id: u32,
|
||||
error: String,
|
||||
},
|
||||
ArchiveLink { fresh_message_counter: usize },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Error { id: u32, error: String },
|
||||
}
|
||||
|
||||
pub(crate) async fn get_chat_list_item_by_id(
|
||||
@@ -66,8 +64,12 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
_ => Some(MsgId::new(entry.1)),
|
||||
};
|
||||
|
||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||
|
||||
if chat_id.is_archived_link() {
|
||||
return Ok(ChatListItemFetchResult::ArchiveLink);
|
||||
return Ok(ChatListItemFetchResult::ArchiveLink {
|
||||
fresh_message_counter,
|
||||
});
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(ctx, chat_id).await?;
|
||||
@@ -111,7 +113,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
(None, false)
|
||||
};
|
||||
|
||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
|
||||
|
||||
Ok(ChatListItemFetchResult::ChatListItem {
|
||||
|
||||
@@ -20,6 +20,10 @@ pub struct ContactObject {
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
is_verified: bool,
|
||||
/// the address that verified this contact
|
||||
verifier_addr: Option<String>,
|
||||
/// the id of the contact that verified this contact
|
||||
verifier_id: Option<u32>,
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
@@ -36,6 +40,18 @@ impl ContactObject {
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
|
||||
|
||||
let (verifier_addr, verifier_id) = if is_verified {
|
||||
(
|
||||
contact.get_verifier_addr(context).await?,
|
||||
contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.map(|contact_id| contact_id.to_u32()),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
Ok(ContactObject {
|
||||
address: contact.get_addr().to_owned(),
|
||||
color: color_int_to_hex_string(contact.get_color()),
|
||||
@@ -48,6 +64,8 @@ impl ContactObject {
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_verified,
|
||||
verifier_addr,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ impl MessageObject {
|
||||
|
||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
||||
let file_bytes = message.get_filebytes(context).await;
|
||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||
let override_sender_name = message.get_override_sender_name();
|
||||
|
||||
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||
|
||||
@@ -4,12 +4,13 @@ pub use yerpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api::{Accounts, CommandApi};
|
||||
use async_channel::unbounded;
|
||||
use futures::StreamExt;
|
||||
use tempfile::TempDir;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
use super::api::{Accounts, CommandApi};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||
use yerpc::axum::handle_ws_rpc;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.104.0"
|
||||
"version": "1.107.1"
|
||||
}
|
||||
@@ -12,7 +12,7 @@ describe("online tests", function () {
|
||||
let accountId1: number, accountId2: number;
|
||||
|
||||
before(async function () {
|
||||
this.timeout(12000);
|
||||
this.timeout(60000);
|
||||
if (!process.env.DCC_NEW_TMP_EMAIL) {
|
||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||
console.error(
|
||||
|
||||
8
deltachat-ratelimit/Cargo.toml
Normal file
8
deltachat-ratelimit/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "ratelimit"
|
||||
version = "1.0.0"
|
||||
description = "Token bucket implementation"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
@@ -7,7 +7,7 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Ratelimit {
|
||||
pub struct Ratelimit {
|
||||
/// Time of the last update.
|
||||
last_update: SystemTime,
|
||||
|
||||
@@ -25,7 +25,7 @@ impl Ratelimit {
|
||||
/// Returns a new rate limiter with the given constraints.
|
||||
///
|
||||
/// Rate limiter will allow to send no more than `quota` messages within duration `window`.
|
||||
pub(crate) fn new(window: Duration, quota: f64) -> Self {
|
||||
pub fn new(window: Duration, quota: f64) -> Self {
|
||||
Self::new_at(window, quota, SystemTime::now())
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Ratelimit {
|
||||
/// Returns true if can send another message now.
|
||||
///
|
||||
/// This method takes mutable reference
|
||||
pub(crate) fn can_send(&self) -> bool {
|
||||
pub fn can_send(&self) -> bool {
|
||||
self.can_send_at(SystemTime::now())
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Ratelimit {
|
||||
/// It is possible to send message even if over quota, e.g. if the message sending is initiated
|
||||
/// by the user and should not be rate limited. However, sending messages when over quota
|
||||
/// further postpones the time when it will be allowed to send low priority messages.
|
||||
pub(crate) fn send(&mut self) {
|
||||
pub fn send(&mut self) {
|
||||
self.send_at(SystemTime::now())
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ impl Ratelimit {
|
||||
}
|
||||
|
||||
/// Calculates the time until `can_send` will return `true`.
|
||||
pub(crate) fn until_can_send(&self) -> Duration {
|
||||
pub fn until_can_send(&self) -> Duration {
|
||||
self.until_can_send_at(SystemTime::now())
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,7 @@ async def log_error(event):
|
||||
|
||||
@hooks.on(events.MemberListChanged)
|
||||
async def on_memberlist_changed(event):
|
||||
logging.info(
|
||||
"member %s was %s", event.member, "added" if event.member_added else "removed"
|
||||
)
|
||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupImageChanged)
|
||||
|
||||
@@ -27,3 +27,13 @@ deltachat_rpc_client = [
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "F", "W", "N", "YTT", "B", "C4", "ISC", "ICN", "PT", "RET", "SIM", "TID", "ARG", "DTZ", "ERA", "PLC", "PLE", "PLW", "PIE", "COM"]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
@@ -8,3 +8,18 @@ from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
"AttrDict",
|
||||
"Bot",
|
||||
"Chat",
|
||||
"Client",
|
||||
"Contact",
|
||||
"DeltaChat",
|
||||
"EventType",
|
||||
"Message",
|
||||
"Rpc",
|
||||
"run_bot_cli",
|
||||
"run_client_cli",
|
||||
]
|
||||
|
||||
@@ -30,12 +30,7 @@ class AttrDict(dict):
|
||||
"""Dictionary that allows accessing values usin the "dot notation" as attributes."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(
|
||||
{
|
||||
_camel_to_snake(key): _to_attrdict(value)
|
||||
for key, value in dict(*args, **kwargs).items()
|
||||
}
|
||||
)
|
||||
super().__init__({_camel_to_snake(key): _to_attrdict(value) for key, value in dict(*args, **kwargs).items()})
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in self:
|
||||
@@ -51,7 +46,7 @@ class AttrDict(dict):
|
||||
async def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Run a simple command line app, using the given hooks.
|
||||
|
||||
@@ -65,7 +60,7 @@ async def run_client_cli(
|
||||
async def run_bot_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Run a simple bot command line using the given hooks.
|
||||
|
||||
@@ -80,7 +75,7 @@ async def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
from .deltachat import DeltaChat
|
||||
from .rpc import Rpc
|
||||
@@ -107,12 +102,9 @@ async def _run_cli(
|
||||
client = client_type(account, hooks)
|
||||
client.logger.debug("Running deltachat core %s", core_version)
|
||||
if not await client.is_configured():
|
||||
assert (
|
||||
args.email and args.password
|
||||
), "Account is not configured and email and password must be provided"
|
||||
asyncio.create_task(
|
||||
client.configure(email=args.email, password=args.password)
|
||||
)
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
asyncio.create_task(client.configure(email=args.email, password=args.password))
|
||||
await client.run_forever()
|
||||
|
||||
|
||||
|
||||
@@ -89,9 +89,7 @@ class Account:
|
||||
"""Configure an account."""
|
||||
await self._rpc.configure(self.id)
|
||||
|
||||
async def create_contact(
|
||||
self, obj: Union[int, str, Contact], name: Optional[str] = None
|
||||
) -> Contact:
|
||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -120,10 +118,7 @@ class Account:
|
||||
async def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = await self._rpc.get_blocked_contacts(self.id)
|
||||
return [
|
||||
AttrDict(contact=Contact(self, contact["id"]), **contact)
|
||||
for contact in contacts
|
||||
]
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
async def get_contacts(
|
||||
self,
|
||||
@@ -148,10 +143,7 @@ class Account:
|
||||
|
||||
if snapshot:
|
||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
||||
return [
|
||||
AttrDict(contact=Contact(self, contact["id"]), **contact)
|
||||
for contact in contacts
|
||||
]
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
||||
return [Contact(self, contact_id) for contact_id in contacts]
|
||||
|
||||
@@ -192,9 +184,7 @@ class Account:
|
||||
if alldone_hint:
|
||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||
|
||||
entries = await self._rpc.get_chatlist_entries(
|
||||
self.id, flags, query, contact and contact.id
|
||||
)
|
||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
if not snapshot:
|
||||
return [Chat(self, entry[0]) for entry in entries]
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class Chat:
|
||||
"""
|
||||
if duration is not None:
|
||||
assert duration > 0, "Invalid duration"
|
||||
dur: Union[str, dict] = dict(Until=duration)
|
||||
dur: Union[str, dict] = {"Until": duration}
|
||||
else:
|
||||
dur = "Forever"
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
@@ -74,27 +74,19 @@ class Chat:
|
||||
|
||||
async def pin(self) -> None:
|
||||
"""Pin this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.PINNED
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
|
||||
async def unpin(self) -> None:
|
||||
"""Unpin this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.NORMAL
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def archive(self) -> None:
|
||||
"""Archive this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.ARCHIVED
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
|
||||
async def unarchive(self) -> None:
|
||||
"""Unarchive this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.NORMAL
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def set_name(self, name: str) -> None:
|
||||
"""Set name of this chat."""
|
||||
@@ -133,9 +125,7 @@ class Chat:
|
||||
if isinstance(quoted_msg, Message):
|
||||
quoted_msg = quoted_msg.id
|
||||
|
||||
msg_id, _ = await self._rpc.misc_send_msg(
|
||||
self.account.id, self.id, text, file, location, quoted_msg
|
||||
)
|
||||
msg_id, _ = await self._rpc.misc_send_msg(self.account.id, self.id, text, file, location, quoted_msg)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_text(self, text: str) -> Message:
|
||||
@@ -241,23 +231,17 @@ class Chat:
|
||||
timestamp_to: Optional[datetime] = None,
|
||||
) -> List[AttrDict]:
|
||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||
time_from = (
|
||||
calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
)
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
contact_id = contact.id if contact else 0
|
||||
|
||||
result = await self._rpc.get_locations(
|
||||
self.account.id, self.id, contact_id, time_from, time_to
|
||||
)
|
||||
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
loc = AttrDict(loc)
|
||||
loc["chat"] = self
|
||||
loc["contact"] = contacts.setdefault(
|
||||
loc.contact_id, Contact(self.account, loc.contact_id)
|
||||
)
|
||||
loc["contact"] = contacts.setdefault(loc.contact_id, Contact(self.account, loc.contact_id))
|
||||
loc["message"] = Message(self.account, loc.msg_id)
|
||||
locations.append(loc)
|
||||
return locations
|
||||
|
||||
@@ -47,15 +47,11 @@ class Client:
|
||||
self._should_process_messages = 0
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(
|
||||
self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]
|
||||
) -> None:
|
||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
def add_hook(
|
||||
self, hook: Callable, event: Union[type, EventFilter] = RawEvent
|
||||
) -> None:
|
||||
def add_hook(self, hook: Callable, event: Union[type, EventFilter] = RawEvent) -> None:
|
||||
"""Register hook for the given event filter."""
|
||||
if isinstance(event, type):
|
||||
event = event()
|
||||
@@ -64,7 +60,7 @@ class Client:
|
||||
isinstance(
|
||||
event,
|
||||
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
|
||||
)
|
||||
),
|
||||
)
|
||||
self._hooks.setdefault(type(event), set()).add((hook, event))
|
||||
|
||||
@@ -76,7 +72,7 @@ class Client:
|
||||
isinstance(
|
||||
event,
|
||||
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
|
||||
)
|
||||
),
|
||||
)
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
@@ -95,9 +91,7 @@ class Client:
|
||||
"""Process events forever."""
|
||||
await self.run_until(lambda _: False)
|
||||
|
||||
async def run_until(
|
||||
self, func: Callable[[AttrDict], Union[bool, Coroutine]]
|
||||
) -> AttrDict:
|
||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
@@ -122,9 +116,7 @@ class Client:
|
||||
if stop:
|
||||
return event
|
||||
|
||||
async def _on_event(
|
||||
self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
|
||||
) -> None:
|
||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if await evfilter.filter(event):
|
||||
try:
|
||||
@@ -133,11 +125,7 @@ class Client:
|
||||
self.logger.exception(ex)
|
||||
|
||||
async def _parse_command(self, event: AttrDict) -> None:
|
||||
cmds = [
|
||||
hook[1].command
|
||||
for hook in self._hooks.get(NewMessage, [])
|
||||
if hook[1].command
|
||||
]
|
||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||
payload = parts[1] if len(parts) > 1 else ""
|
||||
cmd = parts.pop(0)
|
||||
@@ -202,11 +190,7 @@ class Client:
|
||||
for message in await self.account.get_fresh_messages_in_arrival_order():
|
||||
snapshot = await message.get_snapshot()
|
||||
await self._on_new_msg(snapshot)
|
||||
if (
|
||||
snapshot.is_info
|
||||
and snapshot.system_message_type
|
||||
!= SystemMessageType.WEBXDC_INFO_MESSAGE
|
||||
):
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from .const import EventType
|
||||
|
||||
def _tuple_of(obj, type_: type) -> tuple:
|
||||
if not obj:
|
||||
return tuple()
|
||||
return ()
|
||||
if isinstance(obj, type_):
|
||||
obj = (obj,)
|
||||
|
||||
@@ -39,7 +39,7 @@ class EventFilter(ABC):
|
||||
"""Return True if two event filters are equal."""
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
return not self == other
|
||||
|
||||
async def _call_func(self, event) -> bool:
|
||||
if not self.func:
|
||||
@@ -65,9 +65,7 @@ class RawEvent(EventFilter):
|
||||
should be dispatched or not.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs
|
||||
):
|
||||
def __init__(self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
try:
|
||||
self.types = _tuple_of(types, EventType)
|
||||
|
||||
@@ -49,22 +49,14 @@ class Message:
|
||||
"""Mark the message as seen."""
|
||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
async def send_webxdc_status_update(
|
||||
self, update: Union[dict, str], description: str
|
||||
) -> None:
|
||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
update = json.dumps(update)
|
||||
await self._rpc.send_webxdc_status_update(
|
||||
self.account.id, self.id, update, description
|
||||
)
|
||||
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(
|
||||
await self._rpc.get_webxdc_status_updates(
|
||||
self.account.id, self.id, last_known_serial
|
||||
)
|
||||
)
|
||||
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
async def get_webxdc_info(self) -> dict:
|
||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
@@ -12,8 +12,11 @@ from .rpc import Rpc
|
||||
async def get_temp_credentials() -> dict:
|
||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
||||
|
||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url) as response:
|
||||
async with session.post(url, timeout=timeout) as response:
|
||||
return json.loads(await response.text())
|
||||
|
||||
|
||||
@@ -27,12 +30,17 @@ class ACFactory:
|
||||
async def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(await self.get_unconfigured_account())
|
||||
|
||||
async def new_configured_account(self) -> Account:
|
||||
async def new_preconfigured_account(self) -> Account:
|
||||
"""Make a new account with configuration options set, but configuration not started."""
|
||||
credentials = await get_temp_credentials()
|
||||
account = await self.get_unconfigured_account()
|
||||
assert not await account.is_configured()
|
||||
await account.set_config("addr", credentials["email"])
|
||||
await account.set_config("mail_pw", credentials["password"])
|
||||
assert not await account.is_configured()
|
||||
return account
|
||||
|
||||
async def new_configured_account(self) -> Account:
|
||||
account = await self.new_preconfigured_account()
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
return account
|
||||
@@ -59,9 +67,7 @@ class ACFactory:
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (await self.get_online_accounts(1))[0]
|
||||
to_contact = await from_account.create_contact(
|
||||
await to_account.get_config("addr")
|
||||
)
|
||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
||||
if group:
|
||||
to_chat = await from_account.create_group(group)
|
||||
await to_chat.add_contact(to_contact)
|
||||
|
||||
@@ -30,7 +30,7 @@ class Rpc:
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
**self._kwargs
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
@@ -46,7 +46,7 @@ class Rpc:
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
||||
await self.close()
|
||||
|
||||
async def reader_loop(self) -> None:
|
||||
@@ -97,5 +97,6 @@ class Rpc:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
return method
|
||||
|
||||
@@ -6,14 +6,14 @@ from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_system_info(rpc) -> None:
|
||||
system_info = await rpc.get_system_info()
|
||||
assert "arch" in system_info
|
||||
assert "deltachat_core_version" in system_info
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
@@ -27,7 +27,7 @@ async def test_email_address_validity(rpc) -> None:
|
||||
assert not await rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
while True:
|
||||
@@ -41,7 +41,17 @@ async def test_acfactory(acfactory) -> None:
|
||||
print("Successful configuration")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_configure_starttls(acfactory) -> None:
|
||||
account = await acfactory.new_preconfigured_account()
|
||||
|
||||
# Use STARTTLS
|
||||
await account.set_config("mail_security", "2")
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_account(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -101,7 +111,7 @@ async def test_account(acfactory) -> None:
|
||||
await alice.stop_io()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_chat(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -167,7 +177,7 @@ async def test_chat(acfactory) -> None:
|
||||
await group.get_locations()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_contact(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -185,7 +195,7 @@ async def test_contact(acfactory) -> None:
|
||||
await alice_contact_bob.create_chat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_message(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -216,7 +226,7 @@ async def test_message(acfactory) -> None:
|
||||
await message.send_reaction("😎")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_bot(acfactory) -> None:
|
||||
mock = MagicMock()
|
||||
user = (await acfactory.get_online_accounts(1))[0]
|
||||
@@ -227,25 +237,20 @@ async def test_bot(acfactory) -> None:
|
||||
|
||||
hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG)
|
||||
bot.add_hook(*hook)
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="Hello!"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
track = lambda e: mock.hook(e.message_snapshot.id)
|
||||
def track(e):
|
||||
mock.hook(e.message_snapshot.id)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="hello"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="hello!"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
@@ -253,7 +258,5 @@ async def test_bot(acfactory) -> None:
|
||||
|
||||
mock.hook.reset_mock()
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="/help"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
@@ -3,16 +3,14 @@ import pytest
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_webxdc(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_message(
|
||||
text="Let's play chess!", file="../test-data/webxdc/chess.xdc"
|
||||
)
|
||||
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py3
|
||||
lint
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
@@ -16,3 +17,13 @@ deps =
|
||||
pytest-asyncio
|
||||
aiohttp
|
||||
aiodns
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
ruff
|
||||
black
|
||||
commands =
|
||||
black --check src/ examples/ tests/
|
||||
ruff src/ examples/ tests/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.104.0"
|
||||
version = "1.107.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -19,7 +19,7 @@ anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "1.12.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.89"
|
||||
serde_json = "1.0.91"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.22.0", features = ["io-std"] }
|
||||
tokio = { version = "1.23.1", features = ["io-std"] }
|
||||
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![recursion_limit = "128"]
|
||||
extern crate proc_macro;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
|
||||
// For now, assume (not check) that these macroses are applied to enum without
|
||||
// data. If this assumption is violated, compiler error will point to
|
||||
// generated code, which is not very user-friendly.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use tempfile::tempdir;
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
@@ -8,6 +6,7 @@ use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
fn format_line_flowed(line: &str, prefix: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut buffer = prefix.to_string();
|
||||
let mut after_space = false;
|
||||
let mut after_space = prefix.ends_with(' ');
|
||||
|
||||
for c in line.chars() {
|
||||
if c == ' ' {
|
||||
@@ -55,7 +55,7 @@ fn format_line_flowed(line: &str, prefix: &str) -> String {
|
||||
result + &buffer
|
||||
}
|
||||
|
||||
/// Returns text formatted according to RFC 3767 (format=flowed).
|
||||
/// Returns text formatted according to RFC 3676 (format=flowed).
|
||||
///
|
||||
/// This function accepts text separated by LF, but returns text
|
||||
/// separated by CRLF.
|
||||
@@ -70,23 +70,20 @@ pub fn format_flowed(text: &str) -> String {
|
||||
result += "\r\n";
|
||||
}
|
||||
|
||||
let line_no_prefix = line
|
||||
.strip_prefix('>')
|
||||
.map(|line| line.strip_prefix(' ').unwrap_or(line));
|
||||
let is_quote = line_no_prefix.is_some();
|
||||
let line = line_no_prefix.unwrap_or(line).trim_end();
|
||||
let prefix = if is_quote { "> " } else { "" };
|
||||
let line = line.trim_end();
|
||||
let quote_depth = line.chars().take_while(|&c| c == '>').count();
|
||||
let (prefix, mut line) = line.split_at(quote_depth);
|
||||
|
||||
if prefix.len() + line.len() > 78 {
|
||||
result += &format_line_flowed(line, prefix);
|
||||
} else {
|
||||
result += prefix;
|
||||
if prefix.is_empty() && (line.starts_with('>') || line.starts_with(' ')) {
|
||||
// Space stuffing, see RFC 3676
|
||||
result.push(' ');
|
||||
let mut prefix = prefix.to_string();
|
||||
|
||||
if quote_depth > 0 {
|
||||
if let Some(s) = line.strip_prefix(' ') {
|
||||
line = s;
|
||||
prefix += " ";
|
||||
}
|
||||
result += line;
|
||||
}
|
||||
|
||||
result += &format_line_flowed(line, &prefix);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -111,9 +108,6 @@ pub fn format_flowed_quote(text: &str) -> String {
|
||||
///
|
||||
/// Lines must be separated by single LF.
|
||||
///
|
||||
/// Quote processing is not supported, it is assumed that they are
|
||||
/// deleted during simplification.
|
||||
///
|
||||
/// Signature separator line is not processed here, it is assumed to
|
||||
/// be stripped beforehand.
|
||||
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
|
||||
@@ -121,6 +115,12 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
|
||||
let mut skip_newline = true;
|
||||
|
||||
for line in text.split('\n') {
|
||||
let line = if !result.is_empty() && skip_newline {
|
||||
line.trim_start_matches('>')
|
||||
} else {
|
||||
line
|
||||
};
|
||||
|
||||
// Revert space-stuffing
|
||||
let line = line.strip_prefix(' ').unwrap_or(line);
|
||||
|
||||
@@ -150,8 +150,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_format_flowed() {
|
||||
let text = "";
|
||||
assert_eq!(format_flowed(text), "");
|
||||
|
||||
let text = "Foo bar baz";
|
||||
assert_eq!(format_flowed(text), "Foo bar baz");
|
||||
assert_eq!(format_flowed(text), text);
|
||||
|
||||
let text = ">Foo bar";
|
||||
assert_eq!(format_flowed(text), text);
|
||||
|
||||
let text = "> Foo bar";
|
||||
assert_eq!(format_flowed(text), text);
|
||||
|
||||
let text = ">\n\nA";
|
||||
assert_eq!(format_flowed(text), ">\r\n\r\nA");
|
||||
|
||||
let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
||||
\n\
|
||||
@@ -165,17 +177,33 @@ mod tests {
|
||||
let text = "> A quote";
|
||||
assert_eq!(format_flowed(text), "> A quote");
|
||||
|
||||
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
||||
assert_eq!(
|
||||
format_flowed(text),
|
||||
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A"
|
||||
);
|
||||
|
||||
// Test space stuffing of wrapped lines
|
||||
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
||||
> \n\
|
||||
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
|
||||
let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
|
||||
> clients.\r\n\
|
||||
> \r\n\
|
||||
>\r\n\
|
||||
> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
|
||||
> client and enter the setup code presented on the generating device.";
|
||||
assert_eq!(format_flowed(text), expected);
|
||||
|
||||
let text = ">> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
||||
>> \n\
|
||||
>> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
|
||||
let expected = ">> This is the Autocrypt Setup Message used to transfer your key between \r\n\
|
||||
>> clients.\r\n\
|
||||
>>\r\n\
|
||||
>> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
|
||||
>> client and enter the setup code presented on the generating device.";
|
||||
assert_eq!(format_flowed(text), expected);
|
||||
|
||||
// Test space stuffing of spaces.
|
||||
let text = " Foo bar baz";
|
||||
assert_eq!(format_flowed(text), " Foo bar baz");
|
||||
@@ -202,6 +230,12 @@ mod tests {
|
||||
let text = " Foo bar";
|
||||
let expected = " Foo bar";
|
||||
assert_eq!(unformat_flowed(text, false), expected);
|
||||
|
||||
let text =
|
||||
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A";
|
||||
let expected =
|
||||
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
||||
assert_eq!(unformat_flowed(text, false), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
35
fuzz/Cargo.lock
generated
35
fuzz/Cargo.lock
generated
@@ -233,6 +233,12 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.5.3"
|
||||
@@ -702,7 +708,7 @@ dependencies = [
|
||||
"async-smtp",
|
||||
"async_zip",
|
||||
"backtrace",
|
||||
"base64 0.13.1",
|
||||
"base64 0.20.0",
|
||||
"bitflags",
|
||||
"chrono",
|
||||
"deltachat_derive",
|
||||
@@ -719,7 +725,7 @@ dependencies = [
|
||||
"kamadak-exif",
|
||||
"lettre_email",
|
||||
"libc",
|
||||
"mailparse",
|
||||
"mailparse 0.14.0",
|
||||
"native-tls",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
@@ -764,7 +770,7 @@ dependencies = [
|
||||
"bolero",
|
||||
"deltachat",
|
||||
"format-flowed",
|
||||
"mailparse",
|
||||
"mailparse 0.13.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1691,6 +1697,17 @@ dependencies = [
|
||||
"quoted_printable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
|
||||
dependencies = [
|
||||
"charset",
|
||||
"data-encoding",
|
||||
"quoted_printable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
@@ -1887,9 +1904,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -2195,9 +2212,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.26.0"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
|
||||
checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2846,9 +2863,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.23.0"
|
||||
version = "1.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
|
||||
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
|
||||
@@ -10,10 +10,7 @@ fn round_trip(input: &str) -> String {
|
||||
fn main() {
|
||||
check!().for_each(|data: &[u8]| {
|
||||
if let Ok(input) = std::str::from_utf8(data.into()) {
|
||||
let mut input = input.to_string();
|
||||
|
||||
// Only consider inputs that don't contain quotes.
|
||||
input.retain(|c| c != '>');
|
||||
let input = input.trim().to_string();
|
||||
|
||||
// Only consider inputs that are the result of unformatting format=flowed text.
|
||||
// At least this means that lines don't contain any trailing whitespace.
|
||||
|
||||
@@ -28,7 +28,7 @@ This code used to live at [`deltachat-node`](https://github.com/deltachat/deltac
|
||||
|
||||
## Install
|
||||
|
||||
By default the installation will build try to use the bundled prebuilds in the
|
||||
By default the installation will try to use the bundled prebuilds in the
|
||||
npm package. If this fails it falls back to compile `../deltachat-core-rust` from
|
||||
this repository, using `scripts/rebuild-core.js`.
|
||||
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
// @ts-check
|
||||
import DeltaChat, { Message } from '../dist'
|
||||
import binding from '../binding'
|
||||
import DeltaChat from '../dist'
|
||||
|
||||
import { deepEqual, deepStrictEqual, strictEqual } from 'assert'
|
||||
import { deepStrictEqual, strictEqual } from 'assert'
|
||||
import chai, { expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { EventId2EventName, C } from '../dist/constants'
|
||||
import { join } from 'path'
|
||||
import { mkdtempSync, statSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context'
|
||||
import fetch from 'node-fetch'
|
||||
chai.use(chaiAsPromised)
|
||||
chai.config.truncateThreshold = 0; // Do not truncate assertion errors.
|
||||
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
|
||||
|
||||
async function createTempUser(url) {
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
async function postData(url = '') {
|
||||
// Default options are marked with *
|
||||
const response = await fetch(url, {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
mode: 'cors', // no-cors, *cors, same-origin
|
||||
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers: {
|
||||
'cache-control': 'no-cache',
|
||||
},
|
||||
referrerPolicy: 'no-referrer', // no-referrer, *client
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('request failed: ' + response.body.read())
|
||||
}
|
||||
return response.json() // parses JSON response into native JavaScript objects
|
||||
}
|
||||
|
||||
|
||||
@@ -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.104.0"
|
||||
"version": "1.107.1"
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
||||
@@ -34,8 +34,10 @@ class GroupTrackingPlugin:
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr
|
||||
)
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
@@ -44,8 +46,10 @@ class GroupTrackingPlugin:
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr
|
||||
)
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ def datadir():
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip("test-data directory not found")
|
||||
pytest.skip("test-data directory not found")
|
||||
return None
|
||||
|
||||
|
||||
def test_echo_quit_plugin(acfactory, lp):
|
||||
@@ -47,7 +47,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_configure_completed*
|
||||
"""
|
||||
""",
|
||||
)
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
@@ -61,7 +61,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_chat_modified*bot test group*
|
||||
"""
|
||||
""",
|
||||
)
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
@@ -76,8 +76,9 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
"""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr, ac1.get_config("addr")
|
||||
)
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
@@ -86,6 +87,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
"""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr, ac1.get_config("addr")
|
||||
)
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -44,5 +44,9 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM"]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
profile = "black"
|
||||
|
||||
@@ -36,7 +36,8 @@ register_global_plugin(events)
|
||||
|
||||
def run_cmdline(argv=None, account_plugins=None):
|
||||
"""Run a simple default command line app, registering the specified
|
||||
account plugins."""
|
||||
account plugins.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
if argv is None:
|
||||
|
||||
@@ -102,8 +102,8 @@ def find_header(flags):
|
||||
printf("%s", _dc_header_file_location());
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
)
|
||||
""",
|
||||
),
|
||||
)
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
@@ -198,7 +198,7 @@ def ffibuilder():
|
||||
typedef int... time_t;
|
||||
void free(void *ptr);
|
||||
extern int dc_event_has_string_data(int);
|
||||
"""
|
||||
""",
|
||||
)
|
||||
function_defs = extract_functions(flags)
|
||||
defines = extract_defines(flags)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Account class implementation. """
|
||||
"""Account class implementation."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_core_info():
|
||||
ffi.gc(
|
||||
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -172,10 +172,7 @@ class Account(object):
|
||||
namebytes = name.encode("utf8")
|
||||
if isinstance(value, (int, bool)):
|
||||
value = str(int(value))
|
||||
if value is not None:
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
valuebytes = ffi.NULL
|
||||
valuebytes = value.encode("utf8") if value is not None else ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name: str) -> str:
|
||||
@@ -225,9 +222,10 @@ class Account(object):
|
||||
return bool(lib.dc_is_configured(self._dc_context))
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""Determine if account is open
|
||||
"""Determine if account is open.
|
||||
|
||||
:returns True if account is open."""
|
||||
:returns True if account is open.
|
||||
"""
|
||||
return bool(lib.dc_context_is_open(self._dc_context))
|
||||
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
@@ -543,7 +541,7 @@ class Account(object):
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def check_qr(self, qr):
|
||||
"""check qr code and return :class:`ScannedQRCode` instance representing the result"""
|
||||
"""check qr code and return :class:`ScannedQRCode` instance representing the result."""
|
||||
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref)
|
||||
lot = DCLot(res)
|
||||
if lot.state() == const.DC_QR_ERROR:
|
||||
@@ -662,7 +660,7 @@ class Account(object):
|
||||
return lib.dc_all_work_done(self._dc_context)
|
||||
|
||||
def start_io(self):
|
||||
"""start this account's IO scheduling (Rust-core async scheduler)
|
||||
"""start this account's IO scheduling (Rust-core async scheduler).
|
||||
|
||||
If this account is not configured an Exception is raised.
|
||||
You need to call account.configure() and account.wait_configure_finish()
|
||||
@@ -705,12 +703,10 @@ class Account(object):
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
def configure(self) -> ConfigureTracker:
|
||||
"""Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
|
||||
:param reconfigure: deprecated, doesn't need to be checked anymore.
|
||||
"""
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
@@ -733,7 +729,8 @@ class Account(object):
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
underlying dc_context).
|
||||
"""
|
||||
if self._dc_context is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Chat and Location related API. """
|
||||
"""Chat and Location related API."""
|
||||
|
||||
import calendar
|
||||
import json
|
||||
@@ -37,7 +37,7 @@ class Chat(object):
|
||||
return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
return not self == other
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
@@ -74,19 +74,19 @@ class Chat(object):
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_single(self) -> bool:
|
||||
"""Return True if this chat is a single/direct chat, False otherwise"""
|
||||
"""Return True if this chat is a single/direct chat, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
|
||||
|
||||
def is_mailinglist(self) -> bool:
|
||||
"""Return True if this chat is a mailing list, False otherwise"""
|
||||
"""Return True if this chat is a mailing list, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_MAILINGLIST
|
||||
|
||||
def is_broadcast(self) -> bool:
|
||||
"""Return True if this chat is a broadcast list, False otherwise"""
|
||||
"""Return True if this chat is a broadcast list, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_BROADCAST
|
||||
|
||||
def is_multiuser(self) -> bool:
|
||||
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise"""
|
||||
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_MAILINGLIST,
|
||||
@@ -94,11 +94,11 @@ class Chat(object):
|
||||
)
|
||||
|
||||
def is_self_talk(self) -> bool:
|
||||
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise"""
|
||||
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise."""
|
||||
return bool(lib.dc_chat_is_self_talk(self._dc_chat))
|
||||
|
||||
def is_device_talk(self) -> bool:
|
||||
"""Returns True if this chat is the "Device Messages" chat, False otherwise"""
|
||||
"""Returns True if this chat is the "Device Messages" chat, False otherwise."""
|
||||
return bool(lib.dc_chat_is_device_talk(self._dc_chat))
|
||||
|
||||
def is_muted(self) -> bool:
|
||||
@@ -109,12 +109,12 @@ class Chat(object):
|
||||
return bool(lib.dc_chat_is_muted(self._dc_chat))
|
||||
|
||||
def is_pinned(self) -> bool:
|
||||
"""Return True if this chat is pinned, False otherwise"""
|
||||
"""Return True if this chat is pinned, False otherwise."""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_PINNED
|
||||
|
||||
def is_archived(self) -> bool:
|
||||
"""Return True if this chat is archived, False otherwise.
|
||||
:returns: True if archived, False otherwise
|
||||
:returns: True if archived, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
|
||||
@@ -136,7 +136,7 @@ class Chat(object):
|
||||
|
||||
def can_send(self) -> bool:
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the contact requests or for the device-talk
|
||||
This is not true eg. for the contact requests or for the device-talk.
|
||||
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
@@ -167,7 +167,7 @@ class Chat(object):
|
||||
|
||||
def get_color(self):
|
||||
"""return the color of the chat.
|
||||
:returns: color as 0x00rrggbb
|
||||
:returns: color as 0x00rrggbb.
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
@@ -178,21 +178,18 @@ class Chat(object):
|
||||
return json.loads(s)
|
||||
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""mutes the chat
|
||||
"""mutes the chat.
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
:returns: None
|
||||
"""
|
||||
if duration is None:
|
||||
mute_duration = -1
|
||||
else:
|
||||
mute_duration = duration
|
||||
mute_duration = -1 if duration is None else duration
|
||||
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self) -> None:
|
||||
"""unmutes the chat
|
||||
"""unmutes the chat.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
@@ -252,7 +249,8 @@ class Chat(object):
|
||||
def get_encryption_info(self) -> Optional[str]:
|
||||
"""Return encryption info for this chat.
|
||||
|
||||
:returns: a string with encryption preferences of all chat members"""
|
||||
:returns: a string with encryption preferences of all chat members
|
||||
"""
|
||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
@@ -463,7 +461,7 @@ class Chat(object):
|
||||
|
||||
def get_contacts(self):
|
||||
"""get all contacts for this chat.
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat.
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
@@ -547,19 +545,10 @@ class Chat(object):
|
||||
:param timespan_to: a datetime object or None (indicating up till now)
|
||||
:returns: list of :class:`deltachat.chat.Location` objects.
|
||||
"""
|
||||
if timestamp_from is None:
|
||||
time_from = 0
|
||||
else:
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple())
|
||||
if timestamp_to is None:
|
||||
time_to = 0
|
||||
else:
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple())
|
||||
time_from = 0 if timestamp_from is None else calendar.timegm(timestamp_from.utctimetuple())
|
||||
time_to = 0 if timestamp_to is None else calendar.timegm(timestamp_to.utctimetuple())
|
||||
|
||||
if contact is None:
|
||||
contact_id = 0
|
||||
else:
|
||||
contact_id = contact.id
|
||||
contact_id = 0 if contact is None else contact.id
|
||||
|
||||
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Contact object. """
|
||||
"""Contact object."""
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
@@ -28,7 +28,7 @@ class Contact(object):
|
||||
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
return not self == other
|
||||
|
||||
def __repr__(self):
|
||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
|
||||
@@ -76,7 +76,7 @@ class Contact(object):
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_verifier(self, contact):
|
||||
"""Return the address of the contact that verified the contact"""
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
|
||||
@@ -79,15 +79,17 @@ class DirectImap:
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
"""Return info about selected folder if it is
|
||||
configured, otherwise None."""
|
||||
configured, otherwise None.
|
||||
"""
|
||||
if "_" not in config_name:
|
||||
config_name = "configured_{}_folder".format(config_name)
|
||||
foldername = self.account.get_config(config_name)
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
return None
|
||||
|
||||
def list_folders(self) -> List[str]:
|
||||
"""return list of all existing folder names"""
|
||||
"""return list of all existing folder names."""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
@@ -103,7 +105,7 @@ class DirectImap:
|
||||
|
||||
def get_all_messages(self) -> List[MailMessage]:
|
||||
assert not self._idling
|
||||
return [mail for mail in self.conn.fetch()]
|
||||
return list(self.conn.fetch())
|
||||
|
||||
def get_unread_messages(self) -> List[str]:
|
||||
assert not self._idling
|
||||
@@ -221,5 +223,4 @@ class IdleManager:
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
res = self.direct_imap.conn.idle.stop()
|
||||
return res
|
||||
return self.direct_imap.conn.idle.stop()
|
||||
|
||||
@@ -30,6 +30,12 @@ class FFIEvent:
|
||||
self.data2 = data2
|
||||
|
||||
def __str__(self):
|
||||
if self.name == "DC_EVENT_INFO":
|
||||
return "INFO {data2}".format(data2=self.data2)
|
||||
if self.name == "DC_EVENT_WARNING":
|
||||
return "WARNING {data2}".format(data2=self.data2)
|
||||
if self.name == "DC_EVENT_ERROR":
|
||||
return "ERROR {data2}".format(data2=self.data2)
|
||||
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||
|
||||
|
||||
@@ -128,7 +134,8 @@ class FFIEventTracker:
|
||||
def wait_for_connectivity(self, connectivity):
|
||||
"""Wait for the specified connectivity.
|
||||
This only works reliably if the connectivity doesn't change
|
||||
again too quickly, otherwise we might miss it."""
|
||||
again too quickly, otherwise we might miss it.
|
||||
"""
|
||||
while 1:
|
||||
if self.account.get_connectivity() == connectivity:
|
||||
return
|
||||
@@ -136,12 +143,13 @@ class FFIEventTracker:
|
||||
|
||||
def wait_for_connectivity_change(self, previous, expected_next):
|
||||
"""Wait until the connectivity changes to `expected_next`.
|
||||
Fails the test if it changes to something else."""
|
||||
Fails the test if it changes to something else.
|
||||
"""
|
||||
while 1:
|
||||
current = self.account.get_connectivity()
|
||||
if current == expected_next:
|
||||
return
|
||||
elif current != previous:
|
||||
if current != previous:
|
||||
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
|
||||
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
@@ -176,7 +184,8 @@ class FFIEventTracker:
|
||||
- ac1 and ac2 are created
|
||||
- ac1 sends a message to ac2
|
||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent"""
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent
|
||||
"""
|
||||
self.get_info_contains("INBOX: Idle entering")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
@@ -186,14 +195,15 @@ class FFIEventTracker:
|
||||
|
||||
def wait_next_messages_changed(self):
|
||||
"""wait for and return next message-changed message or None
|
||||
if the event contains no msgid"""
|
||||
if the event contains no msgid
|
||||
"""
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_next_reactions_changed(self):
|
||||
"""wait for and return next reactions-changed message"""
|
||||
"""wait for and return next reactions-changed message."""
|
||||
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
|
||||
assert ev.data1 > 0
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
@@ -285,10 +295,10 @@ class EventThread(threading.Thread):
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
comment = ffi_event.data2
|
||||
yield "ac_configure_completed", dict(success=success, comment=comment)
|
||||
yield "ac_configure_completed", {"success": success, "comment": comment}
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
@@ -296,19 +306,19 @@ class EventThread(threading.Thread):
|
||||
res = map_system_message(msg)
|
||||
if res and res[0].startswith("ac_member"):
|
||||
yield res
|
||||
yield "ac_outgoing_message", dict(message=msg)
|
||||
yield "ac_outgoing_message", {"message": msg}
|
||||
elif msg.is_in_fresh():
|
||||
yield map_system_message(msg) or (
|
||||
"ac_incoming_message",
|
||||
dict(message=msg),
|
||||
{"message": msg},
|
||||
)
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_reactions_changed", dict(message=msg)
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_message_delivered", dict(message=msg)
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = account.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", dict(chat=chat)
|
||||
yield "ac_chat_modified", {"chat": chat}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
||||
"""Hooks for Python bindings to Delta Chat Core Rust CFFI."""
|
||||
|
||||
import pluggy
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" The Message object. """
|
||||
"""The Message object."""
|
||||
|
||||
import json
|
||||
import os
|
||||
@@ -59,10 +59,7 @@ class Message(object):
|
||||
:param view_type: the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker", "videochat", "webxdc"
|
||||
"""
|
||||
if isinstance(view_type, int):
|
||||
view_type_code = view_type
|
||||
else:
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
view_type_code = view_type if isinstance(view_type, int) else get_viewtype_code_from_name(view_type)
|
||||
return Message(
|
||||
account,
|
||||
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
|
||||
@@ -129,7 +126,7 @@ class Message(object):
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
"""mime type of the file (if it exists)."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
def get_status_updates(self, serial: int = 0) -> list:
|
||||
@@ -141,7 +138,7 @@ class Message(object):
|
||||
:param serial: The last known serial. Pass 0 if there are no known serials to receive all updates.
|
||||
"""
|
||||
return json.loads(
|
||||
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial))
|
||||
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial)),
|
||||
)
|
||||
|
||||
def send_status_update(self, json_data: Union[str, dict], description: str) -> bool:
|
||||
@@ -158,8 +155,11 @@ class Message(object):
|
||||
json_data = json.dumps(json_data, default=str)
|
||||
return bool(
|
||||
lib.dc_send_webxdc_status_update(
|
||||
self.account._dc_context, self.id, as_dc_charpointer(json_data), as_dc_charpointer(description)
|
||||
)
|
||||
self.account._dc_context,
|
||||
self.id,
|
||||
as_dc_charpointer(json_data),
|
||||
as_dc_charpointer(description),
|
||||
),
|
||||
)
|
||||
|
||||
def send_reaction(self, reaction: str):
|
||||
@@ -232,16 +232,18 @@ class Message(object):
|
||||
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
return None
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
"""Ephemeral timer in seconds
|
||||
"""Ephemeral timer in seconds.
|
||||
|
||||
:returns: timer in seconds or None if there is no timer
|
||||
"""
|
||||
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
|
||||
if timer:
|
||||
return timer
|
||||
return None
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timestamp(self):
|
||||
@@ -255,23 +257,25 @@ class Message(object):
|
||||
|
||||
@property
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
"""Text inside the quote.
|
||||
|
||||
:returns: Quoted text"""
|
||||
:returns: Quoted text
|
||||
"""
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
"""Quote getter
|
||||
"""Quote getter.
|
||||
|
||||
:returns: Quoted message, if found in the database"""
|
||||
:returns: Quoted message, if found in the database
|
||||
"""
|
||||
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
|
||||
if msg:
|
||||
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
|
||||
|
||||
@quote.setter
|
||||
def quote(self, quoted_message):
|
||||
"""Quote setter"""
|
||||
"""Quote setter."""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
@@ -286,7 +290,7 @@ class Message(object):
|
||||
|
||||
:returns: email-mime message object (with headers only, no body).
|
||||
"""
|
||||
import email.parser
|
||||
import email
|
||||
|
||||
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
|
||||
if mime_headers:
|
||||
@@ -297,7 +301,7 @@ class Message(object):
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
"""Error message."""
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
@@ -493,7 +497,8 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError(
|
||||
"message typecode not found for {!r}, " "available {!r}".format(view_type_name, list(_view_type_mapping.keys()))
|
||||
"message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
|
||||
)
|
||||
|
||||
|
||||
@@ -506,14 +511,11 @@ def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if not res:
|
||||
return
|
||||
return None
|
||||
action, affected, actor = res
|
||||
affected = msg.account.get_contact_by_addr(affected)
|
||||
if actor == "me":
|
||||
actor = None
|
||||
else:
|
||||
actor = msg.account.get_contact_by_addr(actor)
|
||||
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
|
||||
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
|
||||
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
@@ -528,8 +530,8 @@ def extract_addr(text):
|
||||
def parse_system_add_remove(text):
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple"""
|
||||
|
||||
returns a (action, affected, actor) triple
|
||||
"""
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
|
||||
@@ -8,7 +8,7 @@ def with_doc(f):
|
||||
# copied over unmodified from
|
||||
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
|
||||
def cached(f):
|
||||
"""returns a cached property that is calculated by function f"""
|
||||
"""returns a cached property that is calculated by function f."""
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
@@ -17,8 +17,9 @@ def cached(f):
|
||||
self._property_cache = {}
|
||||
except KeyError:
|
||||
pass
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
res = f(self)
|
||||
self._property_cache[f] = res
|
||||
return res
|
||||
|
||||
def set(self, val):
|
||||
propcache = self.__dict__.setdefault("_property_cache", {})
|
||||
|
||||
@@ -9,7 +9,8 @@ class ProviderNotFoundError(Exception):
|
||||
|
||||
|
||||
class Provider(object):
|
||||
"""Provider information.
|
||||
"""
|
||||
Provider information.
|
||||
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" The Reactions object. """
|
||||
"""The Reactions object."""
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_dc_charpointer, iter_array
|
||||
|
||||
@@ -29,7 +29,7 @@ def pytest_addoption(parser):
|
||||
"--liveconfig",
|
||||
action="store",
|
||||
default=None,
|
||||
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account",
|
||||
help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
|
||||
)
|
||||
group.addoption(
|
||||
"--ignored",
|
||||
@@ -124,7 +124,7 @@ def pytest_report_header(config, startdir):
|
||||
info["deltachat_core_version"],
|
||||
info["sqlite_version"],
|
||||
info["journal_mode"],
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
@@ -176,11 +176,11 @@ class TestProcess:
|
||||
try:
|
||||
yield self._configlist[index]
|
||||
except IndexError:
|
||||
res = requests.post(liveconfig_opt)
|
||||
res = requests.post(liveconfig_opt, timeout=60)
|
||||
if res.status_code != 200:
|
||||
pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
config = {"addr": d["email"], "mail_pw": d["password"]}
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
self._configlist.append(config)
|
||||
yield config
|
||||
@@ -229,7 +229,7 @@ def write_dict_to_dir(dic, target_dir):
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
@@ -253,6 +253,7 @@ def data(request):
|
||||
if os.path.exists(fn):
|
||||
return fn
|
||||
print("WARNING: path does not exist: {!r}".format(fn))
|
||||
return None
|
||||
|
||||
def read_path(self, bn, mode="r"):
|
||||
fn = self.get_path(bn)
|
||||
@@ -264,8 +265,11 @@ def data(request):
|
||||
|
||||
|
||||
class ACSetup:
|
||||
"""accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases. From tests, use the higher level
|
||||
"""
|
||||
Accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases.
|
||||
|
||||
From tests, use the higher level
|
||||
public ACFactory methods instead of its private helper class.
|
||||
"""
|
||||
|
||||
@@ -289,7 +293,7 @@ class ACSetup:
|
||||
self._account2state[account] = self.CONFIGURED
|
||||
self.log("added already configured account", account, account.get_config("addr"))
|
||||
|
||||
def start_configure(self, account, reconfigure=False):
|
||||
def start_configure(self, account):
|
||||
"""add an account and start its configure process."""
|
||||
|
||||
class PendingTracker:
|
||||
@@ -299,7 +303,7 @@ class ACSetup:
|
||||
|
||||
account.add_account_plugin(PendingTracker(), name="pending_tracker")
|
||||
self._account2state[account] = self.CONFIGURING
|
||||
account.configure(reconfigure=reconfigure)
|
||||
account.configure()
|
||||
self.log("started configure on", account)
|
||||
|
||||
def wait_one_configured(self, account):
|
||||
@@ -411,7 +415,8 @@ class ACFactory:
|
||||
acc.disable_logging()
|
||||
|
||||
def get_next_liveconfig(self):
|
||||
"""Base function to get functional online configurations
|
||||
"""
|
||||
Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
@@ -465,8 +470,7 @@ class ACFactory:
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
|
||||
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
|
||||
# do a pseudo-configured account
|
||||
@@ -476,14 +480,14 @@ class ACFactory:
|
||||
acname = ac._logid
|
||||
addr = "{}@offline.org".format(acname)
|
||||
ac.update_config(
|
||||
dict(
|
||||
addr=addr,
|
||||
displayname=acname,
|
||||
mail_pw="123",
|
||||
configured_addr=addr,
|
||||
configured_mail_pw="123",
|
||||
configured="1",
|
||||
)
|
||||
{
|
||||
"addr": addr,
|
||||
"displayname": acname,
|
||||
"mail_pw": "123",
|
||||
"configured_addr": addr,
|
||||
"configured_mail_pw": "123",
|
||||
"configured": "1",
|
||||
},
|
||||
)
|
||||
self._preconfigure_key(ac, addr)
|
||||
self._acsetup.init_logging(ac)
|
||||
@@ -494,12 +498,12 @@ class ACFactory:
|
||||
configdict = self.get_next_liveconfig()
|
||||
else:
|
||||
# XXX we might want to transfer the key to the new account
|
||||
configdict = dict(
|
||||
addr=cloned_from.get_config("addr"),
|
||||
mail_pw=cloned_from.get_config("mail_pw"),
|
||||
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"),
|
||||
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"),
|
||||
)
|
||||
configdict = {
|
||||
"addr": cloned_from.get_config("addr"),
|
||||
"mail_pw": cloned_from.get_config("mail_pw"),
|
||||
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
|
||||
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
|
||||
}
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
if ac is not None:
|
||||
@@ -600,7 +604,7 @@ class ACFactory:
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
yield am
|
||||
@@ -665,12 +669,12 @@ class BotProcess:
|
||||
ignored.append(line)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def tmp_db_path(tmpdir):
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg: str) -> None:
|
||||
|
||||
@@ -77,11 +77,11 @@ class ConfigureTracker:
|
||||
self.account.remove_account_plugin(self)
|
||||
|
||||
def wait_smtp_connected(self):
|
||||
"""wait until smtp is configured."""
|
||||
"""Wait until SMTP is configured."""
|
||||
self._smtp_finished.wait()
|
||||
|
||||
def wait_imap_connected(self):
|
||||
"""wait until smtp is configured."""
|
||||
"""Wait until IMAP is configured."""
|
||||
self._imap_finished.wait()
|
||||
|
||||
def wait_progress(self, data1=None):
|
||||
@@ -91,7 +91,8 @@ class ConfigureTracker:
|
||||
break
|
||||
|
||||
def wait_finish(self, timeout=None):
|
||||
"""wait until configure is completed.
|
||||
"""
|
||||
Wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
|
||||
@@ -15,5 +15,5 @@ if __name__ == "__main__":
|
||||
p,
|
||||
"-w",
|
||||
workspacedir,
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
|
||||
# would also find the "Sent" folder, but it would be too late:
|
||||
# The sentbox thread, started by `start_io()`, would have seen that there is no
|
||||
# ConfiguredSentboxFolder and do nothing.
|
||||
acfactory._acsetup.start_configure(ac1, reconfigure=True)
|
||||
acfactory._acsetup.start_configure(ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
@@ -492,3 +492,48 @@ def test_multidevice_sync_seen(acfactory, lp):
|
||||
assert ac1_clone_message.is_in_seen
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
|
||||
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
"""The test for the bug #3836:
|
||||
- Alice has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Bob joins the group and sends a message there. Alice sees it.
|
||||
- Alice's second devices goes online, but doesn't see Bob in the group.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
for ac in [ac1, ac1_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmpdir.mkdir("exportdir")
|
||||
ac1.export_self_keys(dir.strpath)
|
||||
ac1_offl.import_self_keys(dir.strpath)
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
qr = chat.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)
|
||||
|
||||
lp.sec("ac2: sending message")
|
||||
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
|
||||
# "Member Me (<addr>) added by <addr>." message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
lp.sec("ac1: receiving message")
|
||||
msg_in = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == msg_out.text
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
lp.sec("ac1_offl: going online, receiving message")
|
||||
ac1_offl.start_io()
|
||||
ac1_offl._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
msg_in = ac1_offl._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == msg_out.text
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
imap2.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.ignored
|
||||
@pytest.mark.ignored()
|
||||
def test_configure_generate_key(acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
acfactory.remove_preconfigured_keys()
|
||||
@@ -88,7 +88,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
|
||||
lp.indent(dir.strpath + os.sep + name)
|
||||
lp.sec("importing into existing account")
|
||||
ac2.import_self_keys(dir.strpath)
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False)
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
|
||||
assert key_id2 == key_id
|
||||
|
||||
|
||||
@@ -510,7 +510,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
idle2.wait_for_seen()
|
||||
|
||||
lp.step("1")
|
||||
for i in range(2):
|
||||
for _i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
|
||||
@@ -529,7 +529,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
pass # mark_seen_messages() has generated events before it returns
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory, lp):
|
||||
def test_moved_markseen(acfactory):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
@@ -553,7 +553,7 @@ def test_moved_markseen(acfactory, lp):
|
||||
ac2.mark_seen_messages([msg])
|
||||
uid = idle2.wait_for_seen()
|
||||
|
||||
assert len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) == 1
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*"))))) == 1
|
||||
|
||||
|
||||
def test_message_override_sender_name(acfactory, lp):
|
||||
@@ -832,7 +832,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
|
||||
text1 = (
|
||||
"hello\nworld\nthis is a very long message that should be"
|
||||
+ " wrapped using format=flowed and unwrapped on the receiver"
|
||||
" wrapped using format=flowed and unwrapped on the receiver"
|
||||
)
|
||||
msg_out = chat.send_text(text1)
|
||||
assert not msg_out.is_encrypted()
|
||||
@@ -894,7 +894,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -908,7 +908,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
message in Sent
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -922,7 +922,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -936,7 +936,21 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: delta<address: inbox@nhroy.com>
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message99@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -950,7 +964,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -964,7 +978,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown message in Junk
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1696,7 +1710,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
|
||||
assert contact.is_blocked()
|
||||
chat_on_ac2.remove_contact(ac1)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
|
||||
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts()
|
||||
assert ac1.get_self_contact() not in chat_on_ac1.get_contacts()
|
||||
|
||||
|
||||
def test_set_get_group_image(acfactory, data, lp):
|
||||
@@ -1770,7 +1784,7 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
lp.sec(
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
|
||||
+ "all messages are fetched"
|
||||
"all messages are fetched",
|
||||
)
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
@@ -2132,7 +2146,7 @@ def test_group_quote(acfactory, lp):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"folder,move,expected_destination,",
|
||||
("folder", "move", "expected_destination"),
|
||||
[
|
||||
(
|
||||
"xyz",
|
||||
@@ -2247,11 +2261,44 @@ def test_aeap_flow_verified(acfactory, lp):
|
||||
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_archived_muted_chat(acfactory, lp):
|
||||
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
|
||||
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send message to ac2")
|
||||
chat.send_text("message0")
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
msg2.mark_seen()
|
||||
|
||||
chat2 = msg2.chat
|
||||
chat2.archive()
|
||||
chat2.mute()
|
||||
|
||||
lp.sec("ac1: send another message to ac2")
|
||||
chat.send_text("message1")
|
||||
|
||||
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
|
||||
while 1:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
|
||||
assert ev.data2 == 0
|
||||
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
|
||||
assert archive.count_fresh_messages() == 1
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
break
|
||||
|
||||
|
||||
class TestOnlineConfigureFails:
|
||||
def test_invalid_password(self, acfactory):
|
||||
configdict = acfactory.get_next_liveconfig()
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123"))
|
||||
ac1.update_config({"addr": configdict["addr"], "mail_pw": "123"})
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
|
||||
@@ -15,7 +15,7 @@ from deltachat.tracker import ImexFailed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"msgtext,res",
|
||||
("msgtext", "res"),
|
||||
[
|
||||
(
|
||||
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
@@ -108,7 +108,7 @@ class TestOfflineAccountBasic:
|
||||
|
||||
def test_update_config(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config(dict(mvbox_move=False))
|
||||
ac1.update_config({"mvbox_move": False})
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_savemime(self, acfactory):
|
||||
@@ -229,11 +229,11 @@ class TestOfflineContact:
|
||||
|
||||
|
||||
class TestOfflineChat:
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def ac1(self, acfactory):
|
||||
return acfactory.get_pseudo_configured_account()
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def chat1(self, ac1):
|
||||
return ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
|
||||
@@ -257,7 +257,7 @@ class TestOfflineChat:
|
||||
assert chat2.id == chat1.id
|
||||
assert chat2.get_name() == chat1.get_name()
|
||||
assert chat1 == chat2
|
||||
assert not (chat1 != chat2)
|
||||
assert not chat1.__ne__(chat2)
|
||||
assert chat1 != chat3
|
||||
|
||||
for ichat in ac1.get_chats():
|
||||
@@ -450,7 +450,7 @@ class TestOfflineChat:
|
||||
assert msg.filemime == "image/png"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fn,typein,typeout",
|
||||
("fn", "typein", "typeout"),
|
||||
[
|
||||
("r", None, "application/octet-stream"),
|
||||
("r.txt", None, "text/plain"),
|
||||
@@ -458,7 +458,7 @@ class TestOfflineChat:
|
||||
("r.txt", "image/png", "image/png"),
|
||||
],
|
||||
)
|
||||
def test_message_file(self, ac1, chat1, data, lp, fn, typein, typeout):
|
||||
def test_message_file(self, chat1, data, lp, fn, typein, typeout):
|
||||
lp.sec("sending file")
|
||||
fp = data.get_path(fn)
|
||||
msg = chat1.send_file(fp, typein)
|
||||
@@ -694,7 +694,7 @@ class TestOfflineChat:
|
||||
chat1.set_draft(None)
|
||||
assert chat1.get_draft() is None
|
||||
|
||||
def test_qr_setup_contact(self, acfactory, lp):
|
||||
def test_qr_setup_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
|
||||
@@ -93,7 +93,7 @@ def test_empty_context():
|
||||
capi.lib.dc_context_unref(ctx)
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir, acfactory):
|
||||
def test_dc_close_events(acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
# register after_shutdown function
|
||||
|
||||
@@ -50,18 +50,14 @@ commands =
|
||||
skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
flake8
|
||||
# isort 5.11.0 is broken: https://github.com/PyCQA/isort/issues/2031
|
||||
isort<5.11.0
|
||||
ruff
|
||||
black
|
||||
# pygments required by rst-lint
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
isort --check setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
flake8 src/deltachat
|
||||
flake8 tests/ examples/
|
||||
ruff src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
@@ -102,7 +98,3 @@ timeout = 150
|
||||
timeout_func_only = True
|
||||
markers =
|
||||
ignored: ignore this test in default test runs, use --ignored to run.
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
ignore = E203, E266, E501, W503
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1.64.0
|
||||
@@ -2,12 +2,13 @@
|
||||
Remove old "dc" indices except for master which always stays.
|
||||
|
||||
"""
|
||||
from requests import Session
|
||||
import datetime
|
||||
import sys
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MAXDAYS=7
|
||||
from requests import Session
|
||||
|
||||
MAXDAYS = 7
|
||||
|
||||
session = Session()
|
||||
session.headers["Accept"] = "application/json"
|
||||
@@ -54,7 +55,8 @@ def run():
|
||||
if not dates:
|
||||
print(
|
||||
"%s has no releases" % (baseurl + username + "/" + indexname),
|
||||
file=sys.stderr)
|
||||
file=sys.stderr,
|
||||
)
|
||||
date = datetime.datetime.now()
|
||||
else:
|
||||
date = datetime.datetime(*max(dates))
|
||||
@@ -67,6 +69,5 @@ def run():
|
||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
@@ -104,9 +104,6 @@ jobs:
|
||||
outputs:
|
||||
- name: py-docs
|
||||
path: ./python/doc/_build/
|
||||
# Source packages
|
||||
- name: py-dist
|
||||
path: ./python/.docker-tox/dist/
|
||||
# Binary wheels
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
@@ -145,7 +142,6 @@ jobs:
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
- name: py-dist
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -162,7 +158,6 @@ jobs:
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
devpi upload py-dist/*
|
||||
|
||||
- name: python-aarch64
|
||||
plan:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
|
||||
@@ -23,7 +23,7 @@ def read_toml_version(relpath):
|
||||
res = regex_matches(relpath, rex)
|
||||
if res is not None:
|
||||
return res.group(1)
|
||||
raise ValueError("no version found in {}".format(relpath))
|
||||
raise ValueError(f"no version found in {relpath}")
|
||||
|
||||
|
||||
def replace_toml_version(relpath, newversion):
|
||||
@@ -34,8 +34,8 @@ def replace_toml_version(relpath, newversion):
|
||||
for line in open(str(p)):
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
print("{}: set version={}".format(relpath, newversion))
|
||||
f.write('version = "{}"\n'.format(newversion))
|
||||
print(f"{relpath}: set version={newversion}")
|
||||
f.write(f'version = "{newversion}"\n')
|
||||
else:
|
||||
f.write(line)
|
||||
os.rename(tmp_path, str(p))
|
||||
@@ -44,7 +44,7 @@ def replace_toml_version(relpath, newversion):
|
||||
def read_json_version(relpath):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
with open(p) as f:
|
||||
json_data = json.loads(f.read())
|
||||
return json_data["version"]
|
||||
|
||||
@@ -52,7 +52,7 @@ def read_json_version(relpath):
|
||||
def update_package_json(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
with open(p) as f:
|
||||
json_data = json.loads(f.read())
|
||||
json_data["version"] = newversion
|
||||
with open(p, "w") as f:
|
||||
@@ -63,7 +63,7 @@ def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
@@ -75,9 +75,9 @@ def main():
|
||||
except SystemExit:
|
||||
print()
|
||||
for x in toml_list:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print(f"{x}: {read_toml_version(x)}")
|
||||
for x in json_list:
|
||||
print("{}: {}".format(x, read_json_version(x)))
|
||||
print(f"{x}: {read_json_version(x)}")
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
@@ -92,19 +92,19 @@ def main():
|
||||
if "alpha" not in newversion:
|
||||
for line in open("CHANGELOG.md"):
|
||||
## 1.25.0
|
||||
if line.startswith("## "):
|
||||
if line[2:].strip().startswith(newversion):
|
||||
break
|
||||
if line.startswith("## ") and line[2:].strip().startswith(newversion):
|
||||
break
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
raise SystemExit(
|
||||
f"CHANGELOG.md contains no entry for version: {newversion}"
|
||||
)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
replace_toml_version(toml_filename, newversion)
|
||||
|
||||
|
||||
for json_filename in json_list:
|
||||
update_package_json(json_filename, newversion)
|
||||
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
@@ -114,13 +114,12 @@ def main():
|
||||
|
||||
print("after commit, on master make sure to: ")
|
||||
print("")
|
||||
print(" git tag -a {}".format(newversion))
|
||||
print(" git push origin {}".format(newversion))
|
||||
print(" git tag -a py-{}".format(newversion))
|
||||
print(" git push origin py-{}".format(newversion))
|
||||
print(f" git tag -a {newversion}")
|
||||
print(f" git push origin {newversion}")
|
||||
print(f" git tag -a py-{newversion}")
|
||||
print(f" git push origin py-{newversion}")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -367,13 +367,20 @@ impl Config {
|
||||
|
||||
// Previous versions of the core stored absolute paths in account config.
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
|
||||
account.dir = new_dir.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Config { file, inner })
|
||||
let config = Self { file, inner };
|
||||
if modified {
|
||||
config.sync().await?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Loads all accounts defined in the configuration file.
|
||||
@@ -502,7 +509,6 @@ impl AccountConfig {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::stock_str::{self, StockMessage};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
//!
|
||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
/// Possible values for encryption preference
|
||||
@@ -36,7 +35,7 @@ impl fmt::Display for EncryptPreference {
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for EncryptPreference {
|
||||
impl FromStr for EncryptPreference {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
@@ -69,29 +68,6 @@ impl Aheader {
|
||||
prefer_encrypt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to parse Autocrypt header.
|
||||
///
|
||||
/// If there is none, returns None. If the header is present but cannot be parsed, returns an
|
||||
/// error.
|
||||
pub fn from_headers(
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<Option<Self>> {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
let header = Self::from_str(&value)?;
|
||||
if !addr_cmp(&header.addr, wanted_from) {
|
||||
bail!(
|
||||
"Autocrypt header address {:?} is not {:?}",
|
||||
header.addr,
|
||||
wanted_from
|
||||
);
|
||||
}
|
||||
Ok(Some(header))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
@@ -118,7 +94,7 @@ impl fmt::Display for Aheader {
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for Aheader {
|
||||
impl FromStr for Aheader {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
|
||||
@@ -355,7 +355,6 @@ mod tests {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::message;
|
||||
|
||||
@@ -499,17 +499,15 @@ fn encoded_img_exceeds_bytes(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use fs::File;
|
||||
use image::{GenericImageView, Pixel};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = image::open(path).expect("failed to open image");
|
||||
|
||||
392
src/chat.rs
392
src/chat.rs
@@ -531,20 +531,68 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Unarchives a chat that is archived and not muted.
|
||||
// Needed when a message is added to a chat so that the chat gets a normal visibility again.
|
||||
// Sending an appropriate event is up to the caller.
|
||||
pub async fn unarchive_if_not_muted(self, context: &Context) -> Result<()> {
|
||||
/// Unarchives a chat that is archived and not muted.
|
||||
/// Needed after a message is added to a chat so that the chat gets a normal visibility again.
|
||||
/// `msg_state` is the state of the message. Matters only for incoming messages currently. For
|
||||
/// multiple outgoing messages the function may be called once with MessageState::Undefined.
|
||||
/// Sending an appropriate event is up to the caller.
|
||||
/// Also emits DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived
|
||||
/// chats with unread messages increases (which is possible if the chat is muted).
|
||||
pub async fn unarchive_if_not_muted(
|
||||
self,
|
||||
context: &Context,
|
||||
msg_state: MessageState,
|
||||
) -> Result<()> {
|
||||
if msg_state != MessageState::InFresh {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
|
||||
AND NOT(muted_until=-1 OR muted_until>?)",
|
||||
paramsv![self, time()],
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if chat.visibility != ChatVisibility::Archived {
|
||||
return Ok(());
|
||||
}
|
||||
if chat.is_muted() {
|
||||
let unread_cnt = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?;
|
||||
if unread_cnt == 1 {
|
||||
// Added the first unread message in the chat.
|
||||
context.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 AND NOT(muted_until=-1 OR muted_until>?)",
|
||||
paramsv![self, time()],
|
||||
)
|
||||
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits an appropriate event for a message. `important` is whether a notification should be
|
||||
/// shown.
|
||||
pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
|
||||
if important {
|
||||
context.emit_incoming_msg(self, msg_id);
|
||||
} else {
|
||||
context.emit_msgs_changed(self, msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a chat.
|
||||
pub async fn delete(self, context: &Context) -> Result<()> {
|
||||
ensure!(
|
||||
@@ -782,17 +830,35 @@ impl ChatId {
|
||||
// the times are average, no matter if there are fresh messages or not -
|
||||
// and have to be multiplied by the number of items shown at once on the chatlist,
|
||||
// so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
let count = if self.is_archived_link() {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(DISTINCT(m.chat_id))
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON m.chat_id=c.id
|
||||
WHERE m.state=10
|
||||
and m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND c.blocked=0
|
||||
AND c.archived=1
|
||||
",
|
||||
paramsv![],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?;
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
@@ -1111,7 +1177,7 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "faild to load contacts for {}: {:?}", chat.id, err);
|
||||
error!(context, "faild to load contacts for {}: {:#}", chat.id, err);
|
||||
}
|
||||
}
|
||||
chat.name = chat_name;
|
||||
@@ -1216,6 +1282,10 @@ impl Chat {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
}
|
||||
} else if self.id.is_archived_link() {
|
||||
if let Ok(image_rel) = get_archive_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
}
|
||||
} else if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
@@ -1708,6 +1778,21 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
|
||||
return Ok(icon);
|
||||
}
|
||||
|
||||
let icon = include_bytes!("../assets/icon-archive.png");
|
||||
let blob = BlobObject::create(context, "icon-archive.png", icon).await?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("icon-archive", Some(&icon))
|
||||
.await?;
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
async fn update_special_chat_name(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
@@ -1972,7 +2057,9 @@ async fn prepare_msg_common(
|
||||
msg.state = change_state_to;
|
||||
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
}
|
||||
msg.id = chat
|
||||
.prepare_msg_raw(
|
||||
context,
|
||||
@@ -2109,7 +2196,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "job: cannot get selfavatar-state: {}", err);
|
||||
warn!(context, "job: cannot get selfavatar-state: {:#}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
@@ -2171,27 +2258,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
|
||||
error!(context, "Failed to set kml sent_timestamp: {:#}", err);
|
||||
}
|
||||
if !msg.hidden {
|
||||
if let Err(err) =
|
||||
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
|
||||
.await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {:?}", err);
|
||||
error!(context, "Failed to set msg_location_id: {:#}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {:?}", err);
|
||||
error!(context, "Failed to delete sync ids: {:#}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||
error!(context, "Failed to set selfavatar timestamp: {:#}", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2394,31 +2481,63 @@ pub(crate) async fn marknoticed_chat_if_older_than(
|
||||
}
|
||||
|
||||
/// Marks all messages in the chat as noticed.
|
||||
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
|
||||
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
// "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
|
||||
// the additional SELECT statement may speed up things as no write-blocking is needed.
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, chat_id],
|
||||
)
|
||||
.await?;
|
||||
if !exists {
|
||||
return Ok(());
|
||||
}
|
||||
if chat_id.is_archived_link() {
|
||||
let chat_ids_in_archive = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT DISTINCT(m.chat_id) FROM msgs m
|
||||
LEFT JOIN chats c ON m.chat_id=c.id
|
||||
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1",
|
||||
paramsv![],
|
||||
|row| row.get::<_, ChatId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into)
|
||||
)
|
||||
.await?;
|
||||
if chat_ids_in_archive.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id IN ({});",
|
||||
sql::repeat_vars(chat_ids_in_archive.len())
|
||||
),
|
||||
rusqlite::params_from_iter(&chat_ids_in_archive),
|
||||
)
|
||||
.await?;
|
||||
for chat_id_in_archive in chat_ids_in_archive {
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
|
||||
}
|
||||
} else {
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, chat_id],
|
||||
)
|
||||
.await?;
|
||||
if !exists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
|
||||
@@ -2833,14 +2952,12 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Returns true if an avatar should be attached in the given chat.
|
||||
///
|
||||
/// This function does not check if the avatar is set.
|
||||
/// If avatar is not set and this function returns `true`,
|
||||
/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar.
|
||||
pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
|
||||
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
|
||||
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
|
||||
// it can be removed after some time.
|
||||
if !context.sql.get_raw_config_bool("attach_selfavatar").await? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
|
||||
let needs_attach = context
|
||||
.sql
|
||||
@@ -3134,7 +3251,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
let mut created_msgs: Vec<MsgId> = Vec::new();
|
||||
let mut curr_timestamp: i64;
|
||||
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
chat_id
|
||||
.unarchive_if_not_muted(context, MessageState::Undefined)
|
||||
.await?;
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
@@ -3337,7 +3456,6 @@ pub async fn add_device_msg_with_importance(
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
msg.try_calc_and_set_dimensions(context).await.ok();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context).await;
|
||||
|
||||
@@ -3357,6 +3475,7 @@ pub async fn add_device_msg_with_importance(
|
||||
}
|
||||
}
|
||||
|
||||
let state = MessageState::InFresh;
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
@@ -3380,7 +3499,7 @@ pub async fn add_device_msg_with_importance(
|
||||
timestamp_sent,
|
||||
timestamp_sent, // timestamp_sent equals timestamp_rcvd
|
||||
msg.viewtype,
|
||||
MessageState::InFresh,
|
||||
state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
@@ -3389,6 +3508,9 @@ pub async fn add_device_msg_with_importance(
|
||||
.await?;
|
||||
|
||||
msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, state).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(label) = label {
|
||||
@@ -3402,11 +3524,7 @@ pub async fn add_device_msg_with_importance(
|
||||
}
|
||||
|
||||
if !msg_id.is_unset() {
|
||||
if important {
|
||||
context.emit_incoming_msg(chat_id, msg_id);
|
||||
} else {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
}
|
||||
chat_id.emit_msg_event(context, msg_id, important);
|
||||
}
|
||||
|
||||
Ok(msg_id)
|
||||
@@ -3557,12 +3675,10 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chatlist::{get_archived_cnt, Chatlist};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::{Contact, ContactAddress};
|
||||
use crate::receive_imf::receive_imf;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -4423,6 +4539,91 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archive_fresh_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
async fn msg_from(t: &TestContext, name: &str, num: u32) -> Result<()> {
|
||||
receive_imf(
|
||||
t,
|
||||
format!(
|
||||
"From: {}@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2022 19:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
name, num
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// receive some messages in archived+muted chats
|
||||
msg_from(&t, "bob", 1).await?;
|
||||
let bob_chat_id = t.get_last_msg().await.get_chat_id();
|
||||
bob_chat_id.accept(&t).await?;
|
||||
set_muted(&t, bob_chat_id, MuteDuration::Forever).await?;
|
||||
bob_chat_id
|
||||
.set_visibility(&t, ChatVisibility::Archived)
|
||||
.await?;
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0);
|
||||
|
||||
msg_from(&t, "bob", 2).await?;
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1);
|
||||
|
||||
msg_from(&t, "bob", 3).await?;
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1);
|
||||
|
||||
msg_from(&t, "claire", 4).await?;
|
||||
let claire_chat_id = t.get_last_msg().await.get_chat_id();
|
||||
claire_chat_id.accept(&t).await?;
|
||||
set_muted(&t, claire_chat_id, MuteDuration::Forever).await?;
|
||||
claire_chat_id
|
||||
.set_visibility(&t, ChatVisibility::Archived)
|
||||
.await?;
|
||||
msg_from(&t, "claire", 5).await?;
|
||||
msg_from(&t, "claire", 6).await?;
|
||||
msg_from(&t, "claire", 7).await?;
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2);
|
||||
assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 3);
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2);
|
||||
|
||||
// mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well
|
||||
marknoticed_chat(&t, claire_chat_id).await?;
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2);
|
||||
assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0);
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1);
|
||||
|
||||
// receive some more messages
|
||||
msg_from(&t, "claire", 8).await?;
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2);
|
||||
assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
msg_from(&t, "dave", 9).await?;
|
||||
let dave_chat_id = t.get_last_msg().await.get_chat_id();
|
||||
dave_chat_id.accept(&t).await?;
|
||||
assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
// mark the archived-link as noticed: check that the real chats are noticed as well
|
||||
marknoticed_chat(&t, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 0);
|
||||
assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0);
|
||||
assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec<ChatId> {
|
||||
let chatlist = Chatlist::try_load(ctx, listflags, None, None)
|
||||
.await
|
||||
@@ -4491,6 +4692,46 @@ mod tests {
|
||||
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned_after_new_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = alice.create_chat(&bob).await.id;
|
||||
let bob_chat_id = bob.create_chat(&alice).await.id;
|
||||
|
||||
assert!(alice_chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Pinned)
|
||||
.await
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, alice_chat_id)
|
||||
.await?
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "hi!".into()).await?;
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, alice_chat_id)
|
||||
.await?
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hi!".into()));
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, alice_chat_id)
|
||||
.await?
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_chat_name() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -4538,15 +4779,21 @@ mod tests {
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::IncomingUnknownTo).await?;
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("foo@bar.org")?,
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
add_contact_to_chat(&t, chat_id, contact_id).await?;
|
||||
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
|
||||
t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
|
||||
assert!(shall_attach_selfavatar(&t, chat_id).await?);
|
||||
|
||||
chat_id.set_selfavatar_timestamp(&t, time()).await?;
|
||||
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
|
||||
|
||||
t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
|
||||
assert!(shall_attach_selfavatar(&t, chat_id).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4785,8 +5032,8 @@ mod tests {
|
||||
alice.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
bob.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated).await?;
|
||||
let alice_bob_contact = alice.add_or_lookup_contact(&bob).await;
|
||||
let contact_id = alice_bob_contact.id;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?;
|
||||
|
||||
@@ -5048,7 +5295,7 @@ mod tests {
|
||||
assert_eq!(msg.get_filename(), Some(filename.to_string()));
|
||||
assert_eq!(msg.get_width(), w);
|
||||
assert_eq!(msg.get_height(), h);
|
||||
assert!(msg.get_filebytes(&bob).await > 250);
|
||||
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5496,8 +5743,13 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?;
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("foo@bar.org")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// create a blocked chat
|
||||
let chat_id_orig =
|
||||
|
||||
@@ -92,8 +92,6 @@ impl Chatlist {
|
||||
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
|
||||
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let msg_id: Option<MsgId> = row.get(1)?;
|
||||
@@ -123,7 +121,7 @@ impl Chatlist {
|
||||
//
|
||||
// The query shows messages from blocked contacts in
|
||||
// groups. Otherwise it would be hard to follow conversations.
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
let ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
@@ -216,7 +214,7 @@ impl Chatlist {
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let ids = context.sql.query_map(
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -236,19 +234,15 @@ impl Chatlist {
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
if !flag_no_specials {
|
||||
add_archived_link_item = true;
|
||||
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
}
|
||||
ids.insert(0, (DC_CHAT_ID_ARCHIVED_LINK, None));
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
if add_archived_link_item && get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
}
|
||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, None));
|
||||
}
|
||||
|
||||
Ok(Chatlist { ids })
|
||||
}
|
||||
|
||||
@@ -371,7 +365,6 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf;
|
||||
|
||||
@@ -292,9 +292,6 @@ impl Context {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||
.await?;
|
||||
self.sql
|
||||
.set_raw_config_bool("attach_selfavatar", true)
|
||||
.await?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||
@@ -443,16 +440,15 @@ fn get_config_keys_string() -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::constants;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
assert_eq!(Config::MailServer.to_string(), "mail_server");
|
||||
|
||||
@@ -6,9 +6,12 @@ mod read_url;
|
||||
mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -28,10 +31,6 @@ use crate::stock_str;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
@@ -565,13 +564,18 @@ async fn try_imap_one_param(
|
||||
provider_strict_tls: bool,
|
||||
) -> Result<Imap, ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
param.oauth2
|
||||
param.oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
} else {
|
||||
"None".to_string()
|
||||
}
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
@@ -579,7 +583,7 @@ async fn try_imap_one_param(
|
||||
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
info!(context, "failure: {:#}", err);
|
||||
return Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{:#}", err),
|
||||
@@ -590,7 +594,7 @@ async fn try_imap_one_param(
|
||||
|
||||
match imap.connect(context).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
info!(context, "failure: {:#}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{:#}", err),
|
||||
@@ -661,6 +665,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
|
||||
if errors.iter().all(|e| {
|
||||
e.msg.to_lowercase().contains("could not resolve")
|
||||
|| e.msg.to_lowercase().contains("no dns resolution results")
|
||||
|| e.msg
|
||||
.to_lowercase()
|
||||
.contains("temporary failure in name resolution")
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Server {
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
|
||||
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
|
||||
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
///
|
||||
|
||||
@@ -16,7 +16,7 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
|
||||
}
|
||||
|
||||
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
|
||||
let client = reqwest::Client::new();
|
||||
let client = crate::http::get_client()?;
|
||||
let mut url = url.to_string();
|
||||
|
||||
// Follow up to 10 http-redirects
|
||||
|
||||
@@ -68,6 +68,7 @@ impl Default for MediaQuality {
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the key to generate.
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
@@ -118,13 +119,13 @@ pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
|
||||
pub const DC_GCL_ADD_SELF: u32 = 0x02;
|
||||
|
||||
// unchanged user avatars are resent to the recipients every some days
|
||||
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
|
||||
// warn about an outdated app after a given number of days.
|
||||
// as we use the "provider-db generation date" as reference (that might not be updated very often)
|
||||
// and as not all system get speedy updates,
|
||||
// do not use too small value that will annoy users checking for nonexistant updates.
|
||||
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
|
||||
@@ -169,7 +170,7 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
/// String that indicates that something is left out or truncated.
|
||||
pub const DC_ELLIPSIS: &str = "[...]";
|
||||
pub(crate) const DC_ELLIPSIS: &str = "[...]";
|
||||
// how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x)
|
||||
// (taken from "subjective" testing what looks ok)
|
||||
pub const DC_DESIRED_TEXT_LINES: usize = 38;
|
||||
@@ -186,11 +187,6 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
|
||||
/// `char`s), not Unicode Grapheme Clusters.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
|
||||
|
||||
// Flags for empty server job
|
||||
|
||||
pub const DC_EMPTY_MVBOX: u32 = 0x01;
|
||||
pub const DC_EMPTY_INBOX: u32 = 0x02;
|
||||
|
||||
// Flags for configuring IMAP and SMTP servers.
|
||||
// These flags are optional
|
||||
// and may be set together with the username, password etc.
|
||||
@@ -220,21 +216,7 @@ pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
|
||||
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
|
||||
// this does not affect MIME'e `To:` header.
|
||||
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
|
||||
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
|
||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyType {
|
||||
Public = 0,
|
||||
Private = 1,
|
||||
}
|
||||
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -262,13 +244,6 @@ mod tests {
|
||||
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keytype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
353
src/contact.rs
353
src/contact.rs
@@ -1,11 +1,10 @@
|
||||
//! Contacts module
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -38,6 +37,51 @@ use crate::{chat, stock_str};
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct ContactAddress<'a>(&'a str);
|
||||
|
||||
impl Deref for ContactAddress<'_> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress<'_> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ContactAddress<'a> {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &'a str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress<'_> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
@@ -48,12 +92,18 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
pub struct ContactId(u32);
|
||||
|
||||
impl ContactId {
|
||||
/// Undefined contact. Used as a placeholder for trashed messages.
|
||||
pub const UNDEFINED: ContactId = ContactId::new(0);
|
||||
|
||||
/// The owner of the account.
|
||||
///
|
||||
/// The email-address is set by `set_config` using "addr".
|
||||
pub const SELF: ContactId = ContactId::new(1);
|
||||
|
||||
/// ID of the contact for info messages.
|
||||
pub const INFO: ContactId = ContactId::new(2);
|
||||
|
||||
/// ID of the contact for device messages.
|
||||
pub const DEVICE: ContactId = ContactId::new(5);
|
||||
const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
|
||||
@@ -177,6 +227,8 @@ pub struct Contact {
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Origin {
|
||||
/// Unknown origin. Can be used as a minimum origin to specify that the caller does not care
|
||||
/// about origin of the contact.
|
||||
Unknown = 0,
|
||||
|
||||
/// The contact is a mailing list address, needed to unblock mailing lists
|
||||
@@ -257,12 +309,13 @@ pub(crate) enum Modifier {
|
||||
Created,
|
||||
}
|
||||
|
||||
/// Verification status of the contact.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum VerifiedStatus {
|
||||
/// Contact is not verified.
|
||||
Unverified = 0,
|
||||
// TODO: is this a thing?
|
||||
/// SELF has verified the fingerprint of a contact. Currently unused.
|
||||
Verified = 1,
|
||||
/// SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
|
||||
BidirectVerified = 2,
|
||||
@@ -275,6 +328,7 @@ impl Default for VerifiedStatus {
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
/// Loads a contact snapshot from the database.
|
||||
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
let mut contact = context
|
||||
.sql
|
||||
@@ -368,12 +422,14 @@ impl Contact {
|
||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
|
||||
let name = improve_single_line_input(name);
|
||||
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
|
||||
|
||||
let (name, addr) = sanitize_name_and_addr(&name, addr);
|
||||
let addr = ContactAddress::new(&addr)?;
|
||||
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?;
|
||||
Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated)
|
||||
.await
|
||||
.context("add_or_lookup")?;
|
||||
let blocked = Contact::is_blocked_load(context, contact_id).await?;
|
||||
match sth_modified {
|
||||
Modifier::None => {}
|
||||
@@ -458,10 +514,12 @@ impl Contact {
|
||||
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
|
||||
///
|
||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
|
||||
///
|
||||
/// Returns None if the contact with such address cannot exist.
|
||||
pub(crate) async fn add_or_lookup(
|
||||
context: &Context,
|
||||
name: &str,
|
||||
addr: &str,
|
||||
addr: ContactAddress<'_>,
|
||||
mut origin: Origin,
|
||||
) -> Result<(ContactId, Modifier)> {
|
||||
let mut sth_modified = Modifier::None;
|
||||
@@ -469,22 +527,10 @@ impl Contact {
|
||||
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
|
||||
ensure!(origin != Origin::Unknown, "Missing valid origin");
|
||||
|
||||
let addr = addr_normalize(addr).to_string();
|
||||
|
||||
if context.is_self_addr(&addr).await? {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
if !may_be_valid_addr(&addr) {
|
||||
warn!(
|
||||
context,
|
||||
"Bad address \"{}\" for contact \"{}\".",
|
||||
addr,
|
||||
if !name.is_empty() { name } else { "<unset>" },
|
||||
);
|
||||
bail!("Bad address supplied: {:?}", addr);
|
||||
}
|
||||
|
||||
let mut name = name;
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if origin <= Origin::OutgoingTo {
|
||||
@@ -543,7 +589,7 @@ impl Contact {
|
||||
|| row_authname.is_empty());
|
||||
|
||||
row_id = u32::try_from(id)?;
|
||||
if origin as i32 >= row_origin as i32 && addr != row_addr {
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
@@ -671,18 +717,25 @@ impl Contact {
|
||||
for (name, addr) in split_address_book(addr_book).into_iter() {
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let name = normalize_name(&name);
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to add address {} from address book: {}", addr, err
|
||||
);
|
||||
}
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
match ContactAddress::new(&addr) {
|
||||
Ok(addr) => {
|
||||
match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await {
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to add address {} from address book: {}", addr, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{:#}.", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if modify_cnt > 0 {
|
||||
@@ -847,6 +900,7 @@ impl Contact {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns number of blocked contacts.
|
||||
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
@@ -1138,23 +1192,16 @@ impl Contact {
|
||||
Ok(VerifiedStatus::Unverified)
|
||||
}
|
||||
|
||||
/// Return the address that verified the given contact
|
||||
pub async fn get_verifier_addr(
|
||||
context: &Context,
|
||||
contact_id: &ContactId,
|
||||
) -> Result<Option<String>> {
|
||||
let contact = Contact::load_from_db(context, *contact_id).await?;
|
||||
|
||||
Ok(Peerstate::from_addr(context, contact.get_addr())
|
||||
/// Returns the address that verified the contact.
|
||||
pub async fn get_verifier_addr(&self, context: &Context) -> Result<Option<String>> {
|
||||
Ok(Peerstate::from_addr(context, self.get_addr())
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned())))
|
||||
}
|
||||
|
||||
pub async fn get_verifier_id(
|
||||
context: &Context,
|
||||
contact_id: &ContactId,
|
||||
) -> Result<Option<ContactId>> {
|
||||
let verifier_addr = Contact::get_verifier_addr(context, contact_id).await?;
|
||||
/// Returns the ContactId that verified the contact.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let verifier_addr = self.get_verifier_addr(context).await?;
|
||||
if let Some(addr) = verifier_addr {
|
||||
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
|
||||
} else {
|
||||
@@ -1162,7 +1209,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the ContactId that verified the given contact
|
||||
/// Returns the number of real (i.e. non-special) contacts in the database.
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
return Ok(0);
|
||||
@@ -1178,6 +1225,7 @@ impl Contact {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Returns true if a contact with this ID exists.
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> {
|
||||
if contact_id.is_special() {
|
||||
return Ok(false);
|
||||
@@ -1193,6 +1241,7 @@ impl Contact {
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Updates the origin of the contact, but only if new origin is higher than the current one.
|
||||
pub async fn scaleup_origin_by_id(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
@@ -1404,6 +1453,7 @@ pub(crate) async fn update_last_seen(
|
||||
)
|
||||
.await?
|
||||
> 0
|
||||
&& timestamp > time() - SEEN_RECENTLY_SECONDS
|
||||
{
|
||||
context.interrupt_recently_seen(contact_id, timestamp).await;
|
||||
}
|
||||
@@ -1453,6 +1503,7 @@ fn cat_fingerprint(
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2).to_lowercase();
|
||||
@@ -1561,6 +1612,9 @@ impl RecentlySeenLoop {
|
||||
context,
|
||||
"Error receiving an interruption in recently seen loop: {}", err
|
||||
);
|
||||
// Maybe the sender side is closed.
|
||||
// Terminate the loop to avoid looping indefinitely.
|
||||
return;
|
||||
}
|
||||
Ok(Ok(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
@@ -1602,7 +1656,6 @@ impl RecentlySeenLoop {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -1680,7 +1733,7 @@ mod tests {
|
||||
let (id, _modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"bob",
|
||||
"user@example.org",
|
||||
ContactAddress::new("user@example.org")?,
|
||||
Origin::IncomingReplyTo,
|
||||
)
|
||||
.await?;
|
||||
@@ -1708,7 +1761,7 @@ mod tests {
|
||||
let (contact_bob_id, modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"someone",
|
||||
"user@example.org",
|
||||
ContactAddress::new("user@example.org")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
@@ -1743,6 +1796,18 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
let contact_address = ContactAddress::new(alice_addr)?;
|
||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||
|
||||
let invalid_addr = "<> foobar";
|
||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_or_lookup() {
|
||||
// add some contacts, this also tests add_address_book()
|
||||
@@ -1758,10 +1823,14 @@ mod tests {
|
||||
assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4);
|
||||
|
||||
// check first added contact, this modifies authname because it is empty
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bla foo",
|
||||
ContactAddress::new("one@eins.org").unwrap(),
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -1773,10 +1842,14 @@ mod tests {
|
||||
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
|
||||
|
||||
// modify first added contact
|
||||
let (contact_id_test, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "Real one", " one@eins.org ", Origin::ManuallyCreated)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Real one",
|
||||
ContactAddress::new(" one@eins.org ").unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -1785,10 +1858,14 @@ mod tests {
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// check third added contact (contact without name)
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "", "three@drei.sam", Origin::IncomingUnknownTo)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("three@drei.sam").unwrap(),
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::None);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -1801,7 +1878,7 @@ mod tests {
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"m. serious",
|
||||
"three@drei.sam",
|
||||
ContactAddress::new("three@drei.sam").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -1813,10 +1890,14 @@ mod tests {
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// manually edit name of third contact (does not changed authorized name)
|
||||
let (contact_id_test, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "schnucki", "three@drei.sam", Origin::ManuallyCreated)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"schnucki",
|
||||
ContactAddress::new("three@drei.sam").unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -1825,10 +1906,14 @@ mod tests {
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// Fourth contact:
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "", "alice@w.de", Origin::IncomingUnknownTo)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"",
|
||||
ContactAddress::new("alice@w.de").unwrap(),
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::None);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -1963,9 +2048,13 @@ mod tests {
|
||||
assert!(Contact::delete(&alice, ContactId::SELF).await.is_err());
|
||||
|
||||
// Create Bob contact
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
@@ -2038,10 +2127,14 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// incoming mail `From: bob1 <bob@example.org>` - this should init authname
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bob1",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Created);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -2050,10 +2143,14 @@ mod tests {
|
||||
assert_eq!(contact.get_display_name(), "bob1");
|
||||
|
||||
// incoming mail `From: bob2 <bob@example.org>` - this should update authname
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bob2",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -2072,10 +2169,14 @@ mod tests {
|
||||
assert_eq!(contact.get_display_name(), "bob3");
|
||||
|
||||
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "bob3"
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "bob4", "bob@example.org", Origin::IncomingUnknownFrom)
|
||||
.await
|
||||
.unwrap();
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"bob4",
|
||||
ContactAddress::new("bob@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
@@ -2100,7 +2201,7 @@ mod tests {
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"claire1",
|
||||
"claire@example.org",
|
||||
ContactAddress::new("claire@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2116,7 +2217,7 @@ mod tests {
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"claire2",
|
||||
"claire@example.org",
|
||||
ContactAddress::new("claire@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
@@ -2138,26 +2239,38 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Incoming message from Bob.
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom)
|
||||
.await?;
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.org")?,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(sth_modified, Modifier::Created);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await?;
|
||||
assert_eq!(contact.get_display_name(), "Bob");
|
||||
|
||||
// Incoming message from someone else with "Not Bob" <bob@example.org> in the "To:" field.
|
||||
let (contact_id_same, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "Not Bob", "bob@example.org", Origin::IncomingUnknownTo)
|
||||
.await?;
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Not Bob",
|
||||
ContactAddress::new("bob@example.org")?,
|
||||
Origin::IncomingUnknownTo,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await?;
|
||||
assert_eq!(contact.get_display_name(), "Not Bob");
|
||||
|
||||
// Incoming message from Bob, changing the name back.
|
||||
let (contact_id_same, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom)
|
||||
.await?;
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.org")?,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix
|
||||
let contact = Contact::load_from_db(&t, contact_id).await?;
|
||||
@@ -2180,9 +2293,14 @@ mod tests {
|
||||
assert_eq!(contact.get_display_name(), "dave1");
|
||||
|
||||
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
|
||||
Contact::add_or_lookup(&t, "dave2", "dave@example.org", Origin::IncomingUnknownFrom)
|
||||
.await
|
||||
.unwrap();
|
||||
Contact::add_or_lookup(
|
||||
&t,
|
||||
"dave2",
|
||||
ContactAddress::new("dave@example.org").unwrap(),
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "dave2");
|
||||
assert_eq!(contact.get_name(), "dave1");
|
||||
@@ -2296,9 +2414,13 @@ mod tests {
|
||||
let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await;
|
||||
assert!(encrinfo.is_err());
|
||||
|
||||
let (contact_bob_id, _modified) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
@@ -2455,9 +2577,13 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
async fn test_last_seen() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
@@ -2504,4 +2630,27 @@ Hi."#;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
// Receive a message from Bob to create a peerstate.
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
use crate::chat::{get_chat_cnt, ChatId};
|
||||
@@ -22,7 +23,6 @@ use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::ratelimit::Ratelimit;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
@@ -383,7 +383,7 @@ impl Context {
|
||||
let mut lock = self.inner.scheduler.write().await;
|
||||
if lock.is_none() {
|
||||
match Scheduler::start(self.clone()).await {
|
||||
Err(err) => error!(self, "Failed to start IO: {}", err),
|
||||
Err(err) => error!(self, "Failed to start IO: {:#}", err),
|
||||
Ok(scheduler) => *lock = Some(scheduler),
|
||||
}
|
||||
}
|
||||
@@ -499,7 +499,7 @@ impl Context {
|
||||
match &*s {
|
||||
RunningState::Running { cancel_sender } => {
|
||||
if let Err(err) = cancel_sender.send(()).await {
|
||||
warn!(self, "could not cancel ongoing: {:?}", err);
|
||||
warn!(self, "could not cancel ongoing: {:#}", err);
|
||||
}
|
||||
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
||||
*s = RunningState::ShallStop;
|
||||
@@ -861,8 +861,13 @@ pub fn get_version_str() -> &'static str {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
@@ -873,10 +878,6 @@ mod tests {
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::create_outgoing_rfc724_mid;
|
||||
use anyhow::Context as _;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! End-to-end decryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use mailparse::ParsedMail;
|
||||
@@ -13,7 +14,6 @@ use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::log::LogExt;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
@@ -72,9 +72,25 @@ pub(crate) async fn prepare_decryption(
|
||||
});
|
||||
}
|
||||
|
||||
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
|
||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||
.flatten();
|
||||
let autocrypt_header =
|
||||
if let Some(autocrypt_header_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Aheader::from_str(&autocrypt_header_value) {
|
||||
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
|
||||
Ok(header) => {
|
||||
warn!(
|
||||
context,
|
||||
"Autocrypt header address {:?} is not {:?}.", header.addr, from
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let dkim_results = handle_authres(context, mail, from, message_time).await?;
|
||||
|
||||
@@ -328,11 +344,10 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_has_decrypted_pgp_armor() {
|
||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! # Download large messages manually.
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
@@ -14,7 +16,6 @@ use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::param::Params;
|
||||
use crate::tools::time;
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
use std::cmp::max;
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
@@ -132,7 +133,7 @@ impl Job {
|
||||
/// 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);
|
||||
warn!(context, "download: could not connect: {:#}", err);
|
||||
return Status::RetryNow;
|
||||
}
|
||||
|
||||
@@ -264,14 +265,13 @@ impl MimeMessage {
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf_inner;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
@@ -144,13 +144,12 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::test_utils::{bob_keypair, TestContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::cmp::max;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
@@ -86,7 +87,6 @@ use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
|
||||
@@ -283,7 +283,7 @@ pub enum EventType {
|
||||
/// @param data2 (int) Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: ContactId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user