Compare commits

...
Author SHA1 Message Date
holger krekel eb61a6cf3b replace wrong link to countermitm with s3ecurejoin 2023-11-22 15:21:07 +01:00
link2xt 30f8522626 docs: generate deltachat-rpc-client documentation
To preview the docs, run:
```
scripts/build-python-docs.sh
firefox dist/html/index.html
```

I have removed the Makefile because modern Sphinx Makefile is just a
wrapper for `sphinx-build -M`:
https://github.com/sphinx-doc/sphinx/blob/35965903177c6ed9a6afb62ccd33243a746a3fc0/sphinx/templates/quickstart/Makefile.new_t
and sphinx-quickstart even has an option `--no-makefile`.
`make.bat` makes even less sense.
In `scripts/build-python-docs.sh` I use `sphinx-build` directly
without `make` wrapper.
2023-11-20 19:56:22 +00:00
link2xt d3c221e061 chore: update dependencies 2023-11-20 19:52:40 +00:00
link2xt 8a421224f8 chore(release): prepare for 1.131.5 2023-11-20 19:04:57 +00:00
link2xt 7dfce71ac9 fix: lowercase addr when it is set
Prevent users from creating new accounts with uppercase letters
in the address.
2023-11-20 16:46:59 +00:00
link2xt 35ba97f76a fix: lowercase the address in member added/removed messages 2023-11-20 16:46:59 +00:00
link2xt 41921eaf3d fix: compare verifier addr to peerstate addr with addr_cmp 2023-11-20 16:46:59 +00:00
link2xt 03221ea86c refactor: lowercase the address in addr_normalize() 2023-11-20 16:46:59 +00:00
link2xt b50761e4d1 api: turn ContactAddress into an owned type
This allows to normalize the address on creation more,
e.g. lowercase it.
2023-11-20 16:46:59 +00:00
link2xt 40dea771cc ci: test with Rust 1.74 2023-11-19 21:41:28 +00:00
link2xt e011f8f42f chore: fix Rust 1.74 clippy warning 2023-11-19 21:40:19 +00:00
link2xt 09d4b4354a feat: lowercase addresses in Autocrypt and Autocrypt-Gossip
Email addresses should generally be compared case-insensitively,
but there may be errors in comparison code.
To reduce the chance of problems, encode addresses
in Autocrypt and Autocrypt-Gossip in lowercase
to avoid propagating uppercase characters over the network
to other accounts potentially running buggy code.
2023-11-19 06:22:25 +00:00
link2xt ab151654fb chore: remove unused import from python tests 2023-11-18 11:32:34 +01:00
link2xt ea9556b1b9 test: port test_aeap_flow_verified to JSON-RPC 2023-11-18 11:32:34 +01:00
link2xt 3dc6fd5c10 api(deltachat-rpc-client): add Message.get_sender_contact() 2023-11-18 11:32:34 +01:00
link2xt f39acbc037 test: port test_qr_new_group_unblocked() to JSON-RPC 2023-11-18 11:32:34 +01:00
link2xt 005f7ff07e test: port test_qr_join_chat_with_pending_bobstate_issue4894 to JSON-RPC 2023-11-18 11:32:34 +01:00
link2xt 144ca7c171 test: port test_qr_join_chat to JSON-RPC
Disabled `verified_one_on_one_chats` is not tested
as it is not interesting, other checks are moved.
2023-11-18 11:32:34 +01:00
link2xt 7012b99d73 test: remove test_qr_setup_contact from online python tests
There is an identical `test_qr_setup_contact` in deltachat-rpc-client
test suite which also checks that contact profiles get verified.
2023-11-18 11:32:34 +01:00
Hocuri 72bacd56f7 Update securejoin links 2023-11-18 11:30:59 +01:00
iequidoo cc75038ccc docs: Contributing guidelines for error handling 2023-11-18 00:34:47 -03:00
link2xt f4810125e3 fix: recognize Chat-Group-Member-Added of self case-insensitively
If configured address is `Bob@example.net`,
but the message arrives adding `bob@example.net`,
Bob's device should still recognize it as addition of self
and fully recreate the group.
2023-11-18 02:42:28 +00:00
link2xt acf1faf151 refactor(deltachat-rpc-client): add helper functions to wait for securejoin 2023-11-17 13:32:20 +00:00
link2xt 255fbe94f7 fix: do not use square brackets error for unknown sender
If the sender of the message in protected group chat
is not a member of the chat, mark the sender name with `~`
as we do it in non-protected chats and set the error
instead of replacing the whole message with
"Unknown sender for this chat. See 'info' for more details."

To send a message to a protected group this way
the sender needs to know the group ID
and sign the message with the current verified key.
Usually this is just a late message
delivered shortly after the user has left
the group or was removed from it.

Replacing the message with a single error text part
as done before this change makes it impossible
to access anything other than text, such as attached images.
2023-11-17 10:26:38 +00:00
link2xt b5d1eba28e docs: add missing 1.131.4 link to the changelog 2023-11-17 10:13:18 +00:00
iequidoo 1509978738 fix: chat::rename_ex(): Sync improved chat name to other devices
Other devices should get the same chat name as the currently used device, i.e. the name a user sees
after renaming the chat. This fix is minor because `improve_single_line_input()` logic isn't going
to change often, but still, and also it simplifies the code.
2023-11-17 03:55:50 -03:00
iequidoo 607b9e55a9 fix: Chat::sync_contacts(): Fetch contact addresses in a single query
In order to protect from races with contacts removal and (just in case) dangling contact ids in
`chats_contacts` table.
2023-11-17 03:55:50 -03:00
holger krekel 7c4c980409 for testrun.org subdomains we can allow 60 messages per minute (or lift the limit completely but maybe good to protect against wild-running bots or so) 2023-11-17 06:50:19 +00:00
B. Petersen b8ad3ec1b1 chore(release): prepare for 1.131.4 2023-11-16 21:04:19 +01:00
66 changed files with 930 additions and 1192 deletions
+4 -4
View File
@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.73.0
RUSTUP_TOOLCHAIN: 1.74.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -76,11 +76,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.73.0
rust: 1.74.0
- os: windows-latest
rust: 1.73.0
rust: 1.74.0
- os: macos-latest
rust: 1.73.0
rust: 1.74.0
# Minimum Supported Rust Version = 1.70.0
- os: ubuntu-latest
+25
View File
@@ -0,0 +1,25 @@
name: Build & Deploy Documentation on py.delta.chat
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Build Python documentation
run: scripts/build-python-docs.sh
- name: Upload to py.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: delta
KEY: ${{ secrets.CODESPEAK_KEY }}
HOST: "lists.codespeak.net"
SOURCE: "dist/html/"
TARGET: "/home/delta/build/master"
+42
View File
@@ -1,5 +1,45 @@
# Changelog
## [1.131.5] - 2023-11-20
### API-Changes
- deltachat-rpc-client: Add `Message.get_sender_contact()`.
- Turn `ContactAddress` into an owned type.
### Features / Changes
- Lowercase addresses in Autocrypt and Autocrypt-Gossip headers.
- Lowercase the address in member added/removed messages.
- Lowercase `addr` when it is set.
- Do not replace the message with an error in square brackets when the sender is not a member of the protected group.
### Fixes
- `Chat::sync_contacts()`: Fetch contact addresses in a single query.
- `Chat::rename_ex()`: Sync improved chat name to other devices.
- Recognize `Chat-Group-Member-Added` of self case-insensitively.
- Compare verifier addr to peerstate addr case-insensitively.
### Tests
- Port [Secure-Join](https://securejoin.readthedocs.io/) tests to JSON-RPC.
### CI
- Test with Rust 1.74.
## [1.131.4] - 2023-11-16
### Documentation
- Document DC_DOWNLOAD_UNDECIPHERABLE.
### Fixes
- Always add "Member added" as system message.
## [1.131.3] - 2023-11-15
### Fixes
@@ -3210,3 +3250,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.131.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.0...v1.131.1
[1.131.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.1...v1.131.2
[1.131.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.2...v1.131.3
[1.131.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.3...v1.131.4
[1.131.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.4...v1.131.5
+5
View File
@@ -86,6 +86,11 @@ For example:
.with_context(|| format!("Unable to trash message {msg_id}"))
```
All errors should be handled in one of these ways:
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
- With `.log_err().ok()`.
- Bubbled up with `?`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Generated
+45 -44
View File
@@ -585,9 +585,9 @@ dependencies = [
[[package]]
name = "cargo-platform"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12024c4645c97566567129c204f65d5815a8c9aecf30fcbe682b2fe034996d36"
checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff"
dependencies = [
"serde",
]
@@ -931,9 +931,9 @@ dependencies = [
[[package]]
name = "crypto-bigint"
version = "0.5.4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core 0.6.4",
@@ -1087,7 +1087,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.131.3"
version = "1.131.5"
dependencies = [
"ansi_term",
"anyhow",
@@ -1165,7 +1165,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.131.3"
version = "1.131.5"
dependencies = [
"anyhow",
"async-channel 2.1.0",
@@ -1189,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.131.3"
version = "1.131.5"
dependencies = [
"ansi_term",
"anyhow",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.131.3"
version = "1.131.5"
dependencies = [
"anyhow",
"deltachat",
@@ -1229,7 +1229,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.131.3"
version = "1.131.5"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,15 +1479,15 @@ dependencies = [
[[package]]
name = "ecdsa"
version = "0.16.8"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der 0.7.8",
"digest 0.10.7",
"elliptic-curve 0.13.6",
"elliptic-curve 0.13.8",
"rfc6979 0.4.0",
"signature 2.1.0",
"signature 2.2.0",
"spki 0.7.2",
]
@@ -1508,7 +1508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8 0.10.2",
"signature 2.1.0",
"signature 2.2.0",
]
[[package]]
@@ -1528,14 +1528,15 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0"
dependencies = [
"curve25519-dalek 4.1.1",
"ed25519 2.2.3",
"serde",
"sha2 0.10.8",
"subtle",
"zeroize",
]
@@ -1578,12 +1579,12 @@ dependencies = [
[[package]]
name = "elliptic-curve"
version = "0.13.6"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct 0.2.0",
"crypto-bigint 0.5.4",
"crypto-bigint 0.5.5",
"digest 0.10.7",
"ff 0.13.0",
"generic-array",
@@ -1763,9 +1764,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.6"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
dependencies = [
"libc",
"windows-sys",
@@ -1899,9 +1900,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a56f0780318174bad1c127063fd0c5fdfb35398e3cd79ffaab931a6c79df80"
checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7"
[[package]]
name = "filetime"
@@ -3159,8 +3160,8 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa 0.16.8",
"elliptic-curve 0.13.6",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"sha2 0.10.8",
]
@@ -3182,8 +3183,8 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
dependencies = [
"ecdsa 0.16.8",
"elliptic-curve 0.13.6",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"sha2 0.10.8",
]
@@ -3280,8 +3281,8 @@ dependencies = [
"derive_builder",
"des",
"digest 0.10.7",
"ed25519-dalek 2.0.0",
"elliptic-curve 0.13.6",
"ed25519-dalek 2.1.0",
"elliptic-curve 0.13.8",
"flate2",
"generic-array",
"hex",
@@ -3300,7 +3301,7 @@ dependencies = [
"sha1",
"sha2 0.10.8",
"sha3",
"signature 2.1.0",
"signature 2.2.0",
"smallvec",
"thiserror",
"twofish",
@@ -3500,11 +3501,11 @@ dependencies = [
[[package]]
name = "primeorder"
version = "0.13.3"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve 0.13.6",
"elliptic-curve 0.13.8",
]
[[package]]
@@ -4000,7 +4001,7 @@ dependencies = [
"pkcs1 0.7.5",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"signature 2.1.0",
"signature 2.2.0",
"spki 0.7.2",
"subtle",
"zeroize",
@@ -4058,9 +4059,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.24"
version = "0.38.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
dependencies = [
"bitflags 2.4.1",
"errno",
@@ -4071,9 +4072,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.8"
version = "0.21.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9"
dependencies = [
"ring 0.17.5",
"rustls-webpki",
@@ -4463,9 +4464,9 @@ dependencies = [
[[package]]
name = "signature"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core 0.6.4",
@@ -5257,9 +5258,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.5.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
"getrandom 0.2.11",
"serde",
@@ -5698,9 +5699,9 @@ dependencies = [
[[package]]
name = "zeroize"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.131.3"
version = "1.131.5"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.131.3"
version = "1.131.5"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+2 -2
View File
@@ -2561,7 +2561,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
* @return The text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
@@ -2597,7 +2597,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
*
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* @memberof dc_context_t
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.131.3"
version = "1.131.5"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
+2 -2
View File
@@ -678,7 +678,7 @@ impl CommandApi {
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://countermitm.readthedocs.io/en/latest/new.html
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// return format: `[code, svg]`
@@ -707,7 +707,7 @@ impl CommandApi {
///
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
///
/// See https://countermitm.readthedocs.io/en/latest/new.html
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// **qr**: The text of the scanned QR code. Typically, the same string as given
+1 -1
View File
@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.131.3"
"version": "1.131.5"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.131.3"
version = "1.131.5"
license = "MPL-2.0"
edition = "2021"
View File
+4 -4
View File
@@ -40,13 +40,13 @@ def main():
while True:
event = account.wait_for_event()
if event["type"] == EventType.INFO:
if event["kind"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
elif event["kind"] == EventType.WARNING:
logging.warning("%s", event["msg"])
elif event["type"] == EventType.ERROR:
elif event["kind"] == EventType.ERROR:
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
elif event["kind"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
process_messages()
@@ -218,7 +218,7 @@ class Account:
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code.
"""
@@ -271,6 +271,18 @@ class Account:
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
@@ -195,7 +195,7 @@ class Client:
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
"""Simple bot implementation that listens to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")
@@ -42,6 +42,10 @@ class Message:
return AttrDict(reactions)
return None
def get_sender_contact(self) -> Contact:
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
def mark_seen(self) -> None:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])
+155 -44
View File
@@ -9,20 +9,14 @@ def test_qr_setup_contact(acfactory) -> None:
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
@@ -35,24 +29,31 @@ def test_qr_securejoin(acfactory):
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# Check that at least some of the handshake messages are deleted.
for ac in [alice, bob]:
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
@@ -97,10 +98,7 @@ def test_qr_readreceipt(acfactory) -> None:
charlie.secure_join(qr_code)
for joiner in [bob, charlie]:
while True:
event = joiner.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
joiner.wait_for_securejoin_joiner_success()
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
@@ -163,10 +161,7 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
ac1.wait_for_securejoin_inviter_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -174,10 +169,7 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
ac1.wait_for_securejoin_inviter_success()
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -186,10 +178,7 @@ def test_verified_group_recovery(acfactory) -> None:
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
ac3.wait_for_securejoin_inviter_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
@@ -236,10 +225,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
ac1.wait_for_securejoin_inviter_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -247,10 +233,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
ac1.wait_for_securejoin_inviter_success()
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -259,10 +242,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
ac3.wait_for_securejoin_inviter_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
@@ -318,3 +298,134 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code, _svg = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
qr_code, _svg = vg.get_qr_code()
ac4.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
while 1:
ev = ac2.wait_for_event()
if "added by unrelated SecureJoin" in str(ev):
return
def test_qr_new_group_unblocked(acfactory):
"""Regression test for a bug introduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
qr_code, _svg = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code, _svg = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
logging.info("sending first message")
msg_out = chat.send_text("old address").get_snapshot()
logging.info("receiving first message")
ac2.wait_for_incoming_msg_event() # member added message
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
ac1.configure()
ac1.start_io()
logging.info("sending second message")
msg_out = chat.send_text("changed address").get_snapshot()
logging.info("receiving second message")
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.131.3"
version = "1.131.5"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
+1 -1
View File
@@ -56,5 +56,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.131.3"
"version": "1.131.5"
}
+3 -157
View File
@@ -1,6 +1,6 @@
=========================
DeltaChat Python bindings
=========================
============================
CFFI Python Bindings
============================
This package provides `Python bindings`_ to the `deltachat-core library`_
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
@@ -8,157 +8,3 @@ a low-level Chat/Contact/Message API to user interfaces and bots.
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
.. _`Python bindings`: https://py.delta.chat/
Installing pre-built packages (Linux-only)
==========================================
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
We recommend to first create a fresh Python virtual environment
and activate it in your shell::
python -m venv env
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
your system installation alone.
For Linux we build wheels for all releases and push them to a python package
index. To install the latest release::
pip install deltachat
To verify it worked::
python -c "import deltachat"
Running tests
=============
Recommended way to run tests is using `scripts/run-python-test.sh`
script provided in the core repository.
This script compiles the library in debug mode and runs the tests using `tox`_.
By default it will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _`tox`: https://tox.wiki
.. _livetests:
Running "live" tests with temporary accounts
--------------------------------------------
If you want to run live functional tests
you can set ``CHATMAIL_DOMAIN`` to a domain of the email server
that creates e-mail accounts like this::
export CHATMAIL_DOMAIN=nine.testrun.org
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the server.
These accounts have the pattern `ci-{6 characters}@{CHATMAIL_DOMAIN}`.
After setting the variable, either rerun `scripts/run-python-test.sh`
or run offline and online tests with `tox` directly::
tox -e py
Each test run creates new accounts.
Developing the bindings
-----------------------
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=debug
tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed
in editable mode (as if installed with `python -m pip install -e`)
together with the testing dependencies like `pytest` and its plugins.
You can then edit the source code in the development tree
and quickly run `pytest` manually without waiting for `tox`
to recreating the virtual environment each time.
.. _sourceinstall:
Installing bindings from source
===============================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
First, build the core library::
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it:
python -m venv env
source env/bin/activate
Build and install the bindings:
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=release
python -m pip install ./python
`DCC_RS_DEV` environment variable specifies the location of
the core development tree. If this variable is not set,
`libdeltachat` library and `deltachat.h` header are expected
to be installed system-wide.
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
the build profile name to look up the artifacts
in the target directory.
In this case setting it can be skipped because
`DCC_RS_TARGET=release` is the default.
Building manylinux based wheels
===============================
Building portable manylinux wheels which come with libdeltachat.so
can be done with Docker_ or Podman_.
.. _Docker: https://www.docker.com/
.. _Podman: https://podman.io/
If you want to build your own wheels, build container image first::
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
$ docker build -t deltachat/coredeps scripts/coredeps
This will use the ``scripts/coredeps/Dockerfile`` to build
container image called ``deltachat/coredeps``. You can afterwards
find it with::
$ docker images
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e CHATMAIL_DOMAIN \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh
-197
View File
@@ -1,197 +0,0 @@
# Makefile for Sphinx documentation
#
VERSION = $(shell python -c "import conf ; print(conf.version)")
DOCZIP = devpi-$(VERSION).doc.zip
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
RSYNCOPTS = -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
export HOME=/tmp/home
export TESTHOME=$(HOME)
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# This variable is not auto generated as the order is important.
USER_MAN_CHAPTERS = commands\
user\
indices\
packages\
# userman/index.rst\
# userman/devpi_misc.rst\
# userman/devpi_concepts.rst\
#export DEVPI_CLIENTDIR=$(CURDIR)/.tmp_devpi_user_man/client
#export DEVPI_SERVERDIR=$(CURDIR)/.tmp_devpi_user_man/server
chapter = commands
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp \
epub latex latexpdf text man changes linkcheck doctest gettext install \
quickstart-releaseprocess quickstart-pypimirror quickstart-server regen \
prepare-quickstart\
regen.server-fresh regen.server-restart regen.server-clean\
regen.uman-all regen.uman
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo
@echo "User Manual Regen Targets"
@echo " regen.uman regenerates page. of the user manual chapeter e.g. regen.uman chapter=..."
@echo " regen.uman-all regenerates the user manual"
@echo " regen.uman-clean stop temp server and clean up directory"
@echo " Chapter List: $(USER_MAN_CHAPTERS)"
clean:
-rm -rf $(BUILDDIR)/*
version:
@echo "version $(VERSION)"
doczip: html
python doczip.py $(DOCZIP) _build/html
install: html
rsync -avz $(RSYNCOPTS) _build/html/ delta@py.delta.chat:build/master
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/devpi.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/devpi.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/devpi"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/devpi"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
-17
View File
@@ -1,17 +0,0 @@
<div class="globaltoc">
<ul>
<li><a href="{{ pathto('index') }}">index</a></li>
<li><a href="{{ pathto('install') }}">install</a></li>
<li><a href="{{ pathto('api') }}">high level API</a></li>
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
</ul>
<b>external links:</b>
<ul>
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
<li><a href="https://web.libera.chat/#deltachat">#deltachat</a></li>
</ul>
</div>
-1
View File
@@ -1 +0,0 @@
<h3>deltachat {{release}}</h3>
+5 -20
View File
@@ -1,5 +1,4 @@
high level API reference
High Level API Reference
========================
- :class:`deltachat.Account` (your main entry point, creates the
@@ -8,28 +7,14 @@ high level API reference
- :class:`deltachat.Chat`
- :class:`deltachat.Message`
Account
-------
.. autoclass:: deltachat.Account
:members:
Contact
-------
:members:
.. autoclass:: deltachat.Contact
:members:
Chat
----
:members:
.. autoclass:: deltachat.Chat
:members:
Message
-------
:members:
.. autoclass:: deltachat.Message
:members:
:members:
@@ -1,11 +1,10 @@
examples
Examples
========
Once you have :doc:`installed deltachat bindings <install>`
you need email/password credentials for an IMAP/SMTP account.
Delta Chat developers and the CI system use a special URL to create
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
temporary email accounts on `testrun.org <https://testrun.org/>`_ for testing.
Receiving a Chat message from the command line
----------------------------------------------
@@ -16,11 +15,11 @@ Here is a simple bot that:
- terminates the bot if the message `/quit` is sent
.. include:: ../examples/echo_and_quit.py
.. include:: ../../examples/echo_and_quit.py
:literal:
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
by specifying a database path, an email address and password of
a SMTP-IMAP account::
$ cd examples
@@ -40,11 +39,11 @@ Here is a simple bot that:
- tracks member additions and removals for all chat groups
.. include:: ../examples/group_tracking.py
.. include:: ../../examples/group_tracking.py
:literal:
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
by specifying a database path, an email address and password of
a SMTP-IMAP account::
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
+80
View File
@@ -0,0 +1,80 @@
Install
=======
Installing pre-built packages (Linux-only)
------------------------------------------
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
We recommend to first create a fresh Python virtual environment
and activate it in your shell::
python -m venv env
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
your system installation alone.
For Linux we build wheels for all releases and push them to a python package
index. To install the latest release::
pip install deltachat
To verify it worked::
python -c "import deltachat"
.. _sourceinstall:
Installing bindings from source
-------------------------------
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
First, build the core library::
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it::
python -m venv env
source env/bin/activate
Build and install the bindings::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=release
python -m pip install ./python
`DCC_RS_DEV` environment variable specifies the location of
the core development tree. If this variable is not set,
`libdeltachat` library and `deltachat.h` header are expected
to be installed system-wide.
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
the build profile name to look up the artifacts
in the target directory.
In this case setting it can be skipped because
`DCC_RS_TARGET=release` is the default.
+11
View File
@@ -0,0 +1,11 @@
Introduction
============
CFFI bindings are available via the `deltachat <https://pypi.org/project/deltachat/>`_ Python package.
The package contains both the Python bindings and the Delta Chat core.
It is provided only for Linux.
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
low-level CFFI bindings to the C interface of the Delta Chat core
and high-level Python bindings built on top of CFFI bindings.
@@ -1,8 +1,7 @@
Low Level API Reference
=======================
low level API reference
===================================
for full doxygen-generated C-docs, defines and functions please checkout
For full doxygen-generated C-docs, defines and functions please checkout
https://c.delta.chat
+25
View File
@@ -0,0 +1,25 @@
Building Manylinux-Based Wheels
===============================
Building portable manylinux wheels which come with libdeltachat.so
can be done with Docker_ or Podman_.
.. _Docker: https://www.docker.com/
.. _Podman: https://podman.io/
If you want to build your own wheels, build container image first::
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
$ docker build -t deltachat/coredeps scripts/coredeps
This will use the ``scripts/coredeps/Dockerfile`` to build
container image called ``deltachat/coredeps``. You can afterwards
find it with::
$ docker images
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e CHATMAIL_DOMAIN \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh
+49
View File
@@ -0,0 +1,49 @@
Running Tests
=============
Recommended way to run tests is using `scripts/run-python-test.sh`
script provided in the core repository.
This script compiles the library in debug mode and runs the tests using `tox`_.
By default it will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real email servers.
.. _`tox`: https://tox.wiki
.. _livetests:
Running "Live" Tests With Temporary Accounts
--------------------------------------------
If you want to run live functional tests
you can set ``CHATMAIL_DOMAIN`` to a domain of the email server
that creates email accounts like this::
export CHATMAIL_DOMAIN=nine.testrun.org
With this account-creation setting, pytest runs create ephemeral email accounts on the server.
These accounts have the pattern `ci-{6 characters}@{CHATMAIL_DOMAIN}`.
After setting the variable, either rerun `scripts/run-python-test.sh`
or run offline and online tests with `tox` directly::
tox -e py
Each test run creates new accounts.
Developing the Bindings
-----------------------
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=debug
tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed
in editable mode (as if installed with `python -m pip install -e`)
together with the testing dependencies like `pytest` and its plugins.
You can then edit the source code in the development tree
and quickly run `pytest` manually without waiting for `tox`
to recreating the virtual environment each time.
-4
View File
@@ -1,4 +0,0 @@
Changelog for deltachat-core's Python bindings
==============================================
.. include:: ../CHANGELOG
+71 -130
View File
@@ -1,138 +1,94 @@
# -*- coding: utf-8 -*-
#
# devpi documentation build configuration file, created by
# sphinx-quickstart on Mon Jun 3 16:11:22 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
from pathlib import Path
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
from deltachat import __version__ as release
version = ".".join(release.split(".")[:2])
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
#'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.viewcode',
'breathe',
#'sphinx.ext.githubpages',
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.viewcode",
"breathe",
"sphinx_rtd_theme",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'deltachat'
copyright = u'2020, holger krekel and contributors'
project = "Delta Chat"
copyright = "2023, Delta Chat contributors"
author = "Delta Chat contributors"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['sketch', '_build', "attic"]
exclude_patterns = ["sketch", "_build", "attic", "Thumbs.db", ".DS_Store"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- breathe options ------
breathe_projects = {
"deltachat": "../../docs/xml/"
}
breathe_projects = {"deltachat": Path("../../docs/xml/")}
breathe_default_project = "deltachat"
# -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'flask'
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'logo': '_static/delta-chat.svg',
'font_size': "1.1em",
'caption_font_size': "0.9em",
'code_font_size': "1.1em",
}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = ["_themes"]
html_theme = "sphinx_rtd_theme"
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
@@ -141,51 +97,34 @@ html_logo = "_static/delta-chat.svg"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = '_static/favicon.ico'
html_favicon = "_static/favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
html_sidebars = {
'index': [
'sidebarintro.html',
'globaltoc.html',
'searchbox.html'
],
'**': [
'sidebarintro.html',
'globaltoc.html',
'relations.html',
'searchbox.html'
]
}
# html_use_smartypants = True
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
@@ -194,71 +133,65 @@ html_show_sourcelink = False
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
html_use_opensearch = 'https://doc.devpi.net'
html_use_opensearch = "https://doc.devpi.net"
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'deltachat-python'
htmlhelp_basename = "deltachat-python"
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
'pointsize': '12pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '12pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'devpi.tex', u'deltachat documentation',
u'holger krekel', 'manual'),
("index", "devpi.tex", "deltachat documentation", "holger krekel", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'deltachat', u'deltachat documentation',
[u'holger krekel'], 1)
]
man_pages = [("index", "deltachat", "deltachat documentation", ["holger krekel"], 1)]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
@@ -267,30 +200,38 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'devpi', u'devpi Documentation',
u'holger krekel', 'devpi', 'One line description of project.',
'Miscellaneous'),
(
"index",
"devpi",
"devpi Documentation",
"holger krekel",
"devpi",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
intersphinx_mapping = {"http://docs.python.org/": None}
# autodoc options
autodoc_member_order = "bysource"
# always document __init__ functions
def skip(app, what, name, obj, skip, options):
return skip
def setup(app):
app.connect("autodoc-skip-member", skip)
+35 -32
View File
@@ -1,41 +1,44 @@
deltachat python bindings
=========================
Delta Chat Python bindings, new and old
=======
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
- :doc:`api` is a high level interface to deltachat-core.
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
<https://github.com/deltachat/deltachat-core-rust>`_.
getting started
---------------
`Delta Chat <https://delta.chat/>`_ provides two kinds of Python bindings
to the `Rust Core <https://github.com/deltachat/deltachat-core-rust>`_:
JSON-RPC bindings and CFFI bindings.
When starting a new project it is recommended to use JSON-RPC bindings,
which are used in the Delta Chat Desktop app through generated Typescript-bindings.
The Python JSON-RPC bindings are maintained by Delta Chat core developers.
Most existing bot projects and many tests in Delta Chat's own core library
still use the CFFI-bindings, and it is going to be maintained certainly also in 2024.
New APIs might however only appear in the JSON-RPC bindings,
as the CFFI bindings are increasingly in maintenance-only mode.
.. toctree::
:maxdepth: 2
:caption: JSON-RPC Bindings
install
examples
jsonrpc/intro
jsonrpc/install
jsonrpc/examples
jsonrpc/reference
jsonrpc/develop
.. toctree::
:hidden:
:maxdepth: 2
:caption: CFFI Bindings
links
changelog
api
lapi
plugins
..
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
cffi/intro
cffi/install
cffi/examples
cffi/manylinux
cffi/tests
cffi/api
cffi/lapi
cffi/plugins
.. _`deltachat`: https://delta.chat
.. _`deltachat-core repo`: https://github.com/deltachat
.. _pip: http://pypi.org/project/pip/
.. _virtualenv: http://pypi.org/project/virtualenv/
.. _merlinux: http://merlinux.eu
.. _pypi: http://pypi.org/
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust
-2
View File
@@ -1,2 +0,0 @@
.. include:: ../README.rst
+68
View File
@@ -0,0 +1,68 @@
===========
Development
===========
To develop JSON-RPC bindings,
clone the `deltachat-core-rust <https://github.com/deltachat/deltachat-core-rust/>`_ repository::
git clone https://github.com/deltachat/deltachat-core-rust.git
Testing
=======
To run online tests, set ``CHATMAIL_DOMAIN``
to a domain of the email server
that can be used to create testing accounts::
export CHATMAIL_DOMAIN=nine.testrun.org
Then run ``scripts/run-rpc-test.sh``
to build debug version of ``deltachat-rpc-server``
and run ``deltachat-rpc-client`` tests
in a separate virtual environment managed by `tox <https://tox.wiki/>`_.
Development Environment
=======================
Creating a new virtual environment
to run the tests each time
as ``scripts/run-rpc-test.sh`` does is slow
if you are changing the tests or the code
and want to rerun the tests each time.
If you are developing the tests,
it is better to create a persistent virtual environment.
You can do this by running ``scripts/make-rpc-testenv.sh``.
This creates a virtual environment ``venv`` which you can then enter with::
. venv/bin/activate
Then you can run the tests with
::
pytest deltachat-rpc-client/tests/
Refer to `pytest documentation <https://docs.pytest.org/>` for details.
If make the changes to Delta Chat core
or Python bindings, you can rebuild the environment by rerunning
``scripts/make-rpc-testenv.sh``.
It is ok to rebuild the activated environment this way,
you do not need to deactivate or reactivate the environment each time.
Using REPL
==========
Once you have a development environment,
you can quickly test things in REPL::
$ python
>>> from deltachat_rpc_client import *
>>> rpc = Rpc()
>>> rpc.start()
>>> dc = DeltaChat(rpc)
>>> system_info = dc.get_system_info()
>>> system_info["level"]
'awesome'
>>> rpc.close()
+19
View File
@@ -0,0 +1,19 @@
Examples
========
Echo bot
--------
.. include:: ../../../deltachat-rpc-client/examples/echobot_no_hooks.py
:literal:
Echo bot with hooks
-------------------
.. include:: ../../../deltachat-rpc-client/examples/echobot.py
:literal:
Advanced echo bot
-----------------
.. include:: ../../../deltachat-rpc-client/examples/echobot_advanced.py
:literal:
+36
View File
@@ -0,0 +1,36 @@
Install
=======
To use JSON-RPC bindings for Delta Chat core you will need
a ``deltachat-rpc-server`` binary which provides Delta Chat core API over JSON-RPC
and a ``deltachat-rpc-client`` Python package which is a JSON-RPC client that starts ``deltachat-rpc-server`` process and uses JSON-RPC API.
`Create a virtual environment <https://docs.python.org/3/library/venv.html>`__ if you
don’t have one already and activate it::
$ python -m venv venv
$ . venv/bin/activate
Install ``deltachat-rpc-server``
--------------------------------
To get ``deltachat-rpc-server`` binary you have three options:
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``.
3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``.
Check that ``deltachat-rpc-server`` is installed and can run::
$ deltachat-rpc-server --version
1.131.4
Then install ``deltachat-rpc-client`` with ``pip install deltachat-rpc-client``.
Install ``deltachat-rpc-client``
--------------------------------
To get ``deltachat-rpc-client`` Python library you can:
1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``.
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``.
+8
View File
@@ -0,0 +1,8 @@
Introduction
============
JSON-RPC bindings are available via the `deltachat-rpc-client <https://pypi.org/project/deltachat-rpc-client/>`_ Python package.
This package provides only the Python bindings and requires ``deltachat-rpc-server`` binary to be installed.
`deltachat-rpc-server <https://pypi.org/project/deltachat-rpc-server/>`_ package provides ``deltachat-rpc-server`` binary for Linux, Windows, macOS and Android.
RPC client connects to standalone Delta Chat RPC server ``deltachat-rpc-server`` and provides Python interface to it.
+5
View File
@@ -0,0 +1,5 @@
API Reference
=============
.. automodule:: deltachat_rpc_client
:members:
-11
View File
@@ -1,11 +0,0 @@
links
================================
.. _`deltachat`: https://delta.chat
.. _`deltachat-core repo`: https://github.com/deltachat
.. _pip: http://pypi.org/project/pip/
.. _virtualenv: http://pypi.org/project/virtualenv/
.. _merlinux: http://merlinux.eu
.. _pypi: http://pypi.org/
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core
-190
View File
@@ -1,190 +0,0 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\devpi.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\devpi.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end
+1 -164
View File
@@ -9,7 +9,7 @@ import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message, Chat
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
@@ -1658,128 +1658,6 @@ def test_ac_setup_message_twice(acfactory, lp):
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
def test_qr_setup_contact(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
qr = ac1.get_setup_contact_qr()
lp.sec("ac2: start QR-code based setup contact protocol")
ch = ac2.qr_setup_contact(qr)
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ch = ac2.qr_join_chat(qr)
lp.sec("ac2: qr_join_chat() returned")
assert ch.id >= 10
# check that at least some of the handshake messages are deleted
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
# ac1 reloads the chat.
chat = Chat(chat.account, chat.id)
assert not chat.is_protected()
# ac2 reloads the chat.
ch = Chat(ch.account, ch.id)
assert not ch.is_protected()
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory, lp):
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
lp.sec("ac3: verify with ac2")
ac3.qr_setup_contact(ac2.get_setup_contact_qr())
ac2._evtracker.wait_securejoin_inviter_progress(1000)
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
lp.sec("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group_chat("ac1-shutoff group", verified=True)
ac2.qr_join_chat(ch1.get_join_qr())
ac1._evtracker.wait_securejoin_inviter_progress(1000)
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
msg = ac2.wait_next_incoming_message()
if msg.text == "ac1 says hello":
assert msg.chat.is_protected()
break
lp.sec("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
ac2.qr_join_chat(ch1.get_join_qr())
ac1.shutdown()
lp.sec("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.get_contact(ac2).is_verified()
assert ac2.get_contact(ac3).is_verified()
lp.sec("ac3: create a verified group VG with ac2")
vg = ac3.create_group_chat("ac3-created", [ac2], verified=True)
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.wait_next_incoming_message()
if msg.text == "hello":
assert msg.chat.is_protected()
break
lp.sec("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
ac4.qr_join_chat(vg.get_join_qr())
ac3._evtracker.wait_securejoin_inviter_progress(1000)
while 1:
ev = ac2._evtracker.get()
if "added by unrelated SecureJoin" in str(ev):
return
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
@@ -2527,47 +2405,6 @@ def test_delete_deltachat_folder(acfactory):
assert "DeltaChat" in ac1.direct_imap.list_folders()
def test_aeap_flow_verified(acfactory, lp):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
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)
assert chat2.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("sending first message")
msg_out = chat.send_text("old address")
lp.sec("receiving first message")
ac2._evtracker.wait_next_incoming_message() # member added message
msg_in_1 = ac2._evtracker.wait_next_incoming_message()
assert msg_in_1.text == msg_out.text
lp.sec("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
configtracker = ac1.configure()
configtracker.wait_finish()
ac1.start_io()
lp.sec("sending second message")
msg_out = chat.send_text("changed address")
lp.sec("receiving second message")
msg_in_2 = ac2._evtracker.wait_next_incoming_message()
assert msg_in_2.text == msg_out.text
assert msg_in_2.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().addr == ac1new.get_config("addr")
assert len(msg_in_2.chat.get_contacts()) == 2
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.
+2 -2
View File
@@ -62,9 +62,9 @@ commands =
[testenv:doc]
changedir=doc
deps =
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
sphinx<=7.1.2
sphinx
breathe
sphinx_rtd_theme
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
+1 -1
View File
@@ -1 +1 @@
2023-11-15
2023-11-20
+2
View File
@@ -39,6 +39,8 @@ and an own build machine.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
python3 -m venv venv
venv/bin/pip install ./python
venv/bin/pip install ./deltachat-rpc-client
venv/bin/pip install sphinx breathe sphinx_rtd_theme
venv/bin/pip install ./deltachat-rpc-client
venv/bin/sphinx-build -b html -a python/doc/ dist/html
-24
View File
@@ -102,8 +102,6 @@ jobs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-docs
path: ./python/doc/_build/
# Binary wheels
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
@@ -115,28 +113,6 @@ jobs:
- |
scripts/run_all.sh
# Upload python docs to py.delta.chat
- task: upload-py-docs
config:
inputs:
- name: py-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
# Upload x86_64 wheels and source packages
- task: upload-wheels
config:
+1 -7
View File
@@ -1,6 +1,6 @@
#!/bin/sh
#
# Build the Delta Chat Core Rust library, Python wheels and docs
# Build the Delta Chat Core Rust library and Python wheels
set -e -x
@@ -34,9 +34,3 @@ unset CHATMAIL_DOMAIN
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
echo -----------------------
echo generating python docs
echo -----------------------
tox --workdir "$TOXWORKDIR" -e doc
+12 -1
View File
@@ -67,7 +67,7 @@ impl Aheader {
impl fmt::Display for Aheader {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "addr={};", self.addr)?;
write!(fmt, "addr={};", self.addr.to_lowercase())?;
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
@@ -262,5 +262,16 @@ mod tests {
)
)
.contains("prefer-encrypt"));
// Always lowercase the address in the header.
assert!(format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
)
.contains("test@example.com"));
}
}
+19 -13
View File
@@ -1935,11 +1935,18 @@ impl Chat {
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
let mut addrs = Vec::new();
for contact_id in get_chat_contacts(context, self.id).await? {
let contact = Contact::get_by_id(context, contact_id).await?;
addrs.push(contact.get_addr().to_string());
}
let addrs = context
.sql
.query_map(
"SELECT c.addr \
FROM contacts c INNER JOIN chats_contacts cc \
ON c.id=cc.contact_id \
WHERE cc.chat_id=?",
(self.id,),
|row| row.get::<_, String>(0),
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
self.sync(context, SyncAction::SetContacts(addrs)).await
}
@@ -3454,8 +3461,8 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
let contact_addr = contact.get_addr();
msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
let contact_addr = contact.get_addr().to_lowercase();
msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
@@ -3626,7 +3633,7 @@ pub async fn remove_contact_from_chat(
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.id = send_msg(context, chat_id, &mut msg).await?;
} else {
sync = Sync;
@@ -3685,7 +3692,6 @@ async fn rename_ex(
chat_id: ChatId,
new_name: &str,
) -> Result<()> {
let sync_name = new_name;
let new_name = improve_single_line_input(new_name);
/* the function only sets the names of group chats; normal chats get their names from the contacts */
let mut success = false;
@@ -3738,7 +3744,7 @@ async fn rename_ex(
bail!("Failed to set name");
}
if sync.into() && chat.name != new_name {
let sync_name = sync_name.to_string();
let sync_name = new_name.to_string();
chat.sync(context, SyncAction::Rename(sync_name))
.await
.log_err(context)
@@ -4227,7 +4233,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
let mut contacts = HashSet::new();
for addr in addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact = Contact::add_or_lookup(context, "", contact_addr, Origin::Hidden)
let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
.await?
.0;
contacts.insert(contact);
@@ -5585,7 +5591,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
&ContactAddress::new("foo@bar.org")?,
Origin::IncomingUnknownTo,
)
.await?;
@@ -6596,7 +6602,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
&ContactAddress::new("foo@bar.org")?,
Origin::ManuallyCreated,
)
.await?;
+20
View File
@@ -513,6 +513,11 @@ impl Context {
);
self.sql.set_raw_config(key.as_ref(), value).await?;
}
Config::Addr => {
self.sql
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -662,6 +667,21 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_addr() {
let t = TestContext::new().await;
// Test that uppercase address get lowercased.
assert!(t
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok());
assert_eq!(
t.get_config(Config::Addr).await.unwrap().unwrap(),
"foobar@example.org"
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {
+47 -49
View File
@@ -43,43 +43,43 @@ use crate::{chat, stock_str};
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// Valid contact address.
#[derive(Debug, Clone, Copy)]
pub(crate) struct ContactAddress<'a>(&'a str);
#[derive(Debug, Clone)]
pub(crate) struct ContactAddress(String);
impl Deref for ContactAddress<'_> {
impl Deref for ContactAddress {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
&self.0
}
}
impl AsRef<str> for ContactAddress<'_> {
impl AsRef<str> for ContactAddress {
fn as_ref(&self) -> &str {
self.0
&self.0
}
}
impl fmt::Display for ContactAddress<'_> {
impl fmt::Display for ContactAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> ContactAddress<'a> {
impl ContactAddress {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &'a str) -> Result<Self> {
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(addr) {
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr))
Ok(Self(addr.to_string()))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress<'_> {
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);
@@ -500,7 +500,7 @@ impl Contact {
let addr = ContactAddress::new(&addr)?;
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated)
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated)
.await
.context("add_or_lookup")?;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
@@ -565,7 +565,7 @@ impl Contact {
let addr_normalized = addr_normalize(addr);
if context.is_self_addr(addr_normalized).await? {
if context.is_self_addr(&addr_normalized).await? {
return Ok(Some(ContactId::SELF));
}
@@ -615,7 +615,7 @@ impl Contact {
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: ContactAddress<'_>,
addr: &ContactAddress,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
@@ -623,7 +623,7 @@ impl Contact {
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
if context.is_self_addr(&addr).await? {
if context.is_self_addr(addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
@@ -778,7 +778,7 @@ impl Contact {
} else {
"".to_string()
},
addr,
&addr,
origin,
if update_authname {
name.to_string()
@@ -823,7 +823,7 @@ impl Contact {
let name = normalize_name(&name);
match ContactAddress::new(&addr) {
Ok(addr) => {
match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await {
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
@@ -1317,7 +1317,7 @@ impl Contact {
return Ok(None);
};
if verifier_addr == self.addr {
if addr_cmp(&verifier_addr, &self.addr) {
// Contact is directly verified via QR code.
return Ok(Some(ContactId::SELF));
}
@@ -1405,12 +1405,13 @@ pub fn may_be_valid_addr(addr: &str) -> bool {
res.is_ok()
}
/// Returns address with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> &str {
let norm = addr.trim();
/// Returns address lowercased,
/// with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> String {
let norm = addr.trim().to_lowercase();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(norm)
norm.get(7..).unwrap_or(&norm).to_string()
} else {
norm
}
@@ -1665,8 +1666,8 @@ 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();
let norm1 = addr_normalize(addr1);
let norm2 = addr_normalize(addr2);
norm1 == norm2
}
@@ -1864,10 +1865,7 @@ mod tests {
fn test_normalize_addr() {
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
// normalisation preserves case to allow user-defined spelling.
// however, case is ignored on addr_cmp()
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
assert_eq!(addr_normalize("John@Doe.com"), "john@doe.com");
}
#[test]
@@ -1893,7 +1891,7 @@ mod tests {
let (id, _modified) = Contact::add_or_lookup(
&context.ctx,
"bob",
ContactAddress::new("user@example.org")?,
&ContactAddress::new("user@example.org")?,
Origin::IncomingReplyTo,
)
.await?;
@@ -1921,7 +1919,7 @@ mod tests {
let (contact_bob_id, modified) = Contact::add_or_lookup(
&context.ctx,
"someone",
ContactAddress::new("user@example.org")?,
&ContactAddress::new("user@example.org")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1986,7 +1984,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bla foo",
ContactAddress::new("one@eins.org").unwrap(),
&ContactAddress::new("one@eins.org").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2005,7 +2003,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"Real one",
ContactAddress::new(" one@eins.org ").unwrap(),
&ContactAddress::new(" one@eins.org ").unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -2021,7 +2019,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("three@drei.sam").unwrap(),
&ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2038,7 +2036,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"m. serious",
ContactAddress::new("three@drei.sam").unwrap(),
&ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2053,7 +2051,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"schnucki",
ContactAddress::new("three@drei.sam").unwrap(),
&ContactAddress::new("three@drei.sam").unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -2069,7 +2067,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("alice@w.de").unwrap(),
&ContactAddress::new("alice@w.de").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2211,7 +2209,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -2290,7 +2288,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob1",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2306,7 +2304,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob2",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2332,7 +2330,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob4",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2361,7 +2359,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire1",
ContactAddress::new("claire@example.org").unwrap(),
&ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2377,7 +2375,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire2",
ContactAddress::new("claire@example.org").unwrap(),
&ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2402,7 +2400,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
ContactAddress::new("bob@example.org")?,
&ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -2414,7 +2412,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Not Bob",
ContactAddress::new("bob@example.org")?,
&ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownTo,
)
.await?;
@@ -2427,7 +2425,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
ContactAddress::new("bob@example.org")?,
&ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -2456,7 +2454,7 @@ mod tests {
Contact::add_or_lookup(
&t,
"dave2",
ContactAddress::new("dave@example.org").unwrap(),
&ContactAddress::new("dave@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2577,7 +2575,7 @@ mod tests {
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -2740,7 +2738,7 @@ CCCB 5AA9 F6E1 141C 9431
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
+2 -1
View File
@@ -413,7 +413,8 @@ impl Context {
.is_some()
{
let mut lock = self.ratelimit.write().await;
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
}
self.scheduler.start(self.clone()).await;
+1 -1
View File
@@ -2519,7 +2519,7 @@ async fn add_all_recipients_as_contacts(
let (_, modified) = Contact::add_or_lookup(
context,
&display_name_normalized,
recipient_addr,
&recipient_addr,
Origin::OutgoingTo,
)
.await?;
+1 -1
View File
@@ -1919,7 +1919,7 @@ mod tests {
let contact_id = Contact::add_or_lookup(
&t,
"Dave",
ContactAddress::new("dave@example.com").unwrap(),
&ContactAddress::new("dave@example.com").unwrap(),
Origin::ManuallyCreated,
)
.await
+1 -1
View File
@@ -648,7 +648,7 @@ impl Peerstate {
let (new_contact_id, _) = Contact::add_or_lookup(
context,
"",
new_addr,
&new_addr,
Origin::IncomingUnknownFrom,
)
.await?;
+7 -6
View File
@@ -365,9 +365,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
if let (Some(grpid), Some(grpname)) = (grpid, grpname) {
if context
@@ -432,7 +433,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let Some(peerstate) = peerstate {
let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan)
Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan)
.await
.context("add_or_lookup")?;
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
@@ -777,7 +778,7 @@ impl Qr {
) -> Result<Self> {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?;
Contact::add_or_lookup(context, name, &addr, Origin::UnhandledQrScan).await?;
Ok(Qr::Addr { contact_id, draft })
}
}
@@ -788,7 +789,7 @@ fn normalize_address(addr: &str) -> Result<String> {
let new_addr = percent_decode_str(addr).decode_utf8()?;
let new_addr = addr_normalize(&new_addr);
ensure!(may_be_valid_addr(new_addr), "Bad e-mail address");
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
Ok(new_addr.to_string())
}
+1 -1
View File
@@ -392,7 +392,7 @@ Can we chat at 1pm pacific, today?"
let bob_id = Contact::add_or_lookup(
&alice,
"",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?
+15 -14
View File
@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -636,14 +636,9 @@ async fn add_parts(
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if chat.is_protected() {
if chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.replace_msg_by_error(&s);
}
if chat.is_protected() && chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
@@ -651,6 +646,12 @@ async fn add_parts(
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
if chat.is_protected() {
// In protected chat, also mark the message with an error.
let s = stock_str::unknown_sender_for_chat(context).await;
part.error = Some(s);
}
}
}
}
@@ -1755,7 +1756,7 @@ async fn apply_group_changes(
// True if a Delta Chat client has explicitly added our current primary address.
let self_added =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
context.get_primary_self_addr().await? == *added_addr
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
} else {
false
};
@@ -2176,7 +2177,7 @@ async fn apply_mailinglist_changes(
return Ok(());
}
};
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
@@ -2476,7 +2477,7 @@ async fn mark_recipients_as_verified(
let (to_contact_id, _) = Contact::add_or_lookup(
context,
"",
ContactAddress::new(&to_addr)?,
&ContactAddress::new(&to_addr)?,
Origin::Hidden,
)
.await?;
@@ -2614,7 +2615,7 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_contact_by_addr(
context: &Context,
display_name: Option<&str>,
addr: ContactAddress<'_>,
addr: ContactAddress,
origin: Origin,
) -> Result<ContactId> {
if context.is_self_addr(&addr).await? {
@@ -2623,7 +2624,7 @@ async fn add_or_lookup_contact_by_addr(
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
let (contact_id, _modified) =
Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?;
Contact::add_or_lookup(context, &display_name_normalized, &addr, origin).await?;
Ok(contact_id)
}
+15 -17
View File
@@ -431,7 +431,7 @@ async fn test_escaped_recipients() {
let carl_contact_id = Contact::add_or_lookup(
&t,
"Carl",
ContactAddress::new("carl@host.tld").unwrap(),
&ContactAddress::new("carl@host.tld").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -477,7 +477,7 @@ async fn test_cc_to_contact() {
let carl_contact_id = Contact::add_or_lookup(
&t,
"garabage",
ContactAddress::new("carl@host.tld").unwrap(),
&ContactAddress::new("carl@host.tld").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2003,7 +2003,7 @@ async fn test_duplicate_message() -> Result<()> {
let bob_contact_id = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await?
@@ -2060,7 +2060,7 @@ async fn test_ignore_footer_status_from_mailinglist() -> Result<()> {
let bob_id = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("bob@example.net").unwrap(),
&ContactAddress::new("bob@example.net").unwrap(),
Origin::IncomingUnknownCc,
)
.await?
@@ -2139,7 +2139,7 @@ async fn test_ignore_old_status_updates() -> Result<()> {
let bob_id = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::AddressBook,
)
.await?
@@ -2623,19 +2623,17 @@ async fn test_incoming_contact_request() -> Result<()> {
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(chat.is_contact_request());
loop {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
match event {
EventType::IncomingMsg { chat_id, msg_id } => {
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.id, msg_id);
return Ok(());
}
_ => unreachable!(),
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
match event {
EventType::IncomingMsg { chat_id, msg_id } => {
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.id, msg_id);
Ok(())
}
_ => unreachable!(),
}
}
+31 -3
View File
@@ -1,4 +1,4 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
//! Verified contact protocol implementation as [specified by countermitm project](https://securejoin.readthedocs.io/en/latest/new.html#setup-contact-protocol).
use std::convert::TryFrom;
@@ -770,7 +770,7 @@ fn encrypted_and_signed(
mod tests {
use super::*;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chat::{remove_contact_from_chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
@@ -1058,7 +1058,7 @@ mod tests {
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1352,4 +1352,32 @@ First thread."#;
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unknown_sender() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.execute_securejoin(&alice, &bob).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
.await;
let sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
// The message from Bob is delivered late, Bob is already removed.
let msg = alice.recv_msg(&sent).await;
assert_eq!(msg.text, "Hi hi!");
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
Ok(())
}
}
+2 -2
View File
@@ -149,7 +149,7 @@ pub enum StockMessage {
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
#[strum(props(fallback = "Unknown sender for this chat."))]
UnknownSenderForChat = 72,
#[strum(props(fallback = "Message from %1$s"))]
@@ -930,7 +930,7 @@ pub(crate) async fn welcome_message(context: &Context) -> String {
translated(context, StockMessage::WelcomeMessage).await
}
/// Stock string: `Unknown sender for this chat. See 'info' for more details.`.
/// Stock string: `Unknown sender for this chat.`.
pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
translated(context, StockMessage::UnknownSenderForChat).await
}
+1 -1
View File
@@ -580,7 +580,7 @@ impl TestContext {
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
let (contact_id, modified) =
Contact::add_or_lookup(self, &name, addr, Origin::MailinglistAddress)
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
.await
.expect("add_or_lookup");
match modified {
+2 -2
View File
@@ -17,7 +17,7 @@ Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162][])
Client/server identification | IMAP ID extension ([RFC 2971][])
Authorization | OAuth2 ([RFC 6749][])
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Detect/prevent active attacks | [countermitm][] protocols
Detect/prevent active attacks | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
@@ -29,7 +29,7 @@ Return receipts | Message Disposition Notification (MDN, [RFC 8
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
[Autocrypt Level 1]: https://autocrypt.org/level1.html
[countermitm]: https://countermitm.readthedocs.io/en/latest/
[securejoin]: https://securejoin.readthedocs.io/en/latest/
[openpgp4fpr]: https://metacode.biz/openpgp/openpgp4fpr
[Autodiscover]: https://learn.microsoft.com/en-us/exchange/autodiscover-service-for-exchange-2013
[XEP-0392]: https://xmpp.org/extensions/xep-0392.html